aztec_pxe/execution/
oracle.rs

1//! Private execution oracle — bridges ACVM foreign calls to local stores + node RPC.
2
3use aztec_core::error::Error;
4use aztec_core::kernel_types::{
5    CallContext, ContractClassLog, CountedContractClassLog, NoteAndSlot, NoteHash, Nullifier,
6    ScopedNoteHash, ScopedNullifier, ScopedReadRequest,
7};
8use aztec_core::tx::HashedValues;
9use aztec_core::types::{AztecAddress, ContractInstance, Fr};
10use aztec_node_client::AztecNode;
11
12use super::acvm_executor::{AcvmExecutionOutput, OracleCallback};
13use super::execution_result::{
14    PrivateCallExecutionResult, PrivateExecutionResult, PrivateLogData, PublicCallRequestData,
15};
16use crate::stores::note_store::{NoteFilter, NoteStatus, StoredNote};
17use crate::stores::{
18    AddressStore, CapsuleStore, ContractStore, KeyStore, NoteStore, SenderTaggingStore,
19};
20
21/// Oracle for private function execution.
22///
23/// Handles foreign-call callbacks from the ACVM during private function
24/// execution, routing them to the appropriate local store or node RPC.
25pub struct PrivateExecutionOracle<'a, N: AztecNode> {
26    node: &'a N,
27    contract_store: &'a ContractStore,
28    key_store: &'a KeyStore,
29    note_store: &'a NoteStore,
30    capsule_store: &'a CapsuleStore,
31    address_store: &'a AddressStore,
32    sender_tagging_store: &'a SenderTaggingStore,
33    /// The block header at which execution is anchored.
34    block_header: serde_json::Value,
35    /// The address of the contract being executed.
36    contract_address: AztecAddress,
37    /// Protocol nullifier derived from the tx request hash.
38    protocol_nullifier: Fr,
39    /// Execution cache: values stored by hash during execution.
40    execution_cache: std::collections::HashMap<Fr, Vec<Fr>>,
41    /// Auth witnesses available during execution.
42    auth_witnesses: Vec<(Fr, Vec<Fr>)>,
43    /// Unconstrained sender override used by tagging during nested/private calls.
44    sender_for_tags: Option<AztecAddress>,
45    /// Execution scopes — used to enforce key validation access control.
46    scopes: Vec<AztecAddress>,
47    /// Whether the currently executing private function is in a static context.
48    call_is_static: bool,
49
50    // --- Counter-bearing side effects (matching upstream oracle) ---
51    /// Side-effect counter, incremented for each side effect.
52    pub(crate) side_effect_counter: u32,
53    /// Notes created during this call.
54    pub(crate) new_notes: Vec<NoteAndSlot>,
55    /// Scoped note hashes with counters.
56    pub(crate) note_hashes: Vec<ScopedNoteHash>,
57    /// Scoped nullifiers with counters.
58    pub(crate) nullifiers: Vec<ScopedNullifier>,
59    /// Maps note hash counter -> nullifier counter.
60    pub(crate) note_hash_nullifier_counter_map: std::collections::HashMap<u32, u32>,
61    /// Siloed nullifier values of DB notes consumed during this execution.
62    /// Used to prevent returning already-consumed persistent notes from get_notes.
63    consumed_db_nullifiers: std::collections::HashSet<Fr>,
64    /// Private logs emitted.
65    pub(crate) private_logs: Vec<PrivateLogData>,
66    /// Contract class logs emitted.
67    pub(crate) contract_class_logs: Vec<CountedContractClassLog>,
68    /// Offchain effects.
69    offchain_effects: Vec<Vec<Fr>>,
70    /// Public function call requests enqueued during private execution.
71    public_call_requests: Vec<PublicCallRequestData>,
72    /// Teardown call request.
73    public_teardown_call_request: Option<PublicCallRequestData>,
74    /// Note hash read requests.
75    pub(crate) note_hash_read_requests: Vec<ScopedReadRequest>,
76    /// Nullifier read requests.
77    pub(crate) nullifier_read_requests: Vec<ScopedReadRequest>,
78    /// Minimum revertible side-effect counter.
79    pub(crate) min_revertible_side_effect_counter: u32,
80    /// Public function calldata preimages.
81    public_function_calldata: Vec<HashedValues>,
82    /// Captured nested execution results (for return value extraction).
83    pub(crate) nested_results: Vec<PrivateCallExecutionResult>,
84    /// Block-header + tx-context fields from the entrypoint witness,
85    /// shared with nested calls so that chain_id/version are correct.
86    pub(crate) context_witness_prefix: Vec<Fr>,
87    /// Capsules from the tx request — used by protocol contract handlers
88    /// (e.g., contract class registerer) that need bytecode data.
89    capsules: Vec<aztec_core::tx::Capsule>,
90}
91
92fn decode_base64_sibling_path(encoded: &str) -> Result<Vec<Fr>, Error> {
93    use base64::Engine;
94
95    let bytes = base64::engine::general_purpose::STANDARD
96        .decode(encoded)
97        .map_err(|e| Error::InvalidData(format!("invalid siblingPath base64: {e}")))?;
98
99    let payload = if bytes.len() >= 4 {
100        let declared_len =
101            u32::from_be_bytes(bytes[..4].try_into().expect("length prefix is 4 bytes")) as usize;
102        let payload = &bytes[4..];
103        if payload.len() == declared_len.saturating_mul(32) {
104            payload
105        } else if bytes.len() % 32 == 0 {
106            bytes.as_slice()
107        } else {
108            return Err(Error::InvalidData(format!(
109                "siblingPath payload length mismatch: declared {declared_len} elements, got {} bytes",
110                payload.len()
111            )));
112        }
113    } else {
114        bytes.as_slice()
115    };
116
117    Ok(payload
118        .chunks(32)
119        .map(|chunk| {
120            let mut padded = [0u8; 32];
121            let start = 32usize.saturating_sub(chunk.len());
122            padded[start..].copy_from_slice(chunk);
123            Fr::from(padded)
124        })
125        .collect())
126}
127
128fn parse_field_string(value: &str) -> Result<Fr, Error> {
129    if value.starts_with("0x") {
130        Fr::from_hex(value)
131    } else {
132        value
133            .parse::<u128>()
134            .map(Fr::from)
135            .map_err(|_| Error::InvalidData(format!("unsupported field string value: {value}")))
136    }
137}
138
139/// A note created during execution (not yet committed to state).
140#[derive(Debug, Clone)]
141pub struct CachedNote {
142    pub contract_address: AztecAddress,
143    pub storage_slot: Fr,
144    pub note_hash: Fr,
145    pub note_data: Vec<Fr>,
146}
147
148impl<'a, N: AztecNode + 'static> PrivateExecutionOracle<'a, N> {
149    /// Extract circuit-constrained side effects from a solved ACVM witness.
150    ///
151    /// Private logs, note hashes, and nullifiers are NOT emitted via oracle calls;
152    /// they are circuit outputs embedded in the witness at known PCPI offsets.
153    fn extract_side_effects_from_witness(
154        witness: &acir::native_types::WitnessMap<acir::FieldElement>,
155        params_size: usize,
156        contract_address: AztecAddress,
157    ) -> (
158        Vec<aztec_core::kernel_types::ScopedNoteHash>,
159        Vec<aztec_core::kernel_types::ScopedNullifier>,
160        Vec<PrivateLogData>,
161    ) {
162        use aztec_core::kernel_types::{NoteHash, Nullifier, ScopedNoteHash, ScopedNullifier};
163
164        const PCPI_LENGTH: usize = 870;
165        const NOTE_HASHES_OFFSET: usize = 454;
166        const NOTE_HASH_LEN: usize = 2;
167        const MAX_NOTE_HASHES: usize = 16;
168        const NOTE_HASHES_ARRAY_LEN: usize = MAX_NOTE_HASHES * NOTE_HASH_LEN + 1;
169        const NULLIFIERS_OFFSET: usize = 487;
170        const NULLIFIER_LEN: usize = 3;
171        const MAX_NULLIFIERS: usize = 16;
172        const NULLIFIERS_ARRAY_LEN: usize = MAX_NULLIFIERS * NULLIFIER_LEN + 1;
173        const PRIVATE_LOGS_OFFSET: usize = 561;
174        const PRIVATE_LOG_DATA_LEN: usize = 19;
175        const PRIVATE_LOG_FIELDS: usize = 16;
176        const MAX_LOGS: usize = 16;
177        const PRIVATE_LOGS_ARRAY_LEN: usize = MAX_LOGS * PRIVATE_LOG_DATA_LEN + 1;
178
179        let pcpi_start = params_size;
180        let mut pcpi = Vec::with_capacity(PCPI_LENGTH);
181        for i in 0..PCPI_LENGTH {
182            let idx = acir::native_types::Witness((pcpi_start + i) as u32);
183            let val = witness
184                .get(&idx)
185                .map(|fe| super::field_conversion::fe_to_fr(fe))
186                .unwrap_or_else(Fr::zero);
187            pcpi.push(val);
188        }
189
190        // Extract note hashes
191        let nh_slice = &pcpi[NOTE_HASHES_OFFSET..][..NOTE_HASHES_ARRAY_LEN];
192        let nh_count = nh_slice[NOTE_HASHES_ARRAY_LEN - 1]
193            .to_usize()
194            .min(MAX_NOTE_HASHES);
195        let mut note_hashes = Vec::with_capacity(nh_count);
196        for i in 0..nh_count {
197            let base = i * NOTE_HASH_LEN;
198            let value = nh_slice[base];
199            let counter = nh_slice[base + 1].to_usize() as u32;
200            if value != Fr::zero() {
201                note_hashes.push(ScopedNoteHash {
202                    note_hash: NoteHash { value, counter },
203                    contract_address,
204                });
205            }
206        }
207
208        // Extract nullifiers
209        let null_slice = &pcpi[NULLIFIERS_OFFSET..][..NULLIFIERS_ARRAY_LEN];
210        let null_count = null_slice[NULLIFIERS_ARRAY_LEN - 1]
211            .to_usize()
212            .min(MAX_NULLIFIERS);
213        let mut nullifiers = Vec::with_capacity(null_count);
214        for i in 0..null_count {
215            let base = i * NULLIFIER_LEN;
216            let value = null_slice[base];
217            let note_hash = null_slice[base + 1];
218            let counter = null_slice[base + 2].to_usize() as u32;
219            if value != Fr::zero() {
220                nullifiers.push(ScopedNullifier {
221                    nullifier: Nullifier {
222                        value,
223                        note_hash,
224                        counter,
225                    },
226                    contract_address,
227                });
228            }
229        }
230
231        // Extract private logs
232        let logs_slice = &pcpi[PRIVATE_LOGS_OFFSET..][..PRIVATE_LOGS_ARRAY_LEN];
233        let log_count = logs_slice[PRIVATE_LOGS_ARRAY_LEN - 1]
234            .to_usize()
235            .min(MAX_LOGS);
236        let mut logs = Vec::with_capacity(log_count);
237        for i in 0..log_count {
238            let base = i * PRIVATE_LOG_DATA_LEN;
239            let fields: Vec<Fr> = logs_slice[base..base + PRIVATE_LOG_FIELDS].to_vec();
240            let emitted_length = logs_slice[base + PRIVATE_LOG_FIELDS].to_usize() as u32;
241            let note_hash_counter = logs_slice[base + PRIVATE_LOG_FIELDS + 1].to_usize() as u32;
242            let counter = logs_slice[base + PRIVATE_LOG_DATA_LEN - 1].to_usize() as u32;
243            if emitted_length > 0 {
244                logs.push(PrivateLogData {
245                    fields,
246                    emitted_length,
247                    note_hash_counter,
248                    counter,
249                    contract_address,
250                });
251            }
252        }
253
254        (note_hashes, nullifiers, logs)
255    }
256}
257
258impl<'a, N: AztecNode + 'static> PrivateExecutionOracle<'a, N> {
259    fn merge_nested_private_logs(
260        nested_logs: Vec<PrivateLogData>,
261        circuit_logs: Vec<PrivateLogData>,
262    ) -> Vec<PrivateLogData> {
263        if circuit_logs.is_empty() {
264            return nested_logs;
265        }
266
267        let mut merged = nested_logs;
268        for circuit_log in circuit_logs {
269            if let Some(existing) = merged
270                .iter_mut()
271                .find(|log| log.counter == circuit_log.counter)
272            {
273                *existing = circuit_log;
274            } else {
275                merged.push(circuit_log);
276            }
277        }
278        merged
279    }
280
281    fn try_handle_protocol_nested_private_call(
282        &mut self,
283        target_address: AztecAddress,
284        selector: aztec_core::abi::FunctionSelector,
285        encoded_args: &[Fr],
286        circuit_side_effect_counter: u32,
287        is_static: bool,
288    ) -> Result<Option<Vec<Vec<Fr>>>, Error> {
289        if is_static {
290            return Ok(None);
291        }
292
293        // --- Contract Class Registerer: publish(Field,Field,Field) ---
294        let publish_class_selector =
295            aztec_core::abi::FunctionSelector::from_signature("publish(Field,Field,Field)");
296        if target_address
297            == aztec_core::constants::protocol_contract_address::contract_class_registry()
298            && selector == publish_class_selector
299        {
300            return self.handle_nested_publish_class(
301                target_address,
302                selector,
303                encoded_args,
304                circuit_side_effect_counter,
305            );
306        }
307
308        // --- Contract Instance Registry: publish_for_public_execution ---
309        let publish_instance_selector = aztec_core::abi::FunctionSelector::from_signature(
310            "publish_for_public_execution(Field,(Field),Field,(((Field,Field,bool)),((Field,Field,bool)),((Field,Field,bool)),((Field,Field,bool))),bool)",
311        );
312        if target_address
313            == aztec_core::constants::protocol_contract_address::contract_instance_registry()
314            && selector == publish_instance_selector
315        {
316            return self.handle_nested_publish_instance(
317                target_address,
318                selector,
319                encoded_args,
320                circuit_side_effect_counter,
321            );
322        }
323
324        Ok(None)
325    }
326
327    /// Handle nested call to contract class registerer `publish(Field,Field,Field)`.
328    ///
329    /// Mirrors the top-level `protocol_private_execution` handler in embedded_pxe.rs.
330    /// Emits a nullifier (class_id) and a contract class log with bytecode.
331    fn handle_nested_publish_class(
332        &mut self,
333        target_address: AztecAddress,
334        selector: aztec_core::abi::FunctionSelector,
335        encoded_args: &[Fr],
336        circuit_side_effect_counter: u32,
337    ) -> Result<Option<Vec<Vec<Fr>>>, Error> {
338        if encoded_args.len() < 3 {
339            return Err(Error::InvalidData(format!(
340                "nested publish args too short: {}",
341                encoded_args.len()
342            )));
343        }
344
345        let artifact_hash = encoded_args[0];
346        let private_functions_root = encoded_args[1];
347        let public_bytecode_commitment = encoded_args[2];
348        let class_id = aztec_core::hash::compute_contract_class_id(
349            artifact_hash,
350            private_functions_root,
351            public_bytecode_commitment,
352        );
353
354        // Load bytecode fields from capsules seeded from the tx request.
355        let bytecode_fields = self
356            .capsules
357            .iter()
358            .find(|capsule| {
359                capsule.contract_address
360                    == aztec_core::constants::protocol_contract_address::contract_class_registry()
361                    && capsule.storage_slot
362                        == aztec_core::constants::contract_class_registry_bytecode_capsule_slot()
363            })
364            .map(|capsule| capsule.data.clone())
365            .unwrap_or_default();
366
367        // Build emitted fields: magic + class_id + version + artifact_hash +
368        // private_functions_root + bytecode_fields
369        let mut emitted_fields = Vec::with_capacity(
370            aztec_core::constants::MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS + 5,
371        );
372        emitted_fields.push(aztec_core::constants::contract_class_published_magic_value());
373        emitted_fields.push(class_id);
374        emitted_fields.push(Fr::from(1u64)); // version
375        emitted_fields.push(artifact_hash);
376        emitted_fields.push(private_functions_root);
377        emitted_fields.extend(bytecode_fields);
378
379        let nullifier_counter = circuit_side_effect_counter;
380        let class_log_counter = nullifier_counter.saturating_add(1);
381        let end_side_effect_counter = class_log_counter.saturating_add(1);
382
383        // Emit nullifier: class_id (prevents duplicate registration)
384        self.nullifiers.push(ScopedNullifier {
385            nullifier: Nullifier {
386                value: class_id,
387                note_hash: Fr::zero(),
388                counter: nullifier_counter,
389            },
390            contract_address: target_address,
391        });
392
393        // Emit contract class log (NOT a private log)
394        self.contract_class_logs.push(CountedContractClassLog {
395            log: ContractClassLog {
396                contract_address: target_address,
397                emitted_length: emitted_fields.len() as u32,
398                fields: emitted_fields,
399            },
400            counter: class_log_counter,
401        });
402
403        self.side_effect_counter = self.side_effect_counter.max(end_side_effect_counter);
404
405        let returns_hash = aztec_core::hash::compute_var_args_hash(&[]);
406        self.execution_cache.entry(returns_hash).or_default();
407        self.nested_results.push(PrivateCallExecutionResult {
408            contract_address: target_address,
409            call_context: CallContext {
410                msg_sender: self.contract_address,
411                contract_address: target_address,
412                function_selector: selector.to_field(),
413                is_static_call: false,
414            },
415            start_side_effect_counter: nullifier_counter,
416            end_side_effect_counter,
417            min_revertible_side_effect_counter: nullifier_counter,
418            ..Default::default()
419        });
420
421        Ok(Some(vec![vec![
422            Fr::from(u64::from(end_side_effect_counter)),
423            returns_hash,
424        ]]))
425    }
426
427    /// Handle nested call to contract instance registry `publish_for_public_execution`.
428    fn handle_nested_publish_instance(
429        &mut self,
430        target_address: AztecAddress,
431        selector: aztec_core::abi::FunctionSelector,
432        encoded_args: &[Fr],
433        circuit_side_effect_counter: u32,
434    ) -> Result<Option<Vec<Vec<Fr>>>, Error> {
435        if encoded_args.len() < 16 {
436            return Err(Error::InvalidData(format!(
437                "nested publish_for_public_execution args too short: {}",
438                encoded_args.len()
439            )));
440        }
441
442        let salt = encoded_args[0];
443        let class_id = encoded_args[1];
444        let initialization_hash = encoded_args[2];
445        let public_keys = aztec_core::types::PublicKeys {
446            master_nullifier_public_key: aztec_core::types::Point {
447                x: encoded_args[3],
448                y: encoded_args[4],
449                is_infinite: encoded_args[5] != Fr::zero(),
450            },
451            master_incoming_viewing_public_key: aztec_core::types::Point {
452                x: encoded_args[6],
453                y: encoded_args[7],
454                is_infinite: encoded_args[8] != Fr::zero(),
455            },
456            master_outgoing_viewing_public_key: aztec_core::types::Point {
457                x: encoded_args[9],
458                y: encoded_args[10],
459                is_infinite: encoded_args[11] != Fr::zero(),
460            },
461            master_tagging_public_key: aztec_core::types::Point {
462                x: encoded_args[12],
463                y: encoded_args[13],
464                is_infinite: encoded_args[14] != Fr::zero(),
465            },
466        };
467        let universal_deploy = encoded_args[15] != Fr::zero();
468        let origin = self.sender_for_tags.unwrap_or(self.contract_address);
469        let deployer = if universal_deploy {
470            AztecAddress::zero()
471        } else {
472            origin
473        };
474
475        let inner = ContractInstance {
476            version: 1,
477            salt,
478            deployer,
479            current_contract_class_id: class_id,
480            original_contract_class_id: class_id,
481            initialization_hash,
482            public_keys: public_keys.clone(),
483        };
484        let instance_address = aztec_core::hash::compute_contract_address_from_instance(&inner)?;
485
486        let event_payload = vec![
487            aztec_core::constants::contract_instance_published_magic_value(),
488            instance_address.0,
489            Fr::from(1u64),
490            salt,
491            class_id,
492            initialization_hash,
493            public_keys.master_nullifier_public_key.x,
494            public_keys.master_nullifier_public_key.y,
495            public_keys.master_incoming_viewing_public_key.x,
496            public_keys.master_incoming_viewing_public_key.y,
497            public_keys.master_outgoing_viewing_public_key.x,
498            public_keys.master_outgoing_viewing_public_key.y,
499            public_keys.master_tagging_public_key.x,
500            public_keys.master_tagging_public_key.y,
501            deployer.0,
502        ];
503        let mut emitted_private_log_fields = event_payload;
504        emitted_private_log_fields.push(Fr::zero());
505
506        let nullifier_counter = circuit_side_effect_counter;
507        let private_log_counter = nullifier_counter.saturating_add(1);
508        let end_side_effect_counter = private_log_counter.saturating_add(1);
509
510        self.nullifiers.push(ScopedNullifier {
511            nullifier: Nullifier {
512                value: instance_address.0,
513                note_hash: Fr::zero(),
514                counter: nullifier_counter,
515            },
516            contract_address: target_address,
517        });
518        self.private_logs.push(PrivateLogData {
519            fields: emitted_private_log_fields,
520            emitted_length: 15,
521            note_hash_counter: 0,
522            counter: private_log_counter,
523            contract_address: target_address,
524        });
525        self.side_effect_counter = self.side_effect_counter.max(end_side_effect_counter);
526
527        let returns_hash = aztec_core::hash::compute_var_args_hash(&[]);
528        self.execution_cache.entry(returns_hash).or_default();
529        self.nested_results.push(PrivateCallExecutionResult {
530            contract_address: target_address,
531            call_context: CallContext {
532                msg_sender: self.contract_address,
533                contract_address: target_address,
534                function_selector: selector.to_field(),
535                is_static_call: false,
536            },
537            start_side_effect_counter: nullifier_counter,
538            end_side_effect_counter,
539            min_revertible_side_effect_counter: nullifier_counter,
540            ..Default::default()
541        });
542
543        Ok(Some(vec![vec![
544            Fr::from(u64::from(end_side_effect_counter)),
545            returns_hash,
546        ]]))
547    }
548
549    /// Map Noir NoteStatus enum values: ACTIVE = 1, ACTIVE_OR_NULLIFIED = 2.
550    fn note_status_from_field(value: Fr) -> Result<NoteStatus, Error> {
551        match value.to_usize() as u64 {
552            1 => Ok(NoteStatus::Active),
553            2 => Ok(NoteStatus::ActiveOrNullified),
554            other => Err(Error::InvalidData(format!("unknown note status: {other}"))),
555        }
556    }
557
558    fn pack_hinted_note(note: &StoredNote) -> Result<Vec<Fr>, Error> {
559        let mut packed = note.note_data.clone();
560        packed.push(note.contract_address.0);
561        packed.push(note.owner.0);
562        packed.push(note.randomness);
563        packed.push(note.storage_slot);
564        let stage = if note.is_pending {
565            if note.note_nonce == Fr::zero() {
566                1u64
567            } else {
568                2u64
569            }
570        } else {
571            if note.note_nonce == Fr::zero() {
572                return Err(Error::InvalidData(
573                    "cannot pack settled note with zero note_nonce".into(),
574                ));
575            }
576            3u64
577        };
578        packed.push(Fr::from(stage));
579        packed.push(note.note_nonce);
580        Ok(packed)
581    }
582
583    fn pack_bounded_vec_of_arrays(
584        arrays: &[Vec<Fr>],
585        max_len: usize,
586        nested_len: usize,
587    ) -> Result<Vec<Vec<Fr>>, Error> {
588        if arrays.len() > max_len {
589            return Err(Error::InvalidData(format!(
590                "bounded vec overflow: {} > {max_len}",
591                arrays.len()
592            )));
593        }
594
595        let mut flattened = Vec::with_capacity(max_len.saturating_mul(nested_len));
596        for array in arrays {
597            if array.len() != nested_len {
598                return Err(Error::InvalidData(format!(
599                    "packed hinted note length mismatch: {} != {nested_len}",
600                    array.len()
601                )));
602            }
603            flattened.extend_from_slice(array);
604        }
605
606        flattened.resize(max_len.saturating_mul(nested_len), Fr::zero());
607        Ok(vec![flattened, vec![Fr::from(arrays.len() as u64)]])
608    }
609
610    pub fn new(
611        node: &'a N,
612        contract_store: &'a ContractStore,
613        key_store: &'a KeyStore,
614        note_store: &'a NoteStore,
615        capsule_store: &'a CapsuleStore,
616        address_store: &'a AddressStore,
617        sender_tagging_store: &'a SenderTaggingStore,
618        block_header: serde_json::Value,
619        contract_address: AztecAddress,
620        protocol_nullifier: Fr,
621        sender_for_tags: Option<AztecAddress>,
622        scopes: Vec<AztecAddress>,
623        call_is_static: bool,
624    ) -> Self {
625        Self {
626            node,
627            contract_store,
628            key_store,
629            note_store,
630            capsule_store,
631            address_store,
632            sender_tagging_store,
633            block_header,
634            contract_address,
635            protocol_nullifier,
636            execution_cache: std::collections::HashMap::new(),
637            auth_witnesses: Vec::new(),
638            sender_for_tags,
639            scopes,
640            call_is_static,
641            side_effect_counter: 0,
642            new_notes: Vec::new(),
643            note_hashes: Vec::new(),
644            nullifiers: Vec::new(),
645            note_hash_nullifier_counter_map: std::collections::HashMap::new(),
646            consumed_db_nullifiers: std::collections::HashSet::new(),
647            private_logs: Vec::new(),
648            contract_class_logs: Vec::new(),
649            offchain_effects: Vec::new(),
650            public_call_requests: Vec::new(),
651            public_teardown_call_request: None,
652            note_hash_read_requests: Vec::new(),
653            nullifier_read_requests: Vec::new(),
654            min_revertible_side_effect_counter: 0,
655            public_function_calldata: Vec::new(),
656            nested_results: Vec::new(),
657            context_witness_prefix: Vec::new(),
658            capsules: Vec::new(),
659        }
660    }
661
662    fn ensure_mutable_context(&self) -> Result<(), Error> {
663        if self.call_is_static {
664            return Err(Error::InvalidData(
665                "Static call cannot update the state".into(),
666            ));
667        }
668        Ok(())
669    }
670
671    /// Set auth witnesses for this execution context.
672    pub fn set_auth_witnesses(&mut self, witnesses: Vec<(Fr, Vec<Fr>)>) {
673        self.auth_witnesses = witnesses;
674    }
675
676    /// Set capsules from the tx request so protocol contract handlers can
677    /// access auxiliary data (e.g., packed bytecode for class registration).
678    pub fn set_capsules(&mut self, capsules: Vec<aztec_core::tx::Capsule>) {
679        self.capsules = capsules;
680    }
681
682    /// Pre-populate the execution cache with hashed values from the tx request.
683    ///
684    /// Mirrors the TS SDK's `HashedValuesCache.create(request.argsOfCalls)`.
685    /// The Noir entrypoint calls `loadFromExecutionCache(hash)` to retrieve
686    /// the args for each nested call; without pre-seeding the cache these
687    /// lookups would fail.
688    pub fn seed_execution_cache(&mut self, hashed_values: &[aztec_core::tx::HashedValues]) {
689        for hv in hashed_values {
690            self.execution_cache.insert(hv.hash, hv.values.clone());
691        }
692    }
693
694    /// Return the public call requests accumulated during this execution.
695    pub fn take_public_call_requests(
696        &mut self,
697    ) -> Vec<crate::execution::execution_result::PublicCallRequestData> {
698        std::mem::take(&mut self.public_call_requests)
699    }
700
701    /// Return the public function calldata accumulated during this execution.
702    pub fn take_public_function_calldata(&mut self) -> Vec<aztec_core::tx::HashedValues> {
703        std::mem::take(&mut self.public_function_calldata)
704    }
705
706    /// Return the teardown call request if one was enqueued.
707    pub fn take_teardown_call_request(
708        &mut self,
709    ) -> Option<crate::execution::execution_result::PublicCallRequestData> {
710        self.public_teardown_call_request.take()
711    }
712
713    /// Handle an ACVM foreign call by name and arguments.
714    ///
715    /// Supports both prefixed names (from compiled Noir bytecode) and
716    /// legacy unprefixed names.
717    pub async fn handle_foreign_call(
718        &mut self,
719        name: &str,
720        args: Vec<Vec<Fr>>,
721    ) -> Result<Vec<Vec<Fr>>, Error> {
722        // Strip the common prefixes used by compiled Noir bytecode
723        let stripped = name
724            .strip_prefix("private")
725            .or_else(|| name.strip_prefix("utility"))
726            .unwrap_or(name);
727
728        // Convert to camelCase handler name (first char lowercase)
729        let handler = if !stripped.is_empty() {
730            let mut chars = stripped.chars();
731            let first = chars.next().unwrap().to_lowercase().to_string();
732            format!("{first}{}", chars.as_str())
733        } else {
734            name.to_owned()
735        };
736
737        match handler.as_str() {
738            // Key management
739            "getSecretKey" | "getKeyValidationRequest" => self.get_secret_key(&args).await,
740            "getPublicKeysAndPartialAddress" | "tryGetPublicKeysAndPartialAddress" => {
741                self.get_public_keys_and_partial_address(&args).await
742            }
743
744            // Note operations
745            "getNotes" => self.get_notes(&args).await,
746            "checkNoteHashExists" => self.check_note_hash_exists(&args).await,
747            "notifyCreatedNote" => self.notify_created_note(&args),
748            "notifyNullifiedNote" => self.notify_nullified_note(&args),
749            "notifyCreatedNullifier" => self.notify_created_nullifier(&args),
750            "isNullifierPending" => self.is_nullifier_pending(&args),
751
752            // Storage
753            "getPublicStorageAt" | "storageRead" => self.get_public_storage_at(&args).await,
754            "getContractInstance" => self.get_contract_instance(&args).await,
755
756            // Capsules
757            "getCapsule" | "loadCapsule" => self.get_capsule(&args).await,
758            "storeCapsule" => self.store_capsule(&args).await,
759
760            // Block header
761            "getBlockHeader" => self.get_block_header(&args).await,
762
763            // Emit side effects (note/nullifier/log)
764            "emitNote" => self.notify_created_note(&args),
765            "emitNullifier" => self.notify_created_nullifier(&args),
766            "emitPrivateLog" | "emitEncryptedLog" => self.emit_private_log(&args),
767            "notifyCreatedContractClassLog" => self.emit_contract_class_log(&args),
768
769            // Execution cache
770            "storeInExecutionCache" => self.store_in_execution_cache(&args),
771            "loadFromExecutionCache" => self.load_from_execution_cache(&args),
772
773            // Auth witnesses
774            "getAuthWitness" => self.get_auth_witness(&args),
775
776            // Public call enqueuing
777            "notifyEnqueuedPublicFunctionCall" | "enqueuePublicFunctionCall" => {
778                self.enqueue_public_function_call(&args, false)
779            }
780            "notifySetPublicTeardownFunctionCall" => self.enqueue_public_function_call(&args, true),
781
782            // Counter management
783            "notifySetMinRevertibleSideEffectCounter" => {
784                if let Some(counter) = args.first().and_then(|v| v.first()) {
785                    self.min_revertible_side_effect_counter = counter.to_usize() as u32;
786                }
787                Ok(vec![])
788            }
789            "isSideEffectCounterRevertible" => {
790                let counter = args
791                    .first()
792                    .and_then(|v| v.first())
793                    .map(|f| f.to_usize() as u32)
794                    .unwrap_or(0);
795                let revertible = counter >= self.min_revertible_side_effect_counter;
796                Ok(vec![vec![Fr::from(revertible)]])
797            }
798
799            // Tagging
800            "getSenderForTags" => self.get_sender_for_tags(),
801            "setSenderForTags" => self.set_sender_for_tags(&args),
802            "getNextAppTagAsSender" => self.get_next_app_tag_as_sender(&args).await,
803
804            // Misc
805            "getRandomField" => Ok(vec![vec![Fr::random()]]),
806            "assertCompatibleOracleVersion" => Ok(vec![]),
807            "log" => {
808                // Parse level and message from args
809                let _level = args
810                    .first()
811                    .and_then(|v| v.first())
812                    .map(|f| f.to_usize())
813                    .unwrap_or(0);
814                tracing::debug!("noir log oracle call");
815                Ok(vec![])
816            }
817            "getUtilityContext" => Ok(vec![]),
818            "aes128Decrypt" => Err(Error::InvalidData("aes128Decrypt not implemented".into())),
819            "getSharedSecret" => Err(Error::InvalidData("getSharedSecret not implemented".into())),
820            "emitOffchainEffect" => {
821                let data = args.first().cloned().unwrap_or_default();
822                self.offchain_effects.push(data);
823                Ok(vec![])
824            }
825
826            // Membership witnesses (from node)
827            "getNoteHashMembershipWitness" => self.get_note_hash_membership_witness(&args).await,
828            "getNullifierMembershipWitness" => self.get_nullifier_membership_witness(&args).await,
829            "getPublicDataWitness" => self.get_public_data_witness(&args).await,
830            "getBlockHashMembershipWitness" => self.get_block_hash_membership_witness(&args).await,
831            "getL1ToL2MembershipWitness" | "utilityGetL1ToL2MembershipWitness" => {
832                self.get_l1_to_l2_membership_witness(&args).await
833            }
834
835            // Note discovery
836            "fetchTaggedLogs" | "bulkRetrieveLogs" => Ok(vec![]),
837            "validateAndStoreEnqueuedNotesAndEvents" => Ok(vec![]),
838
839            // Nested private function calls
840            "callPrivateFunction" => self.call_private_function(&args).await,
841
842            // Nullifier check
843            "checkNullifierExists" => self.check_nullifier_exists(&args).await,
844
845            _ => {
846                tracing::error!(
847                    oracle = name,
848                    handler = handler.as_str(),
849                    "unsupported oracle call"
850                );
851                Err(Error::InvalidData(format!(
852                    "unsupported oracle call: '{name}' (handler: '{handler}'). \
853                     All production oracle calls must be implemented."
854                )))
855            }
856        }
857    }
858
859    /// Return `KeyValidationRequest { pk_m: Point, sk_app: Field }` (4 fields).
860    /// Uses pk_m_hash to find the right key type across all accounts.
861    ///
862    /// Enforces scope isolation: only keys belonging to accounts in the
863    /// current execution scopes are accessible.
864    async fn get_secret_key(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
865        use aztec_core::hash::poseidon2_hash;
866
867        let pk_m_hash = *args.first().and_then(|v| v.first()).ok_or_else(|| {
868            Error::InvalidData("getKeyValidationRequest: missing pk_m_hash".into())
869        })?;
870
871        // Check scope: ensure the key owner is within the current scopes
872        if !self.scopes.is_empty() {
873            let mut key_in_scope = false;
874            for scope in &self.scopes {
875                if let Some(complete) = self.address_store.get(scope).await? {
876                    let pk = &complete.public_keys;
877                    for point in [
878                        &pk.master_nullifier_public_key,
879                        &pk.master_incoming_viewing_public_key,
880                        &pk.master_outgoing_viewing_public_key,
881                        &pk.master_tagging_public_key,
882                    ] {
883                        let hash = poseidon2_hash(&[point.x, point.y, Fr::from(point.is_infinite)]);
884                        if hash == pk_m_hash {
885                            key_in_scope = true;
886                            break;
887                        }
888                    }
889                    if key_in_scope {
890                        break;
891                    }
892                }
893            }
894            if !key_in_scope {
895                return Err(Error::InvalidData("Key validation request denied".into()));
896            }
897        }
898
899        match self
900            .key_store
901            .get_key_validation_request(&pk_m_hash, &self.contract_address)
902            .await?
903        {
904            Some((pk_m, sk_app)) => Ok(vec![
905                vec![pk_m.x],
906                vec![pk_m.y],
907                vec![Fr::from(pk_m.is_infinite)],
908                vec![sk_app],
909            ]),
910            None => Ok(vec![
911                vec![Fr::zero()],
912                vec![Fr::zero()],
913                vec![Fr::zero()],
914                vec![Fr::zero()],
915            ]),
916        }
917    }
918
919    /// Return Option<[Field; 13]> with 4 points (x, y, is_infinite) + partial_address.
920    async fn get_public_keys_and_partial_address(
921        &self,
922        args: &[Vec<Fr>],
923    ) -> Result<Vec<Vec<Fr>>, Error> {
924        let address = AztecAddress(
925            *args
926                .first()
927                .and_then(|v| v.first())
928                .ok_or_else(|| Error::InvalidData("missing address arg".into()))?,
929        );
930
931        let Some(complete) = self.address_store.get(&address).await? else {
932            tracing::debug!(
933                queried_address = %address,
934                "getPublicKeysAndPartialAddress: address not found in store"
935            );
936            return Ok(vec![vec![Fr::zero()], vec![Fr::zero(); 13]]);
937        };
938
939        let pk = &complete.public_keys;
940        let mut fields = Vec::with_capacity(13);
941        for point in [
942            &pk.master_nullifier_public_key,
943            &pk.master_incoming_viewing_public_key,
944            &pk.master_outgoing_viewing_public_key,
945            &pk.master_tagging_public_key,
946        ] {
947            fields.push(point.x);
948            fields.push(point.y);
949            fields.push(Fr::from(point.is_infinite));
950        }
951        fields.push(complete.partial_address);
952        Ok(vec![vec![Fr::from(true)], fields])
953    }
954
955    async fn get_notes(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
956        let owner = match (
957            args.first()
958                .and_then(|v| v.first())
959                .copied()
960                .unwrap_or(Fr::zero()),
961            args.get(1)
962                .and_then(|v| v.first())
963                .copied()
964                .unwrap_or(Fr::zero()),
965        ) {
966            (flag, value) if flag != Fr::zero() => Some(AztecAddress(value)),
967            _ => None,
968        };
969        let storage_slot = args
970            .get(2)
971            .and_then(|v| v.first())
972            .copied()
973            .ok_or_else(|| Error::InvalidData("getNotes: missing storage_slot".into()))?;
974        let limit = args
975            .get(13)
976            .and_then(|v| v.first())
977            .copied()
978            .unwrap_or(Fr::zero())
979            .to_usize();
980        let offset = args
981            .get(14)
982            .and_then(|v| v.first())
983            .copied()
984            .unwrap_or(Fr::zero())
985            .to_usize();
986        let status = Self::note_status_from_field(
987            args.get(15)
988                .and_then(|v| v.first())
989                .copied()
990                .unwrap_or(Fr::zero()),
991        )?;
992        let max_notes = args
993            .get(16)
994            .and_then(|v| v.first())
995            .copied()
996            .unwrap_or(Fr::zero())
997            .to_usize();
998        let packed_hinted_note_length = args
999            .get(17)
1000            .and_then(|v| v.first())
1001            .copied()
1002            .unwrap_or(Fr::zero())
1003            .to_usize();
1004
1005        let mut notes = self
1006            .note_store
1007            .get_notes(&NoteFilter {
1008                contract_address: Some(self.contract_address),
1009                storage_slot: Some(storage_slot),
1010                owner,
1011                status,
1012                ..Default::default()
1013            })
1014            .await?;
1015
1016        // Collect note hash values that were nullified (from counter map)
1017        // for pending note filtering.
1018        let mut nullified_hash_counts: std::collections::HashMap<Fr, usize> =
1019            std::collections::HashMap::new();
1020        for nh_counter in self.note_hash_nullifier_counter_map.keys() {
1021            if let Some(nh) = self
1022                .note_hashes
1023                .iter()
1024                .find(|h| h.note_hash.counter == *nh_counter)
1025            {
1026                *nullified_hash_counts.entry(nh.note_hash.value).or_insert(0) += 1;
1027            }
1028        }
1029
1030        // Filter out DB notes that have been consumed during this execution.
1031        // We check the note's siloed_nullifier against consumed_db_nullifiers
1032        // (which tracks siloed nullifiers computed from notify_nullified_note).
1033        notes.retain(|n| {
1034            if n.siloed_nullifier.is_zero() {
1035                return true;
1036            }
1037            !self.consumed_db_nullifiers.contains(&n.siloed_nullifier)
1038        });
1039
1040        // Include pending notes created during this execution that match the
1041        // query filters (mirrors upstream `noteCache.getNotes(...)` merge).
1042        // For notes with the same hash, only skip as many as were nullified.
1043        let mut consumed_hash_counts: std::collections::HashMap<Fr, usize> =
1044            std::collections::HashMap::new();
1045        for pending in &self.new_notes {
1046            if pending.contract_address != self.contract_address {
1047                continue;
1048            }
1049            if pending.storage_slot != storage_slot {
1050                continue;
1051            }
1052            if let Some(owner_addr) = owner {
1053                if pending.owner != owner_addr {
1054                    continue;
1055                }
1056            }
1057            // Check the note hasn't been nullified: skip up to the number
1058            // of times this hash was nullified.
1059            if let Some(&max_nullified) = nullified_hash_counts.get(&pending.note_hash) {
1060                let already_consumed = consumed_hash_counts.entry(pending.note_hash).or_insert(0);
1061                if *already_consumed < max_nullified {
1062                    *already_consumed += 1;
1063                    continue;
1064                }
1065            }
1066            notes.push(StoredNote {
1067                contract_address: pending.contract_address,
1068                owner: pending.owner,
1069                storage_slot: pending.storage_slot,
1070                randomness: pending.randomness,
1071                note_nonce: Fr::zero(), // nonce unknown during private execution
1072                note_hash: pending.note_hash,
1073                siloed_nullifier: Fr::zero(),
1074                note_data: pending.note_items.clone(),
1075                nullified: false,
1076                is_pending: true,
1077                nullification_block_number: None,
1078                leaf_index: None,
1079                block_number: None,
1080                tx_index_in_block: None,
1081                note_index_in_tx: None,
1082                scopes: vec![pending.owner],
1083            });
1084        }
1085
1086        // Apply select-clause filtering (comparators).
1087        let selects = super::pick_notes::parse_select_clauses(args);
1088        notes = super::pick_notes::select_notes(notes, &selects);
1089
1090        if offset >= notes.len() {
1091            notes.clear();
1092        } else if offset > 0 {
1093            notes = notes.split_off(offset);
1094        }
1095
1096        if limit > 0 && notes.len() > limit {
1097            notes.truncate(limit);
1098        }
1099        if notes.len() > max_notes {
1100            notes.truncate(max_notes);
1101        }
1102
1103        let packed = notes
1104            .iter()
1105            .map(Self::pack_hinted_note)
1106            .collect::<Result<Vec<_>, _>>()?;
1107
1108        Self::pack_bounded_vec_of_arrays(&packed, max_notes, packed_hinted_note_length)
1109    }
1110
1111    async fn check_note_hash_exists(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1112        let note_hash = args
1113            .first()
1114            .and_then(|v| v.first())
1115            .ok_or_else(|| Error::InvalidData("missing note_hash".into()))?;
1116        // Check the persistent store first.
1117        let mut exists = self
1118            .note_store
1119            .has_note(&self.contract_address, note_hash)
1120            .await?;
1121        // Also check pending note hashes from the current execution
1122        // (notes created by sibling calls within the same TX).
1123        if !exists {
1124            exists = self
1125                .note_hashes
1126                .iter()
1127                .any(|nh| nh.note_hash.value == *note_hash);
1128        }
1129        Ok(vec![vec![Fr::from(exists)]])
1130    }
1131
1132    fn notify_created_note(&mut self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1133        self.ensure_mutable_context()?;
1134        let owner = args
1135            .first()
1136            .and_then(|v| v.first())
1137            .copied()
1138            .unwrap_or(Fr::zero());
1139        let storage_slot = args
1140            .get(1)
1141            .and_then(|v| v.first())
1142            .copied()
1143            .unwrap_or(Fr::zero());
1144        let randomness = args
1145            .get(2)
1146            .and_then(|v| v.first())
1147            .copied()
1148            .unwrap_or(Fr::zero());
1149        let note_type_id = args
1150            .get(3)
1151            .and_then(|v| v.first())
1152            .copied()
1153            .unwrap_or(Fr::zero());
1154        let note_items = args.get(4).cloned().unwrap_or_default();
1155        let note_hash = args
1156            .get(5)
1157            .and_then(|v| v.first())
1158            .copied()
1159            .unwrap_or(Fr::zero());
1160        let counter = args
1161            .get(6)
1162            .and_then(|v| v.first())
1163            .map(|f| f.to_usize() as u32)
1164            .unwrap_or_else(|| {
1165                self.side_effect_counter += 1;
1166                self.side_effect_counter
1167            });
1168
1169        self.new_notes.push(NoteAndSlot {
1170            contract_address: self.contract_address,
1171            owner: AztecAddress(owner),
1172            storage_slot,
1173            randomness,
1174            note_type_id,
1175            note_items: note_items.clone(),
1176            note_hash,
1177            counter,
1178        });
1179
1180        self.note_hashes.push(ScopedNoteHash {
1181            note_hash: NoteHash {
1182                value: note_hash,
1183                counter,
1184            },
1185            contract_address: self.contract_address,
1186        });
1187
1188        Ok(vec![])
1189    }
1190
1191    fn notify_nullified_note(&mut self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1192        self.ensure_mutable_context()?;
1193        let inner_nullifier = args
1194            .first()
1195            .and_then(|v| v.first())
1196            .copied()
1197            .unwrap_or(Fr::zero());
1198        let note_hash = args
1199            .get(1)
1200            .and_then(|v| v.first())
1201            .copied()
1202            .unwrap_or(Fr::zero());
1203        let counter = args
1204            .get(2)
1205            .and_then(|v| v.first())
1206            .map(|f| f.to_usize() as u32)
1207            .unwrap_or_else(|| {
1208                self.side_effect_counter += 1;
1209                self.side_effect_counter
1210            });
1211
1212        // Track the siloed nullifier so DB notes can be filtered in get_notes.
1213        let siloed = aztec_core::hash::silo_nullifier(&self.contract_address, &inner_nullifier);
1214        self.consumed_db_nullifiers.insert(siloed);
1215
1216        self.nullifiers.push(ScopedNullifier {
1217            nullifier: Nullifier {
1218                value: inner_nullifier,
1219                note_hash,
1220                counter,
1221            },
1222            contract_address: self.contract_address,
1223        });
1224
1225        // Track the note hash -> nullifier counter mapping for squashing
1226        if note_hash != Fr::zero() {
1227            if let Some(nh) = self
1228                .note_hashes
1229                .iter()
1230                .find(|nh| nh.note_hash.value == note_hash)
1231            {
1232                self.note_hash_nullifier_counter_map
1233                    .insert(nh.note_hash.counter, counter);
1234            }
1235        }
1236
1237        Ok(vec![])
1238    }
1239
1240    fn notify_created_nullifier(&mut self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1241        self.ensure_mutable_context()?;
1242        let inner_nullifier = args
1243            .first()
1244            .and_then(|v| v.first())
1245            .copied()
1246            .unwrap_or(Fr::zero());
1247        self.side_effect_counter += 1;
1248        let counter = self.side_effect_counter;
1249
1250        self.nullifiers.push(ScopedNullifier {
1251            nullifier: Nullifier {
1252                value: inner_nullifier,
1253                note_hash: Fr::zero(),
1254                counter,
1255            },
1256            contract_address: self.contract_address,
1257        });
1258
1259        Ok(vec![])
1260    }
1261
1262    fn is_nullifier_pending(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1263        let nullifier = args
1264            .first()
1265            .and_then(|v| v.first())
1266            .ok_or_else(|| Error::InvalidData("missing nullifier".into()))?;
1267        let pending = self
1268            .nullifiers
1269            .iter()
1270            .any(|n| n.nullifier.value == *nullifier);
1271        Ok(vec![vec![Fr::from(pending)]])
1272    }
1273
1274    async fn get_public_storage_at(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1275        fn slot_with_offset(start_slot: Fr, offset: usize) -> Fr {
1276            let mut bytes = start_slot.to_be_bytes();
1277            let mut carry = offset as u128;
1278            for byte in bytes.iter_mut().rev() {
1279                if carry == 0 {
1280                    break;
1281                }
1282                let sum = u128::from(*byte) + (carry & 0xff);
1283                *byte = (sum & 0xff) as u8;
1284                carry = (carry >> 8) + (sum >> 8);
1285            }
1286            Fr::from(bytes)
1287        }
1288
1289        let (block_hash, contract, start_slot, number_of_elements) = if args.len() >= 4 {
1290            let block_hash = args
1291                .first()
1292                .and_then(|v| v.first())
1293                .copied()
1294                .unwrap_or_else(Fr::zero);
1295            let contract = args
1296                .get(1)
1297                .and_then(|v| v.first())
1298                .ok_or_else(|| Error::InvalidData("missing contract address".into()))?;
1299            let start_slot = args
1300                .get(2)
1301                .and_then(|v| v.first())
1302                .ok_or_else(|| Error::InvalidData("missing storage slot".into()))?;
1303            let count = args
1304                .get(3)
1305                .and_then(|v| v.first())
1306                .copied()
1307                .unwrap_or_else(Fr::zero)
1308                .to_usize();
1309            (Some(block_hash), contract, start_slot, count.max(1))
1310        } else {
1311            let contract = args
1312                .first()
1313                .and_then(|v| v.first())
1314                .ok_or_else(|| Error::InvalidData("missing contract address".into()))?;
1315            let slot = args
1316                .get(1)
1317                .and_then(|v| v.first())
1318                .ok_or_else(|| Error::InvalidData("missing storage slot".into()))?;
1319            (None, contract, slot, 1)
1320        };
1321
1322        let contract_addr = AztecAddress(*contract);
1323        let mut values = Vec::with_capacity(number_of_elements);
1324        for offset in 0..number_of_elements {
1325            let slot = slot_with_offset(*start_slot, offset);
1326            let value = match block_hash.as_ref() {
1327                Some(block_hash) => {
1328                    self.node
1329                        .get_public_storage_at_by_hash(block_hash, &contract_addr, &slot)
1330                        .await?
1331                }
1332                None => {
1333                    self.node
1334                        .get_public_storage_at(0, &contract_addr, &slot)
1335                        .await?
1336                }
1337            };
1338            values.push(value);
1339        }
1340
1341        Ok(vec![values])
1342    }
1343
1344    async fn get_contract_instance(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1345        let address = args
1346            .first()
1347            .and_then(|v| v.first())
1348            .ok_or_else(|| Error::InvalidData("missing address".into()))?;
1349        let addr = AztecAddress(*address);
1350
1351        // Check local store first, then node
1352        let inst = self.contract_store.get_instance(&addr).await?;
1353        let inst = match inst {
1354            Some(i) => Some(i),
1355            None => self.node.get_contract(&addr).await?,
1356        };
1357
1358        match inst {
1359            Some(inst) => Ok(contract_instance_to_fields(&inst.inner)),
1360            None => Ok(vec![vec![Fr::zero()]; 16]),
1361        }
1362    }
1363
1364    async fn get_capsule(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1365        let contract_id = args
1366            .first()
1367            .and_then(|v| v.first())
1368            .ok_or_else(|| Error::InvalidData("missing capsule contract id".into()))?;
1369        match self.capsule_store.pop(contract_id).await? {
1370            Some(capsule) => Ok(capsule),
1371            None => Err(Error::InvalidData("no capsule available".into())),
1372        }
1373    }
1374
1375    async fn store_capsule(&self, _args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1376        // Capsule store operation - return success
1377        Ok(vec![])
1378    }
1379
1380    async fn get_block_header(&self, _args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1381        Ok(vec![])
1382    }
1383
1384    fn emit_private_log(&mut self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1385        self.ensure_mutable_context()?;
1386        let fields = args.first().cloned().unwrap_or_default();
1387        let emitted_length = fields.len() as u32;
1388        self.side_effect_counter += 1;
1389        let counter = self.side_effect_counter;
1390
1391        self.private_logs.push(PrivateLogData {
1392            fields,
1393            emitted_length,
1394            note_hash_counter: 0,
1395            counter,
1396            contract_address: self.contract_address,
1397        });
1398        Ok(vec![])
1399    }
1400
1401    fn emit_contract_class_log(&mut self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1402        self.ensure_mutable_context()?;
1403        let contract_addr = args
1404            .first()
1405            .and_then(|v| v.first())
1406            .copied()
1407            .unwrap_or(Fr::zero());
1408        let fields = args.get(1).cloned().unwrap_or_default();
1409        let emitted_length = args
1410            .get(2)
1411            .and_then(|v| v.first())
1412            .map(|f| f.to_usize() as u32)
1413            .unwrap_or(fields.len() as u32);
1414        let counter = args
1415            .get(3)
1416            .and_then(|v| v.first())
1417            .map(|f| f.to_usize() as u32)
1418            .unwrap_or_else(|| {
1419                self.side_effect_counter += 1;
1420                self.side_effect_counter
1421            });
1422
1423        self.contract_class_logs.push(CountedContractClassLog {
1424            log: ContractClassLog {
1425                contract_address: AztecAddress(contract_addr),
1426                fields,
1427                emitted_length,
1428            },
1429            counter,
1430        });
1431        Ok(vec![])
1432    }
1433
1434    fn store_in_execution_cache(&mut self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1435        let values = args.first().cloned().unwrap_or_default();
1436        let hash = args
1437            .get(1)
1438            .and_then(|v| v.first())
1439            .copied()
1440            .unwrap_or(Fr::zero());
1441        self.execution_cache.insert(hash, values);
1442        Ok(vec![])
1443    }
1444
1445    /// Look up a value in the execution cache by its hash.
1446    pub fn get_execution_cache_entry(&self, hash: &Fr) -> Option<Vec<Fr>> {
1447        self.execution_cache.get(hash).cloned()
1448    }
1449
1450    fn load_from_execution_cache(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1451        let hash = args
1452            .first()
1453            .and_then(|v| v.first())
1454            .ok_or_else(|| Error::InvalidData("missing hash".into()))?;
1455        match self.execution_cache.get(hash) {
1456            Some(values) => Ok(vec![values.clone()]),
1457            None => Err(Error::InvalidData(
1458                "value not found in execution cache".into(),
1459            )),
1460        }
1461    }
1462
1463    fn get_auth_witness(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1464        let message_hash = args
1465            .first()
1466            .and_then(|v| v.first())
1467            .ok_or_else(|| Error::InvalidData("missing message hash".into()))?;
1468        for (hash, witness) in &self.auth_witnesses {
1469            if hash == message_hash {
1470                return Ok(vec![witness.clone()]);
1471            }
1472        }
1473        Err(Error::InvalidData(format!(
1474            "Unknown auth witness for message hash {message_hash}"
1475        )))
1476    }
1477
1478    fn enqueue_public_function_call(
1479        &mut self,
1480        args: &[Vec<Fr>],
1481        is_teardown: bool,
1482    ) -> Result<Vec<Vec<Fr>>, Error> {
1483        let contract_addr = args
1484            .first()
1485            .and_then(|v| v.first())
1486            .copied()
1487            .unwrap_or(Fr::zero());
1488        let calldata_hash = args
1489            .get(1)
1490            .and_then(|v| v.first())
1491            .copied()
1492            .unwrap_or(Fr::zero());
1493        self.side_effect_counter += 1;
1494        let counter = args
1495            .get(2)
1496            .and_then(|v| v.first())
1497            .map(|f| f.to_usize() as u32)
1498            .unwrap_or(self.side_effect_counter);
1499        let is_static = args
1500            .get(3)
1501            .and_then(|v| v.first())
1502            .map(|f| *f != Fr::zero())
1503            .unwrap_or(false);
1504
1505        let request = PublicCallRequestData {
1506            contract_address: AztecAddress(contract_addr),
1507            msg_sender: self.contract_address,
1508            is_static_call: is_static,
1509            calldata_hash,
1510            counter,
1511        };
1512
1513        // Collect the calldata preimage from the execution cache.
1514        // The circuit stores calldata via storeInExecutionCache before
1515        // calling notifyEnqueuedPublicFunctionCall with its hash.
1516        if let Some(calldata) = self.execution_cache.get(&calldata_hash).cloned() {
1517            self.public_function_calldata
1518                .push(HashedValues::from_calldata(calldata));
1519        }
1520
1521        if is_teardown {
1522            self.public_teardown_call_request = Some(request);
1523        } else {
1524            self.public_call_requests.push(request);
1525        }
1526        Ok(vec![])
1527    }
1528
1529    async fn get_l1_to_l2_membership_witness(
1530        &self,
1531        args: &[Vec<Fr>],
1532    ) -> Result<Vec<Vec<Fr>>, Error> {
1533        // args: [contract_address, msg_hash, secret]
1534        let msg_hash = args
1535            .get(1)
1536            .and_then(|v| v.first())
1537            .ok_or_else(|| Error::InvalidData("missing L1→L2 message hash".into()))?;
1538
1539        // Use the anchor block number from the execution context so the
1540        // sibling path matches the L1-to-L2 message tree root in the block
1541        // header that the Noir circuit will verify against.
1542        let block_number = self
1543            .block_header
1544            .pointer("/globalVariables/blockNumber")
1545            .and_then(|v| v.as_u64())
1546            .or_else(|| {
1547                self.block_header
1548                    .get("blockNumber")
1549                    .and_then(|v| v.as_u64())
1550            })
1551            .unwrap_or(0);
1552        let witness_json = self
1553            .node
1554            .get_l1_to_l2_message_membership_witness(block_number, msg_hash)
1555            .await?
1556            .ok_or_else(|| {
1557                Error::InvalidData(format!(
1558                    "L1→L2 message membership witness not found for {msg_hash} at block {block_number}"
1559                ))
1560            })?;
1561
1562        // The node returns: [leafIndex, siblingPathBase64OrArray]
1563        // We need to return: [[leafIndex], [path[0]], [path[1]], ..., [path[height-1]]]
1564        let leaf_index = witness_json
1565            .get(0)
1566            .and_then(|v| v.as_str())
1567            .and_then(|s| {
1568                // Try hex first, then decimal
1569                Fr::from_hex(s)
1570                    .ok()
1571                    .or_else(|| s.parse::<u64>().ok().map(Fr::from))
1572            })
1573            .or_else(|| witness_json.get(0).and_then(|v| v.as_u64()).map(Fr::from))
1574            .unwrap_or(Fr::zero());
1575
1576        // Parse sibling path — the node may return either:
1577        // - A JSON array of hex strings: ["0xabc...", "0xdef...", ...]
1578        // - A base64-encoded binary blob containing 32-byte Fr elements
1579        let mut sibling_path = Vec::with_capacity(aztec_core::constants::L1_TO_L2_MSG_TREE_HEIGHT);
1580        if let Some(path_arr) = witness_json.get(1).and_then(|v| v.as_array()) {
1581            // JSON array of hex strings
1582            for node in path_arr {
1583                let fr = if let Some(s) = node.as_str() {
1584                    Fr::from_hex(s).unwrap_or(Fr::zero())
1585                } else {
1586                    Fr::zero()
1587                };
1588                sibling_path.push(fr);
1589            }
1590        } else if let Some(b64_str) = witness_json.get(1).and_then(|v| v.as_str()) {
1591            // Base64-encoded sibling path: 4-byte BE count prefix + count×32-byte Fr elements
1592            use base64::Engine;
1593            if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(b64_str) {
1594                // Skip the 4-byte count prefix
1595                let data = if bytes.len() >= 4 {
1596                    &bytes[4..]
1597                } else {
1598                    &bytes
1599                };
1600                for chunk in data.chunks(32) {
1601                    if chunk.len() == 32 {
1602                        let mut arr = [0u8; 32];
1603                        arr.copy_from_slice(chunk);
1604                        sibling_path.push(Fr::from(arr));
1605                    }
1606                }
1607            }
1608        }
1609        // Pad to tree height
1610        sibling_path.resize(aztec_core::constants::L1_TO_L2_MSG_TREE_HEIGHT, Fr::zero());
1611
1612        // Return as 2 ACVM slots: [leafIndex] and [path[0..height]]
1613        Ok(vec![vec![leaf_index], sibling_path])
1614    }
1615
1616    async fn get_note_hash_membership_witness(
1617        &self,
1618        args: &[Vec<Fr>],
1619    ) -> Result<Vec<Vec<Fr>>, Error> {
1620        let note_hash = args
1621            .get(1)
1622            .and_then(|v| v.first())
1623            .ok_or_else(|| Error::InvalidData("missing note hash".into()))?;
1624        let _witness = self
1625            .node
1626            .get_note_hash_membership_witness(0, note_hash)
1627            .await?;
1628        // Return the witness as fields (the actual format depends on tree height)
1629        Ok(vec![])
1630    }
1631
1632    async fn get_nullifier_membership_witness(
1633        &self,
1634        args: &[Vec<Fr>],
1635    ) -> Result<Vec<Vec<Fr>>, Error> {
1636        let nullifier = args
1637            .get(1)
1638            .and_then(|v| v.first())
1639            .ok_or_else(|| Error::InvalidData("missing nullifier".into()))?;
1640        let _witness = self
1641            .node
1642            .get_nullifier_membership_witness(0, nullifier)
1643            .await?;
1644        Ok(vec![])
1645    }
1646
1647    async fn get_public_data_witness(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1648        fn fr_at(value: &serde_json::Value, path: &str) -> Result<Fr, Error> {
1649            let raw = value.pointer(path).ok_or_else(|| {
1650                Error::InvalidData(format!("public data witness missing field at {path}"))
1651            })?;
1652            if let Some(s) = raw.as_str() {
1653                return parse_field_string(s).map_err(|_| {
1654                    Error::InvalidData(format!(
1655                        "public data witness field at {path} has unsupported string value: {s}"
1656                    ))
1657                });
1658            }
1659            if let Some(n) = raw.as_u64() {
1660                return Ok(Fr::from(n));
1661            }
1662            Err(Error::InvalidData(format!(
1663                "public data witness field at {path} has unsupported shape: {raw:?}"
1664            )))
1665        }
1666
1667        let block_hash = args
1668            .first()
1669            .and_then(|v| v.first())
1670            .copied()
1671            .unwrap_or_else(Fr::zero);
1672        let leaf_slot = args
1673            .get(1)
1674            .and_then(|v| v.first())
1675            .ok_or_else(|| Error::InvalidData("missing leaf slot".into()))?;
1676        let witness = self
1677            .node
1678            .get_public_data_witness_by_hash(&block_hash, leaf_slot)
1679            .await?;
1680        let Some(witness) = witness else {
1681            return Ok(vec![
1682                vec![Fr::zero()],
1683                vec![Fr::zero()],
1684                vec![Fr::zero()],
1685                vec![Fr::zero()],
1686                vec![Fr::zero()],
1687                vec![Fr::zero(); aztec_core::constants::PUBLIC_DATA_TREE_HEIGHT],
1688            ]);
1689        };
1690
1691        let sibling_path = match witness.pointer("/siblingPath") {
1692            Some(serde_json::Value::Array(entries)) => entries
1693                .iter()
1694                .map(|entry| {
1695                    if let Some(s) = entry.as_str() {
1696                        parse_field_string(s).map_err(|_| {
1697                            Error::InvalidData(format!(
1698                                "public data witness siblingPath entry has unsupported string value: {s}"
1699                            ))
1700                        })
1701                    } else if let Some(n) = entry.as_u64() {
1702                        Ok(Fr::from(n))
1703                    } else {
1704                        Err(Error::InvalidData(format!(
1705                            "public data witness siblingPath entry has unsupported shape: {entry:?}"
1706                        )))
1707                    }
1708                })
1709                .collect::<Result<Vec<_>, _>>()?,
1710            Some(serde_json::Value::String(encoded)) => {
1711                decode_base64_sibling_path(encoded)?
1712            }
1713            _ => {
1714                return Err(Error::InvalidData(
1715                    "public data witness missing siblingPath".into(),
1716                ))
1717            }
1718        };
1719
1720        let mut sibling_path = sibling_path;
1721        sibling_path.resize(aztec_core::constants::PUBLIC_DATA_TREE_HEIGHT, Fr::zero());
1722        sibling_path.truncate(aztec_core::constants::PUBLIC_DATA_TREE_HEIGHT);
1723
1724        Ok(vec![
1725            vec![fr_at(&witness, "/index")?],
1726            vec![fr_at(&witness, "/leafPreimage/leaf/slot")?],
1727            vec![fr_at(&witness, "/leafPreimage/leaf/value")?],
1728            vec![fr_at(&witness, "/leafPreimage/nextKey")?],
1729            vec![fr_at(&witness, "/leafPreimage/nextIndex")?],
1730            sibling_path,
1731        ])
1732    }
1733
1734    async fn get_block_hash_membership_witness(
1735        &self,
1736        args: &[Vec<Fr>],
1737    ) -> Result<Vec<Vec<Fr>>, Error> {
1738        let block_hash = args
1739            .get(1)
1740            .and_then(|v| v.first())
1741            .ok_or_else(|| Error::InvalidData("missing block hash".into()))?;
1742        let _witness = self
1743            .node
1744            .get_block_hash_membership_witness(0, block_hash)
1745            .await?;
1746        Ok(vec![])
1747    }
1748
1749    fn get_sender_for_tags(&self) -> Result<Vec<Vec<Fr>>, Error> {
1750        let (is_some, sender) = match self.sender_for_tags {
1751            Some(sender) => (Fr::one(), sender.0),
1752            None => (Fr::zero(), Fr::zero()),
1753        };
1754        Ok(vec![vec![is_some], vec![sender]])
1755    }
1756
1757    fn set_sender_for_tags(&mut self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1758        let sender = args
1759            .first()
1760            .and_then(|v| v.first())
1761            .copied()
1762            .ok_or_else(|| Error::InvalidData("missing sender_for_tags".into()))?;
1763        self.sender_for_tags = Some(AztecAddress(sender));
1764        Ok(vec![])
1765    }
1766
1767    async fn get_next_app_tag_as_sender(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1768        use aztec_core::hash::poseidon2_hash;
1769
1770        let sender = AztecAddress(
1771            *args
1772                .first()
1773                .and_then(|v| v.first())
1774                .ok_or_else(|| Error::InvalidData("missing sender".into()))?,
1775        );
1776        let recipient = AztecAddress(
1777            *args
1778                .get(1)
1779                .and_then(|v| v.first())
1780                .ok_or_else(|| Error::InvalidData("missing recipient".into()))?,
1781        );
1782
1783        // Compute the directional tagging secret (sender → recipient).
1784        let Some(sender_complete) = self.address_store.get(&sender).await? else {
1785            return Err(Error::InvalidData(format!(
1786                "sender {sender} not in address store"
1787            )));
1788        };
1789        let pk_hash = sender_complete.public_keys.hash();
1790        let ivsk = self
1791            .key_store
1792            .get_master_incoming_viewing_secret_key(&pk_hash)
1793            .await?
1794            .ok_or_else(|| Error::InvalidData(format!("ivsk not found for sender {sender}")))?;
1795        let secret = super::utility_oracle::compute_directional_tagging_secret(
1796            &sender_complete,
1797            ivsk,
1798            &recipient,
1799            &self.contract_address,
1800            &recipient,
1801        )?;
1802
1803        // Get and increment the sender-side tag index.
1804        let index = self.sender_tagging_store.get_next_index(&secret).await?;
1805
1806        let tag = poseidon2_hash(&[secret, Fr::from(index)]);
1807        Ok(vec![vec![tag]])
1808    }
1809
1810    async fn check_nullifier_exists(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1811        let inner_nullifier = args
1812            .first()
1813            .and_then(|v| v.first())
1814            .ok_or_else(|| Error::InvalidData("missing nullifier".into()))?;
1815        // The Noir oracle passes the inner nullifier. The tree stores siloed
1816        // nullifiers, so mirror the upstream PXE and silo before lookup.
1817        let nullifier = aztec_core::hash::silo_nullifier(&self.contract_address, inner_nullifier);
1818        // Check pending nullifiers first
1819        if self
1820            .nullifiers
1821            .iter()
1822            .any(|n| n.nullifier.value == nullifier)
1823        {
1824            return Ok(vec![vec![Fr::from(true)]]);
1825        }
1826        // Check on-chain nullifier tree
1827        let witness = self
1828            .node
1829            .get_nullifier_membership_witness(0, &nullifier)
1830            .await?;
1831        let exists = witness.is_some();
1832        Ok(vec![vec![Fr::from(exists)]])
1833    }
1834
1835    /// Execute a nested private function call.
1836    ///
1837    /// Mirrors upstream TS `privateCallPrivateFunction`: creates a nested oracle
1838    /// sharing the same stores, recursively executes the target function via
1839    /// `AcvmExecutor::execute_private`, then merges side effects back.
1840    ///
1841    /// Input args: `[contractAddress], [functionSelector], [argsHash], [sideEffectCounter], [isStaticCall]`
1842    /// Returns: `[[endSideEffectCounter, returnsHash]]`
1843    async fn call_private_function(&mut self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1844        let target_address = AztecAddress(
1845            *args
1846                .first()
1847                .and_then(|v| v.first())
1848                .ok_or_else(|| Error::InvalidData("missing target address".into()))?,
1849        );
1850        let selector_field = *args
1851            .get(1)
1852            .and_then(|v| v.first())
1853            .ok_or_else(|| Error::InvalidData("missing function selector".into()))?;
1854        let args_hash = *args
1855            .get(2)
1856            .and_then(|v| v.first())
1857            .ok_or_else(|| Error::InvalidData("missing args hash".into()))?;
1858        let circuit_side_effect_counter = args
1859            .get(3)
1860            .and_then(|v| v.first())
1861            .map(|f| f.to_usize() as u32)
1862            .unwrap_or(self.side_effect_counter);
1863        let is_static = args
1864            .get(4)
1865            .and_then(|v| v.first())
1866            .map(|f| *f != Fr::zero())
1867            .unwrap_or(false);
1868
1869        // Find the function selector and retrieve cached args up-front so
1870        // protocol contracts can be handled without requiring local artifacts.
1871        let selector = aztec_core::abi::FunctionSelector::from_field(selector_field);
1872        let cached_args = self
1873            .execution_cache
1874            .get(&args_hash)
1875            .cloned()
1876            .unwrap_or_default();
1877
1878        if let Some(result) = self.try_handle_protocol_nested_private_call(
1879            target_address,
1880            selector,
1881            &cached_args,
1882            circuit_side_effect_counter,
1883            is_static,
1884        )? {
1885            return Ok(result);
1886        }
1887
1888        // Look up the target contract's artifact.
1889        let instance = self
1890            .contract_store
1891            .get_instance(&target_address)
1892            .await?
1893            .ok_or_else(|| {
1894                Error::InvalidData(format!("nested call: contract not found: {target_address}"))
1895            })?;
1896        let artifact = self
1897            .contract_store
1898            .get_artifact(&instance.inner.current_contract_class_id)
1899            .await?
1900            .ok_or_else(|| {
1901                Error::InvalidData(format!(
1902                    "nested call: artifact not found for contract {target_address}"
1903                ))
1904            })?;
1905
1906        // Find the function by selector.
1907        let function = artifact
1908            .find_function_by_selector(&selector)
1909            .ok_or_else(|| {
1910                Error::InvalidData(format!(
1911                    "nested call: function with selector {selector} not found in {target_address}"
1912                ))
1913            })?;
1914        let function_name = function.name.clone();
1915
1916        if function.is_static && !is_static {
1917            return Err(Error::InvalidData("can only be called statically".into()));
1918        }
1919
1920        // Retrieve arguments from the execution cache using the args hash.
1921        // Build the initial witness: PrivateContextInputs + user args.
1922        // The context inputs include call_context, block header, tx_context, etc.
1923        // For nested calls, msg_sender is the calling contract's address.
1924        let context_inputs_size = artifact.private_context_inputs_size(&function_name);
1925
1926        // Build the private context inputs witness for the nested call.
1927        // Reuse the parent's context witness prefix (block header + tx_context)
1928        // so that chain_id, version, and other context values are correct.
1929        let mut full_witness = if !self.context_witness_prefix.is_empty()
1930            && self.context_witness_prefix.len() + 4 <= context_inputs_size
1931        {
1932            // Layout: [call_context(4), block_header+tx_context..., side_effect_counter]
1933            let mut w = Vec::with_capacity(context_inputs_size);
1934            // Call context
1935            w.push(self.contract_address.0); // msg_sender = calling contract
1936            w.push(target_address.0); // contract_address = target
1937            w.push(selector_field); // function_selector
1938            w.push(Fr::from(is_static)); // is_static_call
1939                                         // Block header + tx_context from parent
1940            w.extend_from_slice(&self.context_witness_prefix);
1941            // Side effect counter — use the circuit-provided counter so
1942            // the nested circuit's PrivateContext starts with the correct
1943            // global counter (the oracle's own counter may have diverged).
1944            w.push(Fr::from(circuit_side_effect_counter as u64));
1945            // Pad to context_inputs_size
1946            w.resize(context_inputs_size, Fr::zero());
1947            w
1948        } else {
1949            let mut w = vec![Fr::zero(); context_inputs_size];
1950            if w.len() >= 4 {
1951                w[0] = self.contract_address.0;
1952                w[1] = target_address.0;
1953                w[2] = selector_field;
1954                w[3] = Fr::from(is_static);
1955            }
1956            w
1957        };
1958
1959        // Append user arguments.
1960        full_witness.extend_from_slice(&cached_args);
1961
1962        // Create a nested oracle sharing the same stores.
1963        let mut nested_oracle = PrivateExecutionOracle::new(
1964            self.node,
1965            self.contract_store,
1966            self.key_store,
1967            self.note_store,
1968            self.capsule_store,
1969            self.address_store,
1970            self.sender_tagging_store,
1971            self.block_header.clone(),
1972            target_address,
1973            self.protocol_nullifier,
1974            self.sender_for_tags,
1975            self.scopes.clone(),
1976            is_static,
1977        );
1978
1979        // Share the execution cache so return values are accessible.
1980        nested_oracle.execution_cache = self.execution_cache.clone();
1981        // Share auth witnesses.
1982        nested_oracle.auth_witnesses = self.auth_witnesses.clone();
1983        // Start the nested counter from the circuit-provided counter so
1984        // it stays in sync with the Noir PrivateContext's counter.
1985        nested_oracle.side_effect_counter = circuit_side_effect_counter;
1986        // Inherit revertibility threshold so nested calls answer
1987        // `isSideEffectCounterRevertible` consistently with the parent.
1988        nested_oracle.min_revertible_side_effect_counter = self.min_revertible_side_effect_counter;
1989        // Share context witness prefix (block header + tx_context) for nested calls.
1990        nested_oracle.context_witness_prefix = self.context_witness_prefix.clone();
1991        // Share capsules so nested protocol contract handlers can access bytecode data.
1992        nested_oracle.capsules = self.capsules.clone();
1993        // Share parent state so nested calls can see notes/hashes from sibling
1994        // calls. Track inherited sizes to avoid duplicating during merge.
1995        nested_oracle.new_notes = self.new_notes.clone();
1996        nested_oracle.note_hashes = self.note_hashes.clone();
1997        nested_oracle.nullifiers = self.nullifiers.clone();
1998        nested_oracle.note_hash_nullifier_counter_map =
1999            self.note_hash_nullifier_counter_map.clone();
2000        nested_oracle.consumed_db_nullifiers = self.consumed_db_nullifiers.clone();
2001        let inherited_new_notes = self.new_notes.len();
2002        let inherited_note_hashes = self.note_hashes.len();
2003        let inherited_nullifiers = self.nullifiers.len();
2004        let inherited_counter_map_keys: std::collections::HashSet<u32> = self
2005            .note_hash_nullifier_counter_map
2006            .keys()
2007            .copied()
2008            .collect();
2009
2010        // Execute the nested private function.
2011        let acvm_output = super::acvm_executor::AcvmExecutor::execute_private(
2012            &artifact,
2013            &function_name,
2014            &full_witness,
2015            &mut nested_oracle,
2016        )
2017        .await?;
2018
2019        // Compute the actual end counter from the maximum counter across all
2020        // side effects produced by the nested call. The oracle's side_effect_counter
2021        // may not advance when counters come from circuit args, so we scan
2022        // note hashes, nullifiers, and logs to find the true maximum.
2023        let end_counter = {
2024            let nh_max = nested_oracle
2025                .note_hashes
2026                .iter()
2027                .skip(inherited_note_hashes)
2028                .map(|nh| nh.note_hash.counter)
2029                .max()
2030                .unwrap_or(0);
2031            let null_max = nested_oracle
2032                .nullifiers
2033                .iter()
2034                .skip(inherited_nullifiers)
2035                .map(|n| n.nullifier.counter)
2036                .max()
2037                .unwrap_or(0);
2038            let log_max = nested_oracle
2039                .private_logs
2040                .iter()
2041                .map(|l| l.counter)
2042                .max()
2043                .unwrap_or(0);
2044            let oracle_counter = nested_oracle.side_effect_counter;
2045            nh_max.max(null_max).max(log_max).max(oracle_counter)
2046        };
2047
2048        // Extract returns_hash and end_side_effect_counter from the PCPI
2049        // in the witness, not from ACIR return values. The PCPI starts at
2050        // offset `nested_params_size` in the witness.
2051        let nested_ctx_size_for_pcpi = artifact.private_context_inputs_size(&function_name);
2052        let pcpi_start = nested_ctx_size_for_pcpi + cached_args.len();
2053        // PCPI layout: call_context(4), args_hash(1), returns_hash(1), ...
2054        const PCPI_RETURNS_HASH_OFFSET: usize = 5;
2055
2056        let returns_hash = {
2057            let idx = acir::native_types::Witness((pcpi_start + PCPI_RETURNS_HASH_OFFSET) as u32);
2058            acvm_output
2059                .witness
2060                .get(&idx)
2061                .map(|fe| super::field_conversion::fe_to_fr(fe))
2062                .unwrap_or_else(|| {
2063                    // Fallback: compute from return values (may be empty → zero hash)
2064                    aztec_core::hash::compute_var_args_hash(&acvm_output.return_values)
2065                })
2066        };
2067
2068        // Also store the return values from the execution cache stored by
2069        // the nested circuit itself (via storeInExecutionCache oracle).
2070        // The circuit stores its return values at returns_hash before it
2071        // finishes, so they should already be in the nested oracle's cache.
2072        // We only need to ensure our cache also has them.
2073        if !self.execution_cache.contains_key(&returns_hash) {
2074            if let Some(cached) = nested_oracle.execution_cache.get(&returns_hash) {
2075                self.execution_cache.insert(returns_hash, cached.clone());
2076            } else {
2077                // Store ACVM return values as fallback
2078                self.execution_cache
2079                    .insert(returns_hash, acvm_output.return_values.clone());
2080            }
2081        }
2082
2083        // Extract circuit-constrained side effects (private logs, note hashes, etc.)
2084        // from the nested witness. These are NOT emitted through oracle calls.
2085        let nested_ctx_size = artifact.private_context_inputs_size(&function_name);
2086        let nested_params_size = nested_ctx_size + cached_args.len();
2087        let (circuit_note_hashes, _circuit_nullifiers, circuit_logs) =
2088            Self::extract_side_effects_from_witness(
2089                &acvm_output.witness,
2090                nested_params_size,
2091                target_address,
2092            );
2093
2094        // Capture the nested call's return values for extraction by simulate_tx.
2095        // Only return_values are stored here — side effects (nullifiers, note
2096        // hashes, etc.) are merged into the parent oracle below and must NOT be
2097        // duplicated in nested_execution_results or the kernel will reject the
2098        // tx with "Duplicate nullifier".
2099        //
2100        // For private functions with databus returns, the main circuit's return
2101        // values are the full PCPI structure.  The user's actual return values
2102        // live in the first ACIR sub-circuit call (the inner function body),
2103        // captured by `first_acir_call_return_values`.
2104        {
2105            let mut minimal = PrivateCallExecutionResult::default();
2106            minimal.contract_address = target_address;
2107            minimal.return_values = if !acvm_output.first_acir_call_return_values.is_empty() {
2108                acvm_output.first_acir_call_return_values.clone()
2109            } else {
2110                acvm_output.return_values.clone()
2111            };
2112            self.nested_results.push(minimal);
2113        }
2114
2115        // Merge the nested execution cache back into the parent.
2116        for (k, v) in nested_oracle.execution_cache {
2117            self.execution_cache.entry(k).or_insert(v);
2118        }
2119
2120        // Merge side effects from the nested execution into the parent.
2121        // Skip inherited items to avoid duplicates — only take new additions.
2122        let new_note_hashes: Vec<_> = nested_oracle
2123            .note_hashes
2124            .into_iter()
2125            .skip(inherited_note_hashes)
2126            .collect();
2127        let oracle_has_note_hashes = !new_note_hashes.is_empty();
2128        self.note_hashes.extend(new_note_hashes);
2129        if !oracle_has_note_hashes && !circuit_note_hashes.is_empty() {
2130            self.note_hashes.extend(circuit_note_hashes);
2131        }
2132        self.nullifiers.extend(
2133            nested_oracle
2134                .nullifiers
2135                .into_iter()
2136                .skip(inherited_nullifiers),
2137        );
2138        // Preserve the nested subtree's full log set and replace entries when
2139        // the witness provides a more accurate version for the same counter.
2140        self.private_logs.extend(Self::merge_nested_private_logs(
2141            nested_oracle.private_logs,
2142            circuit_logs,
2143        ));
2144        self.contract_class_logs
2145            .extend(nested_oracle.contract_class_logs);
2146        self.new_notes.extend(
2147            nested_oracle
2148                .new_notes
2149                .into_iter()
2150                .skip(inherited_new_notes),
2151        );
2152        self.note_hash_read_requests
2153            .extend(nested_oracle.note_hash_read_requests);
2154        self.nullifier_read_requests
2155            .extend(nested_oracle.nullifier_read_requests);
2156        self.public_call_requests
2157            .extend(nested_oracle.public_call_requests);
2158        self.public_function_calldata
2159            .extend(nested_oracle.public_function_calldata);
2160        self.offchain_effects.extend(nested_oracle.offchain_effects);
2161        for (k, v) in nested_oracle.note_hash_nullifier_counter_map {
2162            if !inherited_counter_map_keys.contains(&k) {
2163                self.note_hash_nullifier_counter_map.insert(k, v);
2164            }
2165        }
2166        if nested_oracle.public_teardown_call_request.is_some() {
2167            self.public_teardown_call_request = nested_oracle.public_teardown_call_request;
2168        }
2169        // Merge consumed DB nullifiers from nested call.
2170        self.consumed_db_nullifiers
2171            .extend(&nested_oracle.consumed_db_nullifiers);
2172
2173        // Advance the parent's side effect counter.
2174        self.side_effect_counter = end_counter;
2175
2176        // Return [endSideEffectCounter, returnsHash] as a single array.
2177        Ok(vec![vec![Fr::from(end_counter as u64), returns_hash]])
2178    }
2179
2180    /// Get the block header.
2181    pub fn block_header(&self) -> &serde_json::Value {
2182        &self.block_header
2183    }
2184
2185    /// Build a `PrivateExecutionResult` from the ACVM output and oracle-collected
2186    /// side effects. This is the bridge between raw ACVM execution and the typed
2187    /// kernel input structures.
2188    pub fn build_execution_result(
2189        &self,
2190        acvm_output: AcvmExecutionOutput,
2191        contract_address: AztecAddress,
2192        expiration_timestamp: u64,
2193    ) -> PrivateExecutionResult {
2194        let entrypoint = PrivateCallExecutionResult {
2195            acir: acvm_output.acir_bytecode,
2196            vk: Vec::new(), // VK extracted later from artifact
2197            partial_witness: acvm_output.witness,
2198            contract_address,
2199            call_context: CallContext {
2200                msg_sender: AztecAddress::zero(), // Set by caller
2201                contract_address,
2202                function_selector: Fr::zero(),
2203                is_static_call: self.call_is_static,
2204            },
2205            return_values: acvm_output.return_values,
2206            new_notes: self.new_notes.clone(),
2207            note_hash_nullifier_counter_map: self.note_hash_nullifier_counter_map.clone(),
2208            offchain_effects: self.offchain_effects.clone(),
2209            pre_tags: Vec::new(),
2210            nested_execution_results: self.nested_results.clone(),
2211            contract_class_logs: self.contract_class_logs.clone(),
2212            note_hashes: self.note_hashes.clone(),
2213            nullifiers: self.nullifiers.clone(),
2214            note_hash_read_requests: self.note_hash_read_requests.clone(),
2215            nullifier_read_requests: self.nullifier_read_requests.clone(),
2216            private_logs: self.private_logs.clone(),
2217            public_call_requests: self.public_call_requests.clone(),
2218            public_teardown_call_request: self.public_teardown_call_request.clone(),
2219            start_side_effect_counter: 0,
2220            end_side_effect_counter: self.side_effect_counter,
2221            min_revertible_side_effect_counter: self.min_revertible_side_effect_counter,
2222        };
2223
2224        // The first nullifier is always the protocol nullifier (hash of
2225        // the tx request). Application nullifiers are separate.
2226        let first_nullifier = self.protocol_nullifier;
2227
2228        PrivateExecutionResult {
2229            entrypoint,
2230            first_nullifier,
2231            expiration_timestamp,
2232            public_function_calldata: self.public_function_calldata.clone(),
2233        }
2234    }
2235}
2236
2237/// Serialize a [`ContractInstance`] into the flat field layout expected by
2238/// the Noir `utilityGetContractInstance` / `privateGetContractInstance` oracle.
2239///
2240/// Field order must match the Noir `ContractInstance` struct:
2241///   salt, deployer, contract_class_id, initialization_hash,
2242///   npk_m (x, y, is_infinite), ivpk_m, ovpk_m, tpk_m
2243pub(crate) fn contract_instance_to_fields(inst: &ContractInstance) -> Vec<Vec<Fr>> {
2244    let pk = &inst.public_keys;
2245    vec![
2246        vec![inst.salt],
2247        vec![Fr::from(inst.deployer)],
2248        vec![inst.current_contract_class_id],
2249        vec![inst.initialization_hash],
2250        vec![pk.master_nullifier_public_key.x],
2251        vec![pk.master_nullifier_public_key.y],
2252        vec![Fr::from(pk.master_nullifier_public_key.is_infinite)],
2253        vec![pk.master_incoming_viewing_public_key.x],
2254        vec![pk.master_incoming_viewing_public_key.y],
2255        vec![Fr::from(pk.master_incoming_viewing_public_key.is_infinite)],
2256        vec![pk.master_outgoing_viewing_public_key.x],
2257        vec![pk.master_outgoing_viewing_public_key.y],
2258        vec![Fr::from(pk.master_outgoing_viewing_public_key.is_infinite)],
2259        vec![pk.master_tagging_public_key.x],
2260        vec![pk.master_tagging_public_key.y],
2261        vec![Fr::from(pk.master_tagging_public_key.is_infinite)],
2262    ]
2263}
2264
2265#[async_trait::async_trait]
2266impl<'a, N: AztecNode + Send + Sync + 'static> OracleCallback for PrivateExecutionOracle<'a, N> {
2267    async fn handle_foreign_call(
2268        &mut self,
2269        function: &str,
2270        inputs: Vec<Vec<Fr>>,
2271    ) -> Result<Vec<Vec<Fr>>, Error> {
2272        self.handle_foreign_call(function, inputs).await
2273    }
2274}