aztec_pxe/
embedded_pxe.rs

1//! Embedded PXE implementation that runs PXE logic in-process.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use aztec_core::abi::FunctionSelector;
7use aztec_core::abi::{abi_type_signature, ContractArtifact, EventSelector, FunctionType};
8use aztec_core::constants::{
9    contract_class_published_magic_value, contract_instance_published_magic_value,
10    current_vk_tree_root, protocol_contract_address, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS,
11};
12use aztec_core::error::Error;
13use aztec_core::hash::{
14    compute_contract_address_from_instance, compute_contract_class_id,
15    compute_contract_class_id_from_artifact, compute_protocol_contracts_hash,
16    compute_protocol_nullifier,
17};
18use aztec_core::tx::{compute_tx_request_hash, Capsule, FunctionCall, TxContext};
19use aztec_core::types::{
20    AztecAddress, CompleteAddress, ContractInstance, ContractInstanceWithAddress, Fr, Point,
21    PublicKeys,
22};
23use aztec_crypto::complete_address_from_secret_key_and_partial_address;
24use aztec_crypto::schnorr::{schnorr_verify, SchnorrSignature};
25use aztec_node_client::AztecNode;
26use aztec_pxe_client::{
27    BlockHeader, ExecuteUtilityOpts, PackedPrivateEvent, PrivateEventFilter, ProfileTxOpts, Pxe,
28    RegisterContractRequest, SimulateTxOpts, TxExecutionRequest, TxProfileResult, TxProvingResult,
29    TxSimulationResult, UtilityExecutionResult,
30};
31
32use crate::kernel::prover::{BbPrivateKernelProver, BbProverConfig};
33use crate::stores::anchor_block_store::AnchorBlockHeader;
34use crate::stores::kv::KvStore;
35use crate::stores::{
36    AddressStore, AnchorBlockStore, CapsuleStore, ContractStore, KeyStore, NoteStore,
37    PrivateEventStore, RecipientTaggingStore, SenderStore, SenderTaggingStore,
38};
39use crate::sync::block_state_synchronizer::{BlockStateSynchronizer, BlockSyncConfig};
40use crate::sync::event_filter::PrivateEventFilterValidator;
41use crate::sync::ContractSyncService;
42
43#[derive(Debug, serde::Deserialize)]
44#[serde(rename_all = "camelCase")]
45struct DecodedTxExecutionRequest {
46    origin: AztecAddress,
47    first_call_args_hash: Fr,
48    args_of_calls: Vec<aztec_core::tx::HashedValues>,
49    #[serde(default)]
50    fee_payer: Option<AztecAddress>,
51}
52
53#[derive(Debug, Clone, Copy)]
54struct ParsedEntrypointCall {
55    args_hash: Fr,
56    selector: aztec_core::abi::FunctionSelector,
57    to: AztecAddress,
58    is_public: bool,
59    hide_msg_sender: bool,
60    is_static: bool,
61}
62
63#[derive(Debug)]
64struct DecodedEntrypointCall {
65    to: AztecAddress,
66    selector: aztec_core::abi::FunctionSelector,
67    encoded_args: Vec<Fr>,
68    hide_msg_sender: bool,
69    is_static: bool,
70}
71
72#[derive(Debug, Clone)]
73struct CallExecutionBundle {
74    execution_result: crate::execution::execution_result::PrivateExecutionResult,
75    contract_class_log_fields: Vec<aztec_core::tx::ContractClassLogFields>,
76    public_function_calldata: Vec<aztec_core::tx::HashedValues>,
77    /// Return values from the first inner ACIR call (for private return value extraction).
78    first_acir_call_return_values: Vec<Fr>,
79    /// User-visible return values for the top-level simulated call bundle.
80    simulated_return_values: Vec<Fr>,
81}
82
83fn parse_encoded_calls(fields: &[Fr]) -> Result<Vec<ParsedEntrypointCall>, Error> {
84    const CALL_FIELDS: usize = 6;
85    const APP_MAX_CALLS: usize = 5;
86    let required = CALL_FIELDS * APP_MAX_CALLS + 1;
87    if fields.len() < required {
88        return Err(Error::InvalidData(format!(
89            "entrypoint args too short: {} < {}",
90            fields.len(),
91            required
92        )));
93    }
94
95    let mut calls = Vec::with_capacity(APP_MAX_CALLS);
96    for idx in 0..APP_MAX_CALLS {
97        let offset = idx * CALL_FIELDS;
98        calls.push(ParsedEntrypointCall {
99            args_hash: fields[offset],
100            selector: aztec_core::abi::FunctionSelector::from_field(fields[offset + 1]),
101            to: AztecAddress(fields[offset + 2]),
102            is_public: fields[offset + 3] != Fr::zero(),
103            hide_msg_sender: fields[offset + 4] != Fr::zero(),
104            is_static: fields[offset + 5] != Fr::zero(),
105        });
106    }
107    Ok(calls)
108}
109
110/// Embedded PXE that runs private execution logic in-process.
111///
112/// In-process PXE for Aztec v4.x where PXE runs client-side.
113/// Talks to the Aztec node via `node_*` RPC methods and maintains local
114/// stores for contracts, keys, addresses, notes, capsules, and events.
115///
116/// Phase 3 additions: anchor block tracking, block reorg handling,
117/// private event retrieval, transaction profiling, and persistent storage.
118pub struct EmbeddedPxe<N: AztecNode> {
119    node: N,
120    contract_store: ContractStore,
121    key_store: KeyStore,
122    address_store: AddressStore,
123    note_store: Arc<NoteStore>,
124    #[allow(dead_code)] // Used when ACVM integration is complete
125    capsule_store: CapsuleStore,
126    /// Registered sender addresses for private log discovery.
127    sender_store: SenderStore,
128    /// Sender tagging store for outgoing tag index tracking.
129    #[allow(dead_code)] // Used when full prove_tx flow is wired
130    sender_tagging_store: SenderTaggingStore,
131    /// Recipient tagging store for incoming tag index tracking.
132    #[allow(dead_code)] // Used when full prove_tx flow is wired
133    recipient_tagging_store: RecipientTaggingStore,
134    /// Private event store for discovered private events.
135    private_event_store: Arc<PrivateEventStore>,
136    /// Kernel prover for generating proofs via bb binary.
137    #[allow(dead_code)] // Used when full prove_tx flow is wired
138    kernel_prover: BbPrivateKernelProver,
139    /// Anchor block store for persistent block header tracking.
140    anchor_block_store: Arc<AnchorBlockStore>,
141    /// Block state synchronizer with reorg handling.
142    block_synchronizer: BlockStateSynchronizer,
143    /// Contract sync service for note discovery caching.
144    contract_sync_service: ContractSyncService<N>,
145    /// VK tree root from node info — needed in TxConstantData.
146    vk_tree_root: Fr,
147    /// Protocol contracts hash from node info — needed in TxConstantData.
148    protocol_contracts_hash: Fr,
149}
150
151/// Configuration for EmbeddedPxe creation.
152#[derive(Debug, Clone)]
153pub struct EmbeddedPxeConfig {
154    /// BB prover configuration.
155    pub prover_config: BbProverConfig,
156    /// Block synchronization configuration.
157    pub block_sync_config: BlockSyncConfig,
158}
159
160impl Default for EmbeddedPxeConfig {
161    fn default() -> Self {
162        Self {
163            prover_config: BbProverConfig::default(),
164            block_sync_config: BlockSyncConfig::default(),
165        }
166    }
167}
168
169impl<N: AztecNode + Clone + 'static> EmbeddedPxe<N> {
170    async fn execute_sync_state_for_contract(
171        &self,
172        contract_address: AztecAddress,
173        scopes: Vec<AztecAddress>,
174    ) -> Result<(), Error> {
175        let Some(instance) = self.contract_store.get_instance(&contract_address).await? else {
176            return Ok(());
177        };
178        let Some(artifact) = self
179            .contract_store
180            .get_artifact(&instance.inner.current_contract_class_id)
181            .await?
182        else {
183            return Ok(());
184        };
185
186        let Ok(function) = artifact.find_function("sync_state") else {
187            return Ok(());
188        };
189        if function.function_type != FunctionType::Utility {
190            return Ok(());
191        }
192
193        let selector = function.selector.ok_or_else(|| {
194            Error::InvalidData(format!(
195                "sync_state missing selector in artifact {}",
196                artifact.name
197            ))
198        })?;
199
200        let call = FunctionCall {
201            to: contract_address,
202            selector,
203            args: vec![],
204            function_type: FunctionType::Utility,
205            is_static: function.is_static,
206            hide_msg_sender: false,
207        };
208
209        self.execute_utility(
210            &call,
211            ExecuteUtilityOpts {
212                scopes,
213                ..Default::default()
214            },
215        )
216        .await?;
217        Ok(())
218    }
219
220    async fn persist_pending_notes(
221        &self,
222        exec_result: &crate::execution::PrivateExecutionResult,
223        scopes: &[AztecAddress],
224    ) -> Result<(), Error> {
225        let nullifiers_by_counter: std::collections::HashMap<u32, Fr> = exec_result
226            .all_nullifiers()
227            .into_iter()
228            .map(|n| (n.nullifier.counter, n.nullifier.value))
229            .collect();
230        let note_to_nullifier_counter = exec_result.all_note_hash_nullifier_counter_maps();
231
232        for call in exec_result.iter_all_calls() {
233            for note in &call.new_notes {
234                // Check if this note was also nullified in the same execution
235                // (transient note — squashed by the kernel).
236                let (siloed_nullifier, nullified) = if let Some(nullifier_counter) =
237                    note_to_nullifier_counter.get(&note.counter).copied()
238                {
239                    if let Some(sn) = nullifiers_by_counter.get(&nullifier_counter).copied() {
240                        (sn, true)
241                    } else {
242                        (Fr::zero(), false)
243                    }
244                } else {
245                    (Fr::zero(), false)
246                };
247
248                let stored = crate::stores::note_store::StoredNote {
249                    contract_address: note.contract_address,
250                    owner: note.owner,
251                    storage_slot: note.storage_slot,
252                    randomness: note.randomness,
253                    note_nonce: Fr::zero(),
254                    note_hash: note.note_hash,
255                    siloed_nullifier,
256                    note_data: note.note_items.clone(),
257                    nullified,
258                    is_pending: true,
259                    nullification_block_number: None,
260                    leaf_index: None,
261                    block_number: None,
262                    tx_index_in_block: None,
263                    note_index_in_tx: None,
264                    scopes: vec![],
265                };
266
267                let mut stored_for_owner = false;
268                for scope in scopes {
269                    if *scope == note.owner {
270                        self.note_store.add_notes(&[stored.clone()], scope).await?;
271                        stored_for_owner = true;
272                    }
273                }
274
275                if !stored_for_owner {
276                    let fallback_scope = if !note.owner.0.is_zero() {
277                        note.owner
278                    } else {
279                        AztecAddress::zero()
280                    };
281                    self.note_store
282                        .add_notes(&[stored], &fallback_scope)
283                        .await?;
284                }
285            }
286        }
287
288        Ok(())
289    }
290
291    fn tx_request_hash(tx_request: &TxExecutionRequest) -> Result<Fr, Error> {
292        #[derive(serde::Deserialize)]
293        #[serde(rename_all = "camelCase")]
294        struct WireTxRequest {
295            origin: AztecAddress,
296            function_selector: FunctionSelector,
297            first_call_args_hash: Fr,
298            tx_context: TxContext,
299            salt: Fr,
300        }
301
302        let request: WireTxRequest = serde_json::from_value(tx_request.data.clone())
303            .map_err(|err| Error::InvalidData(format!("invalid tx request payload: {err}")))?;
304
305        Ok(compute_tx_request_hash(
306            request.origin,
307            request.first_call_args_hash,
308            &request.tx_context,
309            request.function_selector,
310            true, // isPrivate — the account entrypoint is a private function
311            request.salt,
312        ))
313    }
314
315    /// Create a new EmbeddedPxe backed by the given node client and KV store.
316    pub async fn create(node: N, kv: Arc<dyn KvStore>) -> Result<Self, Error> {
317        Self::create_with_config(node, kv, EmbeddedPxeConfig::default()).await
318    }
319
320    /// Create a new EmbeddedPxe with custom BB prover configuration.
321    pub async fn create_with_prover_config(
322        node: N,
323        kv: Arc<dyn KvStore>,
324        prover_config: BbProverConfig,
325    ) -> Result<Self, Error> {
326        Self::create_with_config(
327            node,
328            kv,
329            EmbeddedPxeConfig {
330                prover_config,
331                ..Default::default()
332            },
333        )
334        .await
335    }
336
337    /// Create a new EmbeddedPxe with full configuration.
338    pub async fn create_with_config(
339        node: N,
340        kv: Arc<dyn KvStore>,
341        config: EmbeddedPxeConfig,
342    ) -> Result<Self, Error> {
343        let contract_store = ContractStore::new(Arc::clone(&kv));
344        let key_store = KeyStore::new(Arc::clone(&kv));
345        let address_store = AddressStore::new(Arc::clone(&kv));
346        let note_store = Arc::new(NoteStore::new(Arc::clone(&kv)));
347        let capsule_store = CapsuleStore::new(Arc::clone(&kv));
348        let sender_store = SenderStore::new(Arc::clone(&kv));
349        let sender_tagging_store = SenderTaggingStore::new(Arc::clone(&kv));
350        let recipient_tagging_store = RecipientTaggingStore::new(Arc::clone(&kv));
351        let private_event_store = Arc::new(PrivateEventStore::new(Arc::clone(&kv)));
352        let anchor_block_store = Arc::new(AnchorBlockStore::new(Arc::clone(&kv)));
353        let kernel_prover = BbPrivateKernelProver::new(config.prover_config);
354
355        // Block state synchronizer with reorg handling
356        let block_synchronizer = BlockStateSynchronizer::new(
357            Arc::clone(&anchor_block_store),
358            Arc::clone(&note_store),
359            Arc::clone(&private_event_store),
360            config.block_sync_config,
361        );
362
363        // Contract sync service — needs a shared reference to the node
364        let contract_sync_service =
365            ContractSyncService::new(Arc::new(node.clone()), Arc::clone(&note_store));
366
367        // The node API does not expose these fields on Aztec v4.x.
368        // Match upstream PXE behavior by deriving them locally and only
369        // honoring node-provided values when they are explicitly present.
370        let node_info = node.get_node_info().await?;
371        let vk_tree_root = node_info
372            .l2_circuits_vk_tree_root
373            .as_deref()
374            .and_then(|s| Fr::from_hex(s).ok())
375            .unwrap_or_else(current_vk_tree_root);
376        let protocol_contracts_hash = node_info
377            .l2_protocol_contracts_hash
378            .as_deref()
379            .and_then(|s| Fr::from_hex(s).ok())
380            .unwrap_or_else(compute_protocol_contracts_hash);
381
382        let pxe = Self {
383            node,
384            contract_store,
385            key_store,
386            address_store,
387            note_store,
388            capsule_store,
389            sender_store,
390            sender_tagging_store,
391            recipient_tagging_store,
392            private_event_store,
393            kernel_prover,
394            anchor_block_store,
395            block_synchronizer,
396            contract_sync_service,
397            vk_tree_root,
398            protocol_contracts_hash,
399        };
400
401        // Initial block sync
402        pxe.block_synchronizer.sync(&pxe.node).await?;
403
404        Ok(pxe)
405    }
406
407    /// Create a new EmbeddedPxe with an in-memory KV store.
408    pub async fn create_ephemeral(node: N) -> Result<Self, Error> {
409        let kv = Arc::new(crate::stores::InMemoryKvStore::new());
410        Self::create(node, kv).await
411    }
412
413    /// Recursively flatten an [`AbiValue`] into field elements.
414    fn abi_value_to_fields(v: &aztec_core::abi::AbiValue) -> Vec<Fr> {
415        match v {
416            aztec_core::abi::AbiValue::Field(f) => vec![*f],
417            aztec_core::abi::AbiValue::Integer(i) => vec![Fr::from(*i as u64)],
418            aztec_core::abi::AbiValue::Boolean(b) => vec![Fr::from(*b)],
419            aztec_core::abi::AbiValue::String(s) => s.chars().map(|c| Fr::from(c as u64)).collect(),
420            aztec_core::abi::AbiValue::Array(arr) => {
421                arr.iter().flat_map(Self::abi_value_to_fields).collect()
422            }
423            aztec_core::abi::AbiValue::Struct(fields) => fields
424                .values()
425                .flat_map(Self::abi_value_to_fields)
426                .collect(),
427            aztec_core::abi::AbiValue::Tuple(elems) => {
428                elems.iter().flat_map(Self::abi_value_to_fields).collect()
429            }
430        }
431    }
432
433    /// Sync the block state from the node, handling reorgs.
434    ///
435    /// After sync, if the anchor block changed, wipe the contract sync cache.
436    async fn sync_block_state(&self) -> Result<(), Error> {
437        self.block_synchronizer.sync(&self.node).await?;
438
439        // If anchor changed, wipe contract sync cache
440        if self.block_synchronizer.take_anchor_changed().await {
441            self.contract_sync_service.wipe().await;
442        }
443
444        Ok(())
445    }
446
447    /// Get the current anchor block header, syncing if necessary.
448    async fn get_anchor_block_header(&self) -> Result<AnchorBlockHeader, Error> {
449        self.sync_block_state().await?;
450        self.block_synchronizer
451            .get_anchor_block_header()
452            .await?
453            .ok_or_else(|| Error::InvalidData("anchor block header not set after sync".into()))
454    }
455
456    /// Get the current anchor block number.
457    async fn get_anchor_block_number(&self) -> Result<u64, Error> {
458        self.sync_block_state().await?;
459        self.block_synchronizer.get_anchor_block_number().await
460    }
461
462    /// Get a reference to the underlying node client.
463    pub fn node(&self) -> &N {
464        &self.node
465    }
466
467    /// Get a reference to the contract store.
468    pub fn contract_store(&self) -> &ContractStore {
469        &self.contract_store
470    }
471
472    /// Get a reference to the key store.
473    pub fn key_store(&self) -> &KeyStore {
474        &self.key_store
475    }
476
477    /// Get a reference to the address store.
478    pub fn address_store(&self) -> &AddressStore {
479        &self.address_store
480    }
481
482    /// Get a reference to the note store.
483    pub fn note_store(&self) -> &NoteStore {
484        &self.note_store
485    }
486
487    /// Get a reference to the anchor block store.
488    pub fn anchor_block_store(&self) -> &AnchorBlockStore {
489        &self.anchor_block_store
490    }
491
492    /// Get a reference to the private event store.
493    pub fn private_event_store(&self) -> &PrivateEventStore {
494        &self.private_event_store
495    }
496
497    /// Extract the target contract address, function name, encoded args, and origin
498    /// from a serialized TxExecutionRequest.
499    ///
500    /// The TxExecutionRequest is built by the account entrypoint and contains:
501    /// - `origin`: the account address (msg_sender)
502    /// - Encoded calls inside `args_of_calls` with target addresses
503    ///
504    /// We try multiple strategies to find the target contract:
505    /// 1. Direct `contractAddress` / `to` field (simple format)
506    /// 2. Extract from encoded calls (entrypoint format)
507    /// 3. Scan all registered contracts for matching function
508    #[allow(dead_code)]
509    async fn extract_call_info(
510        &self,
511        tx_request: &TxExecutionRequest,
512    ) -> Result<(AztecAddress, String, Vec<Fr>, AztecAddress), Error> {
513        let request: DecodedTxExecutionRequest = serde_json::from_value(tx_request.data.clone())?;
514        let origin = request.origin;
515
516        if let Some(call) = Self::decode_entrypoint_call(&request)? {
517            let function_name = self
518                .resolve_function_name_by_selector(&call.to, call.selector)
519                .await;
520            return Ok((call.to, function_name, call.encoded_args, origin));
521        }
522
523        // Fallback: scan registered contracts
524        let contracts = self.contract_store.get_contract_addresses().await?;
525        if let Some(addr) = contracts.first() {
526            return Ok((*addr, "unknown".to_owned(), vec![], origin));
527        }
528
529        Err(Error::InvalidData(
530            "could not determine target contract from TxExecutionRequest".into(),
531        ))
532    }
533
534    /// Resolve function name from contract address and selector hex string.
535    async fn resolve_function_name_by_selector(
536        &self,
537        addr: &AztecAddress,
538        sel: aztec_core::abi::FunctionSelector,
539    ) -> String {
540        if let Some(name) = Self::resolve_protocol_function_name(addr, sel) {
541            return name.to_owned();
542        }
543
544        let inst = match self.contract_store.get_instance(addr).await {
545            Ok(Some(i)) => i,
546            _ => return "unknown".to_owned(),
547        };
548        let artifact = match self
549            .contract_store
550            .get_artifact(&inst.inner.current_contract_class_id)
551            .await
552        {
553            Ok(Some(a)) => a,
554            _ => return "unknown".to_owned(),
555        };
556        artifact
557            .find_function_by_selector(&sel)
558            .map(|f| f.name.clone())
559            .unwrap_or_else(|| "unknown".to_owned())
560    }
561
562    fn resolve_protocol_function_name(
563        addr: &AztecAddress,
564        sel: aztec_core::abi::FunctionSelector,
565    ) -> Option<&'static str> {
566        if *addr == protocol_contract_address::contract_class_registry()
567            && sel
568                == aztec_core::abi::FunctionSelector::from_signature("publish(Field,Field,Field)")
569        {
570            return Some("publish");
571        }
572
573        if *addr == protocol_contract_address::contract_instance_registry()
574            && sel
575                == aztec_core::abi::FunctionSelector::from_signature(
576                    "publish_for_public_execution(Field,(Field),Field,(((Field,Field,bool)),((Field,Field,bool)),((Field,Field,bool)),((Field,Field,bool))),bool)",
577                )
578        {
579            return Some("publish_for_public_execution");
580        }
581
582        // AuthRegistry protocol contract
583        if *addr == protocol_contract_address::auth_registry() {
584            if sel
585                == aztec_core::abi::FunctionSelector::from_signature("set_authorized(Field,bool)")
586            {
587                return Some("set_authorized(Field,bool)");
588            }
589            if sel == aztec_core::abi::FunctionSelector::from_signature("consume((Field),Field)") {
590                return Some("consume((Field),Field)");
591            }
592        }
593
594        None
595    }
596
597    fn protocol_private_execution(
598        &self,
599        tx_request: &TxExecutionRequest,
600        contract_address: AztecAddress,
601        function_name: &str,
602        encoded_args: &[Fr],
603        origin: AztecAddress,
604        first_nullifier: Fr,
605    ) -> Result<Option<CallExecutionBundle>, Error> {
606        match (contract_address, function_name) {
607            (addr, "publish") if addr == protocol_contract_address::contract_class_registry() => {
608                let artifact_hash = *encoded_args
609                    .first()
610                    .ok_or_else(|| Error::InvalidData("publish missing artifact_hash".into()))?;
611                let private_functions_root = *encoded_args.get(1).ok_or_else(|| {
612                    Error::InvalidData("publish missing private_functions_root".into())
613                })?;
614                let public_bytecode_commitment = *encoded_args.get(2).ok_or_else(|| {
615                    Error::InvalidData("publish missing public_bytecode_commitment".into())
616                })?;
617                let class_id = compute_contract_class_id(
618                    artifact_hash,
619                    private_functions_root,
620                    public_bytecode_commitment,
621                );
622
623                let capsules = tx_request
624                    .data
625                    .get("capsules")
626                    .cloned()
627                    .map(serde_json::from_value::<Vec<Capsule>>)
628                    .transpose()?
629                    .unwrap_or_default();
630                let bytecode_fields = capsules
631                    .into_iter()
632                    .find(|capsule| {
633                        capsule.contract_address == protocol_contract_address::contract_class_registry()
634                            && capsule.storage_slot
635                                == aztec_core::constants::contract_class_registry_bytecode_capsule_slot()
636                    })
637                    .map(|capsule| capsule.data)
638                    .unwrap_or_default();
639
640                let mut emitted_fields =
641                    Vec::with_capacity(MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS + 5);
642                emitted_fields.push(contract_class_published_magic_value());
643                emitted_fields.push(class_id);
644                emitted_fields.push(Fr::from(1u64));
645                emitted_fields.push(artifact_hash);
646                emitted_fields.push(private_functions_root);
647                emitted_fields.extend(bytecode_fields);
648                let entrypoint = crate::execution::execution_result::PrivateCallExecutionResult {
649                    contract_address,
650                    call_context: aztec_core::kernel_types::CallContext {
651                        msg_sender: origin,
652                        contract_address,
653                        function_selector: FunctionSelector::from_signature(
654                            "publish(Field,Field,Field)",
655                        )
656                        .to_field(),
657                        is_static_call: false,
658                    },
659                    nullifiers: vec![aztec_core::kernel_types::ScopedNullifier {
660                        nullifier: aztec_core::kernel_types::Nullifier {
661                            value: class_id,
662                            note_hash: Fr::zero(),
663                            counter: 2,
664                        },
665                        contract_address,
666                    }],
667                    contract_class_logs: vec![aztec_core::kernel_types::CountedContractClassLog {
668                        log: aztec_core::kernel_types::ContractClassLog {
669                            contract_address,
670                            emitted_length: emitted_fields.len() as u32,
671                            fields: emitted_fields.clone(),
672                        },
673                        counter: 3,
674                    }],
675                    start_side_effect_counter: 2,
676                    end_side_effect_counter: 4,
677                    min_revertible_side_effect_counter: 2,
678                    ..Default::default()
679                };
680
681                return Ok(Some(CallExecutionBundle {
682                    first_acir_call_return_values: Vec::new(),
683                    simulated_return_values: Vec::new(),
684                    execution_result: crate::execution::execution_result::PrivateExecutionResult {
685                        entrypoint,
686                        first_nullifier,
687                        expiration_timestamp: 0,
688                        public_function_calldata: vec![],
689                    },
690                    contract_class_log_fields: vec![
691                        aztec_core::tx::ContractClassLogFields::from_emitted_fields(emitted_fields),
692                    ],
693                    public_function_calldata: vec![],
694                }));
695            }
696            (addr, "publish_for_public_execution")
697                if addr == protocol_contract_address::contract_instance_registry() =>
698            {
699                if encoded_args.len() < 16 {
700                    return Err(Error::InvalidData(format!(
701                        "publish_for_public_execution args too short: {}",
702                        encoded_args.len()
703                    )));
704                }
705
706                let salt = encoded_args[0];
707                let class_id = encoded_args[1];
708                let initialization_hash = encoded_args[2];
709                let public_keys = PublicKeys {
710                    master_nullifier_public_key: Point {
711                        x: encoded_args[3],
712                        y: encoded_args[4],
713                        is_infinite: encoded_args[5] != Fr::zero(),
714                    },
715                    master_incoming_viewing_public_key: Point {
716                        x: encoded_args[6],
717                        y: encoded_args[7],
718                        is_infinite: encoded_args[8] != Fr::zero(),
719                    },
720                    master_outgoing_viewing_public_key: Point {
721                        x: encoded_args[9],
722                        y: encoded_args[10],
723                        is_infinite: encoded_args[11] != Fr::zero(),
724                    },
725                    master_tagging_public_key: Point {
726                        x: encoded_args[12],
727                        y: encoded_args[13],
728                        is_infinite: encoded_args[14] != Fr::zero(),
729                    },
730                };
731                let universal_deploy = encoded_args[15] != Fr::zero();
732                let deployer = if universal_deploy {
733                    AztecAddress::zero()
734                } else {
735                    origin
736                };
737                let instance = ContractInstanceWithAddress {
738                    address: compute_contract_address_from_instance(&ContractInstance {
739                        version: 1,
740                        salt,
741                        deployer,
742                        current_contract_class_id: class_id,
743                        original_contract_class_id: class_id,
744                        initialization_hash,
745                        public_keys: public_keys.clone(),
746                    })?,
747                    inner: ContractInstance {
748                        version: 1,
749                        salt,
750                        deployer,
751                        current_contract_class_id: class_id,
752                        original_contract_class_id: class_id,
753                        initialization_hash,
754                        public_keys: public_keys.clone(),
755                    },
756                };
757
758                let event_payload = vec![
759                    contract_instance_published_magic_value(),
760                    instance.address.0,
761                    Fr::from(1u64),
762                    salt,
763                    class_id,
764                    initialization_hash,
765                    public_keys.master_nullifier_public_key.x,
766                    public_keys.master_nullifier_public_key.y,
767                    public_keys.master_incoming_viewing_public_key.x,
768                    public_keys.master_incoming_viewing_public_key.y,
769                    public_keys.master_outgoing_viewing_public_key.x,
770                    public_keys.master_outgoing_viewing_public_key.y,
771                    public_keys.master_tagging_public_key.x,
772                    public_keys.master_tagging_public_key.y,
773                    deployer.0,
774                ];
775                let mut emitted_private_log_fields = event_payload.clone();
776                emitted_private_log_fields.push(Fr::zero());
777                // Emit the raw address as the nullifier — the kernel silos it later
778                // with the deployer protocol contract address.
779                let entrypoint = crate::execution::execution_result::PrivateCallExecutionResult {
780                    contract_address,
781                    call_context: aztec_core::kernel_types::CallContext {
782                        msg_sender: origin,
783                        contract_address,
784                        function_selector: FunctionSelector::from_signature(
785                            "publish_for_public_execution(Field,(Field),Field,(((Field,Field,bool)),((Field,Field,bool)),((Field,Field,bool)),((Field,Field,bool))),bool)",
786                        )
787                        .to_field(),
788                        is_static_call: false,
789                    },
790                    nullifiers: vec![aztec_core::kernel_types::ScopedNullifier {
791                        nullifier: aztec_core::kernel_types::Nullifier {
792                            value: instance.address.0,
793                            note_hash: Fr::zero(),
794                            counter: 2,
795                        },
796                        contract_address,
797                    }],
798                    private_logs: vec![crate::execution::execution_result::PrivateLogData {
799                        fields: emitted_private_log_fields,
800                        emitted_length: 15,
801                        note_hash_counter: 0,
802                        counter: 3,
803                        contract_address,
804                    }],
805                    start_side_effect_counter: 2,
806                    end_side_effect_counter: 4,
807                    min_revertible_side_effect_counter: 2,
808                    ..Default::default()
809                };
810
811                return Ok(Some(CallExecutionBundle {
812                    first_acir_call_return_values: Vec::new(),
813                    simulated_return_values: Vec::new(),
814                    execution_result: crate::execution::execution_result::PrivateExecutionResult {
815                        entrypoint,
816                        first_nullifier,
817                        expiration_timestamp: 0,
818                        public_function_calldata: vec![],
819                    },
820                    contract_class_log_fields: vec![],
821                    public_function_calldata: vec![],
822                }));
823            }
824            _ => {}
825        }
826
827        Ok(None)
828    }
829
830    /// Handle public function calls to protocol contracts that may not be
831    /// registered in the local contract store (e.g. AuthRegistry).
832    ///
833    /// For public calls the PXE only needs to build a `PublicCallRequestData`;
834    /// it does not execute the function — that happens on the sequencer.
835    fn protocol_public_execution(
836        contract_address: AztecAddress,
837        function_name: &str,
838        encoded_args: &[Fr],
839        origin: AztecAddress,
840        first_nullifier: Fr,
841        hide_msg_sender: bool,
842        is_static_call: bool,
843    ) -> Result<Option<CallExecutionBundle>, Error> {
844        use crate::execution::execution_result::{
845            PrivateCallExecutionResult, PrivateExecutionResult, PublicCallRequestData,
846        };
847
848        // Only handle known protocol contract addresses.
849        if contract_address != protocol_contract_address::auth_registry() {
850            return Ok(None);
851        }
852
853        // Compute selector from function name.
854        let selector_fr: Fr = FunctionSelector::from_signature(function_name).into();
855        let mut calldata = vec![selector_fr];
856        calldata.extend_from_slice(encoded_args);
857        let hashed = aztec_core::tx::HashedValues::from_calldata(calldata);
858        let calldata_hash = hashed.hash();
859        let msg_sender = if hide_msg_sender {
860            AztecAddress::zero()
861        } else {
862            origin
863        };
864
865        let entrypoint = PrivateCallExecutionResult {
866            contract_address: origin,
867            public_call_requests: vec![PublicCallRequestData {
868                contract_address,
869                msg_sender,
870                is_static_call,
871                calldata_hash,
872                counter: 2,
873            }],
874            start_side_effect_counter: 2,
875            min_revertible_side_effect_counter: 2,
876            end_side_effect_counter: 3,
877            ..Default::default()
878        };
879
880        let exec_result = PrivateExecutionResult {
881            entrypoint,
882            first_nullifier,
883            expiration_timestamp: 0,
884            public_function_calldata: vec![hashed.clone()],
885        };
886
887        Ok(Some(CallExecutionBundle {
888            first_acir_call_return_values: Vec::new(),
889            simulated_return_values: Vec::new(),
890            execution_result: exec_result,
891            contract_class_log_fields: vec![],
892            public_function_calldata: vec![hashed],
893        }))
894    }
895
896    #[allow(dead_code)]
897    fn decode_entrypoint_call(
898        request: &DecodedTxExecutionRequest,
899    ) -> Result<Option<DecodedEntrypointCall>, Error> {
900        Ok(Self::decode_entrypoint_calls(request)?.into_iter().next())
901    }
902
903    fn decode_entrypoint_calls(
904        request: &DecodedTxExecutionRequest,
905    ) -> Result<Vec<DecodedEntrypointCall>, Error> {
906        let entrypoint_args = request
907            .args_of_calls
908            .iter()
909            .find(|hv| hv.hash == request.first_call_args_hash)
910            .ok_or_else(|| {
911                Error::InvalidData("firstCallArgsHash not found in argsOfCalls".into())
912            })?;
913
914        let encoded_calls = parse_encoded_calls(&entrypoint_args.values)?;
915        let mut decoded = Vec::new();
916        for call in encoded_calls {
917            if call.to == AztecAddress::zero()
918                || call.selector == aztec_core::abi::FunctionSelector::empty()
919            {
920                continue;
921            }
922
923            let hashed_args = request
924                .args_of_calls
925                .iter()
926                .find(|hv| hv.hash == call.args_hash)
927                .ok_or_else(|| {
928                    Error::InvalidData(format!(
929                        "call args hash {} not found in argsOfCalls",
930                        call.args_hash
931                    ))
932                })?;
933
934            let encoded_args = if call.is_public {
935                hashed_args.values.iter().copied().skip(1).collect()
936            } else {
937                hashed_args.values.clone()
938            };
939
940            decoded.push(DecodedEntrypointCall {
941                to: call.to,
942                selector: call.selector,
943                encoded_args,
944                hide_msg_sender: call.hide_msg_sender,
945                is_static: call.is_static,
946            });
947        }
948
949        Ok(decoded)
950    }
951
952    /// Extract args from tx request data.
953    #[allow(dead_code)] // Used when ACVM integration is complete
954    fn extract_args(data: &serde_json::Value) -> Vec<Fr> {
955        data.get("args")
956            .and_then(|v| v.as_array())
957            .map(|arr| {
958                arr.iter()
959                    .filter_map(|v| v.as_str().and_then(|s| Fr::from_hex(s).ok()))
960                    .collect()
961            })
962            .unwrap_or_default()
963    }
964
965    /// Build the full initial witness for a private function call.
966    ///
967    /// Private functions expect `[PrivateContextInputs fields..., user args...]`
968    /// where PrivateContextInputs is 37 fields: call_context(4) +
969    /// anchor_block_header(22) + tx_context(10) + start_side_effect_counter(1).
970    fn build_private_witness(
971        &self,
972        artifact: &ContractArtifact,
973        function_name: &str,
974        user_args: &[Fr],
975        contract_address: AztecAddress,
976        msg_sender: AztecAddress,
977        tx_request: &TxExecutionRequest,
978        anchor: &AnchorBlockHeader,
979        function_selector: aztec_core::abi::FunctionSelector,
980        is_static_call: bool,
981    ) -> Vec<Fr> {
982        let context_size = artifact.private_context_inputs_size(function_name);
983        if context_size == 0 {
984            // Not a private function or no context inputs needed
985            return user_args.to_vec();
986        }
987
988        let mut witness = Vec::with_capacity(context_size + user_args.len());
989        let tx_constants = Self::build_tx_constant_data(
990            anchor,
991            tx_request,
992            self.vk_tree_root,
993            self.protocol_contracts_hash,
994        );
995        let call_context = aztec_core::kernel_types::CallContext {
996            msg_sender,
997            contract_address,
998            function_selector: function_selector.to_field(),
999            is_static_call,
1000        };
1001
1002        witness.extend(call_context.to_fields());
1003        witness.extend(tx_constants.anchor_block_header.to_fields());
1004        witness.extend(tx_constants.tx_context.to_fields());
1005        // Upstream reserves the first side effect slot for the tx hash.
1006        witness.push(Fr::from(2u64));
1007
1008        // Ensure we have exactly context_size fields
1009        witness.truncate(context_size);
1010        while witness.len() < context_size {
1011            witness.push(Fr::zero());
1012        }
1013
1014        // Append user args
1015        witness.extend_from_slice(user_args);
1016
1017        witness
1018    }
1019
1020    fn offset_private_call_result(
1021        call: &crate::execution::execution_result::PrivateCallExecutionResult,
1022        offset: u32,
1023    ) -> crate::execution::execution_result::PrivateCallExecutionResult {
1024        let mut adjusted = call.clone();
1025        adjusted.start_side_effect_counter =
1026            adjusted.start_side_effect_counter.saturating_add(offset);
1027        adjusted.end_side_effect_counter = adjusted.end_side_effect_counter.saturating_add(offset);
1028        adjusted.min_revertible_side_effect_counter = adjusted
1029            .min_revertible_side_effect_counter
1030            .saturating_add(offset);
1031
1032        for note in &mut adjusted.new_notes {
1033            note.counter = note.counter.saturating_add(offset);
1034        }
1035        adjusted.note_hash_nullifier_counter_map = adjusted
1036            .note_hash_nullifier_counter_map
1037            .iter()
1038            .map(|(note_counter, nullifier_counter)| {
1039                (
1040                    note_counter.saturating_add(offset),
1041                    nullifier_counter.saturating_add(offset),
1042                )
1043            })
1044            .collect();
1045        for log in &mut adjusted.contract_class_logs {
1046            log.counter = log.counter.saturating_add(offset);
1047        }
1048        for note_hash in &mut adjusted.note_hashes {
1049            note_hash.note_hash.counter = note_hash.note_hash.counter.saturating_add(offset);
1050        }
1051        for nullifier in &mut adjusted.nullifiers {
1052            nullifier.nullifier.counter = nullifier.nullifier.counter.saturating_add(offset);
1053        }
1054        for req in &mut adjusted.note_hash_read_requests {
1055            req.read_request.counter = req.read_request.counter.saturating_add(offset);
1056        }
1057        for req in &mut adjusted.nullifier_read_requests {
1058            req.read_request.counter = req.read_request.counter.saturating_add(offset);
1059        }
1060        for log in &mut adjusted.private_logs {
1061            log.counter = log.counter.saturating_add(offset);
1062        }
1063        for req in &mut adjusted.public_call_requests {
1064            req.counter = req.counter.saturating_add(offset);
1065        }
1066        if let Some(req) = &mut adjusted.public_teardown_call_request {
1067            req.counter = req.counter.saturating_add(offset);
1068        }
1069        adjusted.nested_execution_results = adjusted
1070            .nested_execution_results
1071            .iter()
1072            .map(|nested| Self::offset_private_call_result(nested, offset))
1073            .collect();
1074        adjusted
1075    }
1076
1077    fn aggregate_call_bundles(
1078        origin: AztecAddress,
1079        bundles: Vec<CallExecutionBundle>,
1080    ) -> CallExecutionBundle {
1081        let mut offset = 0u32;
1082        let mut nested_execution_results = Vec::with_capacity(bundles.len());
1083        let mut first_nullifier = Fr::zero();
1084        let mut public_function_calldata = Vec::new();
1085        let mut contract_class_log_fields = Vec::new();
1086        let mut min_revertible_side_effect_counter = 0u32;
1087        let mut first_acir_returns = Vec::new();
1088        let mut simulated_return_values = Vec::new();
1089        let mut expiration_timestamp = 0u64;
1090
1091        for (idx, bundle) in bundles.into_iter().enumerate() {
1092            if idx == 0 {
1093                first_nullifier = bundle.execution_result.first_nullifier;
1094                first_acir_returns = bundle.first_acir_call_return_values;
1095                simulated_return_values = bundle.simulated_return_values;
1096                expiration_timestamp = bundle.execution_result.expiration_timestamp;
1097                min_revertible_side_effect_counter = bundle
1098                    .execution_result
1099                    .entrypoint
1100                    .min_revertible_side_effect_counter;
1101            } else if bundle.execution_result.expiration_timestamp != 0 {
1102                expiration_timestamp = if expiration_timestamp == 0 {
1103                    bundle.execution_result.expiration_timestamp
1104                } else {
1105                    expiration_timestamp.min(bundle.execution_result.expiration_timestamp)
1106                };
1107            }
1108            let adjusted_entrypoint =
1109                Self::offset_private_call_result(&bundle.execution_result.entrypoint, offset);
1110            offset = adjusted_entrypoint.end_side_effect_counter;
1111            nested_execution_results.push(adjusted_entrypoint);
1112            public_function_calldata.extend(bundle.public_function_calldata);
1113            contract_class_log_fields.extend(bundle.contract_class_log_fields);
1114        }
1115
1116        let root = crate::execution::execution_result::PrivateCallExecutionResult {
1117            contract_address: origin,
1118            call_context: aztec_core::kernel_types::CallContext {
1119                msg_sender: origin,
1120                contract_address: origin,
1121                function_selector: Fr::zero(),
1122                is_static_call: false,
1123            },
1124            nested_execution_results,
1125            start_side_effect_counter: 0,
1126            end_side_effect_counter: offset,
1127            min_revertible_side_effect_counter,
1128            ..Default::default()
1129        };
1130
1131        CallExecutionBundle {
1132            first_acir_call_return_values: first_acir_returns,
1133            simulated_return_values,
1134            execution_result: crate::execution::execution_result::PrivateExecutionResult {
1135                entrypoint: root,
1136                first_nullifier,
1137                expiration_timestamp,
1138                public_function_calldata: public_function_calldata.clone(),
1139            },
1140            contract_class_log_fields,
1141            public_function_calldata,
1142        }
1143    }
1144
1145    async fn execute_entrypoint_call_bundle(
1146        &self,
1147        tx_request: &TxExecutionRequest,
1148        call: &DecodedEntrypointCall,
1149        origin: AztecAddress,
1150        protocol_nullifier: Fr,
1151        anchor: &AnchorBlockHeader,
1152        scopes: &[AztecAddress],
1153    ) -> Result<CallExecutionBundle, Error> {
1154        let contract_address = call.to;
1155        let function_name = self
1156            .resolve_function_name_by_selector(&contract_address, call.selector)
1157            .await;
1158
1159        if let Some(bundle) = self.protocol_private_execution(
1160            tx_request,
1161            contract_address,
1162            &function_name,
1163            &call.encoded_args,
1164            origin,
1165            protocol_nullifier,
1166        )? {
1167            return Ok(bundle);
1168        }
1169
1170        // Handle public calls to protocol contracts (e.g. AuthRegistry)
1171        // that may not be registered in the local contract store.
1172        if let Some(bundle) = Self::protocol_public_execution(
1173            contract_address,
1174            &function_name,
1175            &call.encoded_args,
1176            origin,
1177            protocol_nullifier,
1178            call.hide_msg_sender,
1179            call.is_static,
1180        )? {
1181            return Ok(bundle);
1182        }
1183
1184        let contract_instance = self.contract_store.get_instance(&contract_address).await?;
1185        let class_id = contract_instance
1186            .as_ref()
1187            .map(|i| i.inner.current_contract_class_id)
1188            .ok_or_else(|| Error::InvalidData(format!("contract not found: {contract_address}")))?;
1189        let artifact = self
1190            .contract_store
1191            .get_artifact(&class_id)
1192            .await?
1193            .ok_or_else(|| {
1194                Error::InvalidData(format!("artifact not found for class {class_id}"))
1195            })?;
1196        let function = artifact.find_function(&function_name)?;
1197
1198        if function.function_type == FunctionType::Public {
1199            let (execution_result, contract_class_log_fields, public_function_calldata) =
1200                Self::build_public_call_execution(
1201                    &artifact,
1202                    &function_name,
1203                    &call.encoded_args,
1204                    contract_address,
1205                    origin,
1206                    protocol_nullifier,
1207                    call.hide_msg_sender,
1208                    call.is_static,
1209                )?;
1210            return Ok(CallExecutionBundle {
1211                first_acir_call_return_values: Vec::new(),
1212                simulated_return_values: Vec::new(),
1213                execution_result,
1214                contract_class_log_fields,
1215                public_function_calldata,
1216            });
1217        }
1218
1219        let full_witness = self.build_private_witness(
1220            &artifact,
1221            &function_name,
1222            &call.encoded_args,
1223            contract_address,
1224            origin,
1225            tx_request,
1226            anchor,
1227            function.selector.expect("private function selector"),
1228            function.is_static,
1229        );
1230
1231        let mut oracle = crate::execution::PrivateExecutionOracle::new(
1232            &self.node,
1233            &self.contract_store,
1234            &self.key_store,
1235            &self.note_store,
1236            &self.capsule_store,
1237            &self.address_store,
1238            &self.sender_tagging_store,
1239            anchor.data.clone(),
1240            contract_address,
1241            protocol_nullifier,
1242            Some(origin),
1243            scopes.to_vec(),
1244            call.is_static,
1245        );
1246
1247        // Extract and set auth witnesses from the TX request so nested
1248        // calls (e.g. verify_private_authwit) can look up the signature.
1249        if let Some(auth_witnesses) = tx_request.data.get("authWitnesses").and_then(|v| {
1250            serde_json::from_value::<Vec<aztec_core::tx::AuthWitness>>(v.clone()).ok()
1251        }) {
1252            let pairs: Vec<(Fr, Vec<Fr>)> = auth_witnesses
1253                .iter()
1254                .map(|aw| (aw.request_hash, aw.fields.clone()))
1255                .collect();
1256            oracle.set_auth_witnesses(pairs);
1257        }
1258
1259        // Store the block-header + tx-context portion of the witness so
1260        // nested calls can reuse it (for chain_id, version, etc.).
1261        let context_inputs_size = artifact.private_context_inputs_size(&function_name);
1262        if context_inputs_size > 5 {
1263            // Skip call_context (4 fields), take everything up to (but not
1264            // including) the last field (start_side_effect_counter).
1265            oracle.context_witness_prefix =
1266                full_witness[4..context_inputs_size.saturating_sub(1)].to_vec();
1267        }
1268
1269        let acvm_output = crate::execution::AcvmExecutor::execute_private(
1270            &artifact,
1271            &function_name,
1272            &full_witness,
1273            &mut oracle,
1274        )
1275        .await?;
1276
1277        // Extract return values from the execution cache (for databus returns).
1278        // The PCPI layout: call_context(4), args_hash(1), returns_hash(1), ...
1279        // returns_hash is at offset 5 from the PCPI start.
1280        let acir_call_returns = {
1281            let ctx_size = artifact.private_context_inputs_size(&function_name);
1282            let user_args_size = call.encoded_args.len();
1283            let pcpi_start = ctx_size + user_args_size;
1284            const PCPI_RETURNS_HASH_OFFSET: usize = 5;
1285            let returns_hash_idx =
1286                acir::native_types::Witness((pcpi_start + PCPI_RETURNS_HASH_OFFSET) as u32);
1287            let returns_hash = acvm_output
1288                .witness
1289                .get(&returns_hash_idx)
1290                .map(super::execution::field_conversion::fe_to_fr);
1291            if let Some(rh) = returns_hash {
1292                oracle.get_execution_cache_entry(&rh).unwrap_or_default()
1293            } else {
1294                acvm_output.first_acir_call_return_values.clone()
1295            }
1296        };
1297        let mut execution_result = oracle.build_execution_result(acvm_output, contract_address, 0);
1298        execution_result.entrypoint.call_context = aztec_core::kernel_types::CallContext {
1299            msg_sender: origin,
1300            contract_address,
1301            function_selector: function
1302                .selector
1303                .expect("private function selector")
1304                .to_field(),
1305            is_static_call: function.is_static,
1306        };
1307
1308        // Extract circuit-constrained side effects from PrivateCircuitPublicInputs
1309        // in the solved witness. These are NOT emitted through oracle calls.
1310        {
1311            let ctx_size = artifact.private_context_inputs_size(&function_name);
1312            let user_args_size = call.encoded_args.len();
1313            let params_size = ctx_size + user_args_size;
1314            let expiration_timestamp = Self::extract_expiration_timestamp_from_witness(
1315                &execution_result.entrypoint.partial_witness,
1316                params_size,
1317                ctx_size,
1318            );
1319            execution_result.expiration_timestamp = expiration_timestamp;
1320
1321            let (circuit_note_hashes, _circuit_nullifiers, circuit_logs) =
1322                Self::extract_side_effects_from_witness(
1323                    &execution_result.entrypoint.partial_witness,
1324                    params_size,
1325                    contract_address,
1326                );
1327            // Note hashes may come from oracle calls (notifyCreatedNote)
1328            // OR from the PCPI witness. Only add PCPI note hashes if the
1329            // oracle didn't produce any, to avoid duplicates.
1330            if execution_result.entrypoint.note_hashes.is_empty() && !circuit_note_hashes.is_empty()
1331            {
1332                execution_result
1333                    .entrypoint
1334                    .note_hashes
1335                    .extend(circuit_note_hashes);
1336            }
1337            // Private logs are always circuit-constrained (never from oracle).
1338            if !circuit_logs.is_empty() {
1339                execution_result
1340                    .entrypoint
1341                    .private_logs
1342                    .extend(circuit_logs);
1343            }
1344        }
1345
1346        self.persist_pending_notes(&execution_result, scopes)
1347            .await?;
1348
1349        let contract_class_log_fields = execution_result
1350            .all_contract_class_logs_sorted()
1351            .iter()
1352            .map(|ccl| {
1353                aztec_core::tx::ContractClassLogFields::from_emitted_fields(ccl.log.fields.clone())
1354            })
1355            .collect::<Vec<_>>();
1356        let public_function_calldata = execution_result.public_function_calldata.clone();
1357
1358        let simulated_return_values = if !acir_call_returns.is_empty() {
1359            acir_call_returns.clone()
1360        } else {
1361            execution_result.entrypoint.return_values.clone()
1362        };
1363
1364        Ok(CallExecutionBundle {
1365            execution_result,
1366            contract_class_log_fields,
1367            public_function_calldata,
1368            first_acir_call_return_values: acir_call_returns,
1369            simulated_return_values,
1370        })
1371    }
1372
1373    /// Extract the origin (msg_sender) from a TxExecutionRequest.
1374    /// Extract all circuit-constrained side effects from the solved ACVM
1375    /// witness (`PrivateCircuitPublicInputs`, 870 fields starting at
1376    /// witness index `params_size`).
1377    ///
1378    /// Returns (note_hashes, nullifiers, private_logs).
1379    fn extract_side_effects_from_witness(
1380        witness: &acir::native_types::WitnessMap<acir::FieldElement>,
1381        params_size: usize,
1382        contract_address: AztecAddress,
1383    ) -> (
1384        Vec<aztec_core::kernel_types::ScopedNoteHash>,
1385        Vec<aztec_core::kernel_types::ScopedNullifier>,
1386        Vec<crate::execution::PrivateLogData>,
1387    ) {
1388        use aztec_core::kernel_types::{NoteHash, Nullifier, ScopedNoteHash, ScopedNullifier};
1389
1390        const PCPI_LENGTH: usize = 870;
1391        const NOTE_HASHES_OFFSET: usize = 454;
1392        const NOTE_HASH_LEN: usize = 2;
1393        const MAX_NOTE_HASHES: usize = 16;
1394        const NOTE_HASHES_ARRAY_LEN: usize = MAX_NOTE_HASHES * NOTE_HASH_LEN + 1;
1395        const NULLIFIERS_OFFSET: usize = 487;
1396        const NULLIFIER_LEN: usize = 3;
1397        const MAX_NULLIFIERS: usize = 16;
1398        const NULLIFIERS_ARRAY_LEN: usize = MAX_NULLIFIERS * NULLIFIER_LEN + 1;
1399        const PRIVATE_LOGS_OFFSET: usize = 561;
1400        const PRIVATE_LOG_DATA_LEN: usize = 19;
1401        const PRIVATE_LOG_FIELDS: usize = 16;
1402        const MAX_LOGS: usize = 16;
1403        const PRIVATE_LOGS_ARRAY_LEN: usize = MAX_LOGS * PRIVATE_LOG_DATA_LEN + 1;
1404
1405        let pcpi_start = params_size;
1406        let mut pcpi = Vec::with_capacity(PCPI_LENGTH);
1407        for i in 0..PCPI_LENGTH {
1408            let idx = acir::native_types::Witness((pcpi_start + i) as u32);
1409            let val = witness
1410                .get(&idx)
1411                .map(|fe| crate::execution::field_conversion::fe_to_fr(fe))
1412                .unwrap_or_else(Fr::zero);
1413            pcpi.push(val);
1414        }
1415
1416        // Extract note hashes
1417        let nh_slice = &pcpi[NOTE_HASHES_OFFSET..][..NOTE_HASHES_ARRAY_LEN];
1418        let nh_count = nh_slice[NOTE_HASHES_ARRAY_LEN - 1]
1419            .to_usize()
1420            .min(MAX_NOTE_HASHES);
1421        let mut note_hashes = Vec::with_capacity(nh_count);
1422        for i in 0..nh_count {
1423            let base = i * NOTE_HASH_LEN;
1424            let value = nh_slice[base];
1425            let counter = nh_slice[base + 1].to_usize() as u32;
1426            if value != Fr::zero() {
1427                note_hashes.push(ScopedNoteHash {
1428                    note_hash: NoteHash { value, counter },
1429                    contract_address,
1430                });
1431            }
1432        }
1433
1434        // Extract nullifiers
1435        let null_slice = &pcpi[NULLIFIERS_OFFSET..][..NULLIFIERS_ARRAY_LEN];
1436        let null_count = null_slice[NULLIFIERS_ARRAY_LEN - 1]
1437            .to_usize()
1438            .min(MAX_NULLIFIERS);
1439        let mut nullifiers = Vec::with_capacity(null_count);
1440        for i in 0..null_count {
1441            let base = i * NULLIFIER_LEN;
1442            let value = null_slice[base];
1443            let note_hash = null_slice[base + 1];
1444            let counter = null_slice[base + 2].to_usize() as u32;
1445            if value != Fr::zero() {
1446                nullifiers.push(ScopedNullifier {
1447                    nullifier: Nullifier {
1448                        value,
1449                        note_hash,
1450                        counter,
1451                    },
1452                    contract_address,
1453                });
1454            }
1455        }
1456
1457        // Extract private logs
1458        let logs_slice = &pcpi[PRIVATE_LOGS_OFFSET..][..PRIVATE_LOGS_ARRAY_LEN];
1459        let log_count = logs_slice[PRIVATE_LOGS_ARRAY_LEN - 1]
1460            .to_usize()
1461            .min(MAX_LOGS);
1462        let mut logs = Vec::with_capacity(log_count);
1463        for i in 0..log_count {
1464            let base = i * PRIVATE_LOG_DATA_LEN;
1465            let fields: Vec<Fr> = logs_slice[base..base + PRIVATE_LOG_FIELDS].to_vec();
1466            let emitted_length = logs_slice[base + PRIVATE_LOG_FIELDS].to_usize() as u32;
1467            let note_hash_counter = logs_slice[base + PRIVATE_LOG_FIELDS + 1].to_usize() as u32;
1468            let counter = logs_slice[base + PRIVATE_LOG_DATA_LEN - 1].to_usize() as u32;
1469            if emitted_length > 0 {
1470                logs.push(crate::execution::PrivateLogData {
1471                    fields,
1472                    emitted_length,
1473                    note_hash_counter,
1474                    counter,
1475                    contract_address,
1476                });
1477            }
1478        }
1479
1480        (note_hashes, nullifiers, logs)
1481    }
1482
1483    fn extract_expiration_timestamp_from_witness(
1484        witness: &acir::native_types::WitnessMap<acir::FieldElement>,
1485        params_size: usize,
1486        context_inputs_size: usize,
1487    ) -> u64 {
1488        let prefix_len = context_inputs_size.saturating_sub(5);
1489        let expiration_offset = prefix_len + 8;
1490        let idx = acir::native_types::Witness((params_size + expiration_offset) as u32);
1491        witness
1492            .get(&idx)
1493            .map(crate::execution::field_conversion::fe_to_fr)
1494            .map(|fr| fr.to_usize() as u64)
1495            .unwrap_or(0)
1496    }
1497
1498    fn extract_origin(&self, tx_request: &TxExecutionRequest) -> AztecAddress {
1499        tx_request
1500            .data
1501            .get("origin")
1502            .and_then(|v| v.as_str())
1503            .and_then(|s| Fr::from_hex(s).ok())
1504            .map(AztecAddress)
1505            .unwrap_or(AztecAddress(Fr::zero()))
1506    }
1507
1508    /// Verify auth witness Schnorr signatures against the origin account's
1509    /// stored signing public key.
1510    ///
1511    /// The Noir account contract's entrypoint verifies auth witnesses via
1512    /// circuit constraints, but our oracle-based execution skips those checks.
1513    /// This method replicates the verification so that `simulate_tx` (and
1514    /// `prove_tx`) reject transactions signed with an incorrect key.
1515    async fn verify_auth_witness_signatures(
1516        &self,
1517        tx_request: &TxExecutionRequest,
1518        origin: AztecAddress,
1519    ) -> Result<(), Error> {
1520        let auth_witnesses: Vec<aztec_core::tx::AuthWitness> = tx_request
1521            .data
1522            .get("authWitnesses")
1523            .and_then(|v| serde_json::from_value(v.clone()).ok())
1524            .unwrap_or_default();
1525
1526        if auth_witnesses.is_empty() {
1527            return Ok(());
1528        }
1529
1530        // Look up the signing public key note for the origin account.
1531        // Schnorr account contracts store the signing key at storage slot 1
1532        // as a two-field note [pk.x, pk.y].
1533        let signing_key_notes = self
1534            .note_store
1535            .get_notes_by_slot(&origin, &Fr::from(1u64))
1536            .await
1537            .unwrap_or_default();
1538
1539        let signing_pk = signing_key_notes.iter().find_map(|note| {
1540            if note.note_data.len() >= 2 && !note.nullified {
1541                Some(Point {
1542                    x: note.note_data[0],
1543                    y: note.note_data[1],
1544                    is_infinite: false,
1545                })
1546            } else {
1547                None
1548            }
1549        });
1550
1551        let Some(pk) = signing_pk else {
1552            // No signing key note found — skip verification (the account may
1553            // use a non-Schnorr scheme or the note hasn't been synced).
1554            return Ok(());
1555        };
1556
1557        for aw in &auth_witnesses {
1558            // Schnorr signatures are exactly 64 fields (one per byte).
1559            if aw.fields.len() != 64 {
1560                continue;
1561            }
1562
1563            let sig_bytes: Vec<u8> = aw.fields.iter().map(|f| f.to_usize() as u8).collect();
1564            let mut sig_arr = [0u8; 64];
1565            sig_arr.copy_from_slice(&sig_bytes);
1566            let sig = SchnorrSignature::from_bytes(&sig_arr);
1567
1568            if !schnorr_verify(&pk, &aw.request_hash, &sig) {
1569                return Err(Error::InvalidData(
1570                    "Cannot satisfy constraint: auth witness signature verification failed".into(),
1571                ));
1572            }
1573        }
1574
1575        Ok(())
1576    }
1577
1578    /// Build `TxConstantData` from the anchor block header and tx request.
1579    fn build_tx_constant_data(
1580        anchor: &AnchorBlockHeader,
1581        tx_request: &TxExecutionRequest,
1582        vk_tree_root: Fr,
1583        protocol_contracts_hash: Fr,
1584    ) -> aztec_core::kernel_types::TxConstantData {
1585        let h = &anchor.data;
1586
1587        // Helper to extract an Fr from a JSON path (hex string or integer).
1588        let fr_at = |val: &serde_json::Value, path: &str| -> Fr {
1589            let v = val.pointer(path);
1590            match v {
1591                Some(serde_json::Value::String(s)) => Fr::from_hex(s).unwrap_or(Fr::zero()),
1592                Some(serde_json::Value::Number(n)) => Fr::from(n.as_u64().unwrap_or(0)),
1593                _ => Fr::zero(),
1594            }
1595        };
1596        // Parse a string as hex (0x-prefixed) or decimal.
1597        let parse_u64_str = |s: &str| -> u64 {
1598            if let Some(hex) = s.strip_prefix("0x") {
1599                u64::from_str_radix(hex, 16).unwrap_or(0)
1600            } else {
1601                s.parse::<u64>().unwrap_or(0)
1602            }
1603        };
1604        let parse_u128_str = |s: &str| -> u128 {
1605            if let Some(hex) = s.strip_prefix("0x") {
1606                u128::from_str_radix(hex, 16).unwrap_or(0)
1607            } else {
1608                s.parse::<u128>().unwrap_or(0)
1609            }
1610        };
1611        let u32_at = |val: &serde_json::Value, path: &str| -> u32 {
1612            let v = val.pointer(path);
1613            match v {
1614                Some(serde_json::Value::Number(n)) => n.as_u64().unwrap_or(0) as u32,
1615                Some(serde_json::Value::String(s)) => parse_u64_str(s) as u32,
1616                _ => 0,
1617            }
1618        };
1619        let u64_at = |val: &serde_json::Value, path: &str| -> u64 {
1620            let v = val.pointer(path);
1621            match v {
1622                Some(serde_json::Value::Number(n)) => n.as_u64().unwrap_or(0),
1623                Some(serde_json::Value::String(s)) => parse_u64_str(s),
1624                _ => 0,
1625            }
1626        };
1627        let u128_at = |val: &serde_json::Value, path: &str| -> u128 {
1628            let v = val.pointer(path);
1629            match v {
1630                Some(serde_json::Value::Number(n)) => n.as_u64().unwrap_or(0) as u128,
1631                Some(serde_json::Value::String(s)) => parse_u128_str(s),
1632                _ => 0,
1633            }
1634        };
1635        let eth_at = |val: &serde_json::Value, path: &str| -> aztec_core::types::EthAddress {
1636            // Parse EthAddress from hex string — use Fr conversion which zero-pads correctly
1637            match val.pointer(path).and_then(|v| v.as_str()) {
1638                Some(s) => {
1639                    // Parse as Fr, then extract the low 20 bytes
1640                    let fr = Fr::from_hex(s).unwrap_or(Fr::zero());
1641                    let bytes = fr.to_be_bytes();
1642                    let mut addr = [0u8; 20];
1643                    addr.copy_from_slice(&bytes[12..32]);
1644                    aztec_core::types::EthAddress(addr)
1645                }
1646                None => aztec_core::types::EthAddress::default(),
1647            }
1648        };
1649
1650        let snap = |val: &serde_json::Value,
1651                    prefix: &str|
1652         -> aztec_core::kernel_types::AppendOnlyTreeSnapshot {
1653            aztec_core::kernel_types::AppendOnlyTreeSnapshot {
1654                root: fr_at(val, &format!("{prefix}/root")),
1655                next_available_leaf_index: u32_at(val, &format!("{prefix}/nextAvailableLeafIndex")),
1656            }
1657        };
1658
1659        let block_header = aztec_core::kernel_types::BlockHeader {
1660            last_archive: snap(h, "/lastArchive"),
1661            state: aztec_core::kernel_types::StateReference {
1662                l1_to_l2_message_tree: snap(h, "/state/l1ToL2MessageTree"),
1663                partial: aztec_core::kernel_types::PartialStateReference {
1664                    note_hash_tree: snap(h, "/state/partial/noteHashTree"),
1665                    nullifier_tree: snap(h, "/state/partial/nullifierTree"),
1666                    public_data_tree: snap(h, "/state/partial/publicDataTree"),
1667                },
1668            },
1669            sponge_blob_hash: fr_at(h, "/spongeBlobHash"),
1670            global_variables: aztec_core::kernel_types::GlobalVariables {
1671                chain_id: fr_at(h, "/globalVariables/chainId"),
1672                version: fr_at(h, "/globalVariables/version"),
1673                block_number: u64_at(h, "/globalVariables/blockNumber"),
1674                slot_number: u64_at(h, "/globalVariables/slotNumber"),
1675                timestamp: u64_at(h, "/globalVariables/timestamp"),
1676                coinbase: eth_at(h, "/globalVariables/coinbase"),
1677                fee_recipient: AztecAddress(fr_at(h, "/globalVariables/feeRecipient")),
1678                gas_fees: aztec_core::fee::GasFees {
1679                    fee_per_da_gas: u128_at(h, "/globalVariables/gasFees/feePerDaGas"),
1680                    fee_per_l2_gas: u128_at(h, "/globalVariables/gasFees/feePerL2Gas"),
1681                },
1682            },
1683            total_fees: fr_at(h, "/totalFees"),
1684            total_mana_used: fr_at(h, "/totalManaUsed"),
1685        };
1686
1687        // Extract tx context from the request
1688        let req = &tx_request.data;
1689        let tx_context = aztec_core::kernel_types::TxContext {
1690            chain_id: fr_at(req, "/txContext/chainId"),
1691            version: fr_at(req, "/txContext/version"),
1692            gas_settings: aztec_core::fee::GasSettings {
1693                gas_limits: Some(aztec_core::fee::Gas {
1694                    da_gas: u64_at(req, "/txContext/gasSettings/gasLimits/daGas"),
1695                    l2_gas: u64_at(req, "/txContext/gasSettings/gasLimits/l2Gas"),
1696                }),
1697                teardown_gas_limits: Some(aztec_core::fee::Gas {
1698                    da_gas: u64_at(req, "/txContext/gasSettings/teardownGasLimits/daGas"),
1699                    l2_gas: u64_at(req, "/txContext/gasSettings/teardownGasLimits/l2Gas"),
1700                }),
1701                max_fee_per_gas: Some(aztec_core::fee::GasFees {
1702                    fee_per_da_gas: u128_at(req, "/txContext/gasSettings/maxFeePerGas/feePerDaGas"),
1703                    fee_per_l2_gas: u128_at(req, "/txContext/gasSettings/maxFeePerGas/feePerL2Gas"),
1704                }),
1705                max_priority_fee_per_gas: Some(aztec_core::fee::GasFees {
1706                    fee_per_da_gas: u128_at(
1707                        req,
1708                        "/txContext/gasSettings/maxPriorityFeePerGas/feePerDaGas",
1709                    ),
1710                    fee_per_l2_gas: u128_at(
1711                        req,
1712                        "/txContext/gasSettings/maxPriorityFeePerGas/feePerL2Gas",
1713                    ),
1714                }),
1715            },
1716        };
1717
1718        aztec_core::kernel_types::TxConstantData {
1719            anchor_block_header: block_header,
1720            tx_context,
1721            vk_tree_root,
1722            protocol_contracts_hash,
1723        }
1724    }
1725
1726    /// Compute the expiration timestamp from the anchor block header.
1727    fn compute_expiration(anchor: &AnchorBlockHeader) -> u64 {
1728        let timestamp = anchor
1729            .data
1730            .pointer("/globalVariables/timestamp")
1731            .and_then(|v| {
1732                v.as_u64().or_else(|| {
1733                    v.as_str().and_then(|s| {
1734                        if let Some(hex) = s.strip_prefix("0x") {
1735                            u64::from_str_radix(hex, 16).ok()
1736                        } else {
1737                            s.parse::<u64>().ok()
1738                        }
1739                    })
1740                })
1741            })
1742            .unwrap_or(0);
1743        timestamp + aztec_core::constants::MAX_TX_LIFETIME
1744    }
1745
1746    /// Ensure the tx's max_fee_per_gas is at least the current network gas price.
1747    ///
1748    /// Reads the current gas fees from the anchor block header and applies a 1.5x
1749    /// safety multiplier. If the tx's fee cap is below this, it is raised.
1750    fn ensure_min_fees(
1751        tx_constants: &mut aztec_core::kernel_types::TxConstantData,
1752        anchor: &AnchorBlockHeader,
1753    ) {
1754        let parse_u128 = |val: &serde_json::Value| -> u128 {
1755            if let Some(s) = val.as_str() {
1756                let s = s.strip_prefix("0x").unwrap_or(s);
1757                u128::from_str_radix(
1758                    s,
1759                    if val.as_str().unwrap_or("").starts_with("0x") {
1760                        16
1761                    } else {
1762                        10
1763                    },
1764                )
1765                .unwrap_or(0)
1766            } else {
1767                val.as_u64().unwrap_or(0) as u128
1768            }
1769        };
1770
1771        let block_da_fee = anchor
1772            .data
1773            .pointer("/globalVariables/gasFees/feePerDaGas")
1774            .map(|v| parse_u128(v))
1775            .unwrap_or(0);
1776        let block_l2_fee = anchor
1777            .data
1778            .pointer("/globalVariables/gasFees/feePerL2Gas")
1779            .map(|v| parse_u128(v))
1780            .unwrap_or(0);
1781
1782        // Apply 1.5x safety margin
1783        let min_da = block_da_fee + block_da_fee / 2;
1784        let min_l2 = block_l2_fee + block_l2_fee / 2;
1785
1786        if let Some(ref mut fees) = tx_constants.tx_context.gas_settings.max_fee_per_gas {
1787            if fees.fee_per_da_gas < min_da {
1788                fees.fee_per_da_gas = min_da;
1789            }
1790            if fees.fee_per_l2_gas < min_l2 {
1791                fees.fee_per_l2_gas = min_l2;
1792            }
1793        } else {
1794            tx_constants.tx_context.gas_settings.max_fee_per_gas = Some(aztec_core::fee::GasFees {
1795                fee_per_da_gas: min_da,
1796                fee_per_l2_gas: min_l2,
1797            });
1798        }
1799    }
1800
1801    /// Build a synthetic execution result for a public function call.
1802    ///
1803    /// Run the account contract's entrypoint function through ACVM.
1804    ///
1805    /// This mirrors the TS SDK flow where the entire entrypoint Noir circuit is
1806    /// executed. The circuit handles both private (via nested ACVM calls) and
1807    /// public (via `enqueuePublicFunctionCall` oracle) calls. This produces
1808    /// correct `PublicCallRequest` data that the node can process.
1809    async fn execute_entrypoint_via_acvm(
1810        &self,
1811        tx_request: &TxExecutionRequest,
1812        origin: AztecAddress,
1813        protocol_nullifier: Fr,
1814        anchor: &AnchorBlockHeader,
1815        scopes: &[AztecAddress],
1816    ) -> Result<CallExecutionBundle, Error> {
1817        let request: DecodedTxExecutionRequest = serde_json::from_value(tx_request.data.clone())?;
1818
1819        // Find the account contract artifact
1820        let contract_instance = self.contract_store.get_instance(&origin).await?;
1821        let class_id = contract_instance
1822            .as_ref()
1823            .map(|i| i.inner.current_contract_class_id)
1824            .ok_or_else(|| Error::InvalidData(format!("account contract not found: {origin}")))?;
1825        let artifact = self
1826            .contract_store
1827            .get_artifact(&class_id)
1828            .await?
1829            .ok_or_else(|| {
1830                Error::InvalidData(format!(
1831                    "account contract artifact not found for class {class_id}"
1832                ))
1833            })?;
1834
1835        // Find the entrypoint function — look up from the tx request's selector
1836        let entrypoint_args = request
1837            .args_of_calls
1838            .iter()
1839            .find(|hv| hv.hash == request.first_call_args_hash)
1840            .ok_or_else(|| {
1841                Error::InvalidData("firstCallArgsHash not found in argsOfCalls".into())
1842            })?;
1843
1844        let function_name = {
1845            let selector_field = tx_request
1846                .data
1847                .get("functionSelector")
1848                .and_then(|v| v.as_str())
1849                .and_then(|s| aztec_core::abi::FunctionSelector::from_hex(s).ok());
1850            if let Some(sel) = selector_field {
1851                self.resolve_function_name_by_selector(&origin, sel).await
1852            } else {
1853                // Default to "entrypoint" for account contracts
1854                "entrypoint".to_owned()
1855            }
1856        };
1857
1858        let function = artifact.find_function(&function_name)?;
1859
1860        // Build the ACVM witness for the entrypoint
1861        let full_witness = self.build_private_witness(
1862            &artifact,
1863            &function_name,
1864            &entrypoint_args.values,
1865            origin, // contract_address = account address
1866            origin, // msg_sender = self-call
1867            tx_request,
1868            anchor,
1869            function
1870                .selector
1871                .unwrap_or_else(aztec_core::abi::FunctionSelector::empty),
1872            function.is_static,
1873        );
1874
1875        // Create the oracle with pre-seeded execution cache
1876        let mut oracle = crate::execution::PrivateExecutionOracle::new(
1877            &self.node,
1878            &self.contract_store,
1879            &self.key_store,
1880            &self.note_store,
1881            &self.capsule_store,
1882            &self.address_store,
1883            &self.sender_tagging_store,
1884            anchor.data.clone(),
1885            origin,
1886            protocol_nullifier,
1887            Some(origin),
1888            scopes.to_vec(),
1889            false,
1890        );
1891
1892        // Pre-populate execution cache with all hashed values from the tx request
1893        // (mirrors TS: HashedValuesCache.create(request.argsOfCalls))
1894        oracle.seed_execution_cache(&request.args_of_calls);
1895
1896        // Set auth witnesses from the tx request
1897        if let Some(auth_witnesses) = tx_request.data.get("authWitnesses").and_then(|v| {
1898            serde_json::from_value::<Vec<aztec_core::tx::AuthWitness>>(v.clone()).ok()
1899        }) {
1900            let pairs: Vec<(Fr, Vec<Fr>)> = auth_witnesses
1901                .iter()
1902                .map(|aw| (aw.request_hash, aw.fields.clone()))
1903                .collect();
1904            oracle.set_auth_witnesses(pairs);
1905        }
1906
1907        // Seed capsules from the tx request so protocol contract handlers
1908        // (e.g., contract class registerer) can access bytecode data.
1909        if let Some(capsules) = tx_request
1910            .data
1911            .get("capsules")
1912            .cloned()
1913            .map(serde_json::from_value::<Vec<aztec_core::tx::Capsule>>)
1914            .transpose()?
1915        {
1916            oracle.set_capsules(capsules);
1917        }
1918
1919        // Store block-header + tx-context for nested calls
1920        let context_inputs_size = artifact.private_context_inputs_size(&function_name);
1921        if context_inputs_size > 5 {
1922            oracle.context_witness_prefix =
1923                full_witness[4..context_inputs_size.saturating_sub(1)].to_vec();
1924        }
1925
1926        // Execute the entrypoint via ACVM
1927        let acvm_output = crate::execution::AcvmExecutor::execute_private(
1928            &artifact,
1929            &function_name,
1930            &full_witness,
1931            &mut oracle,
1932        )
1933        .await?;
1934
1935        // Extract public call requests and calldata from oracle
1936        let public_call_requests = oracle.take_public_call_requests();
1937        let public_function_calldata = oracle.take_public_function_calldata();
1938        let teardown_request = oracle.take_teardown_call_request();
1939
1940        // Build the execution result from oracle state
1941        let entrypoint_result = crate::execution::execution_result::PrivateCallExecutionResult {
1942            contract_address: origin,
1943            call_context: aztec_core::kernel_types::CallContext {
1944                msg_sender: origin,
1945                contract_address: origin,
1946                function_selector: function
1947                    .selector
1948                    .map(|s| s.to_field())
1949                    .unwrap_or(Fr::zero()),
1950                is_static_call: false,
1951            },
1952            acir: acvm_output.acir_bytecode.clone(),
1953            vk: Vec::new(),
1954            partial_witness: acvm_output.witness.clone(),
1955            return_values: acvm_output.return_values.clone(),
1956            new_notes: oracle.new_notes.clone(),
1957            note_hash_nullifier_counter_map: oracle.note_hash_nullifier_counter_map.clone(),
1958            offchain_effects: Vec::new(),
1959            pre_tags: Vec::new(),
1960            note_hashes: oracle.note_hashes.clone(),
1961            nullifiers: oracle.nullifiers.clone(),
1962            private_logs: oracle.private_logs.clone(),
1963            contract_class_logs: oracle.contract_class_logs.clone(),
1964            public_call_requests,
1965            public_teardown_call_request: teardown_request,
1966            start_side_effect_counter: 2,
1967            end_side_effect_counter: oracle.side_effect_counter,
1968            min_revertible_side_effect_counter: oracle.min_revertible_side_effect_counter,
1969            note_hash_read_requests: oracle.note_hash_read_requests.clone(),
1970            nullifier_read_requests: oracle.nullifier_read_requests.clone(),
1971            nested_execution_results: oracle.nested_results.clone(),
1972        };
1973
1974        let contract_class_log_fields = entrypoint_result
1975            .contract_class_logs
1976            .iter()
1977            .map(|log| {
1978                aztec_core::tx::ContractClassLogFields::from_emitted_fields(log.log.fields.clone())
1979            })
1980            .collect();
1981        let expiration_timestamp = Self::extract_expiration_timestamp_from_witness(
1982            &acvm_output.witness,
1983            full_witness.len(),
1984            context_inputs_size,
1985        );
1986
1987        Ok(CallExecutionBundle {
1988            simulated_return_values: if !acvm_output.first_acir_call_return_values.is_empty() {
1989                acvm_output.first_acir_call_return_values.clone()
1990            } else {
1991                acvm_output.return_values.clone()
1992            },
1993            first_acir_call_return_values: acvm_output.first_acir_call_return_values,
1994            execution_result: crate::execution::execution_result::PrivateExecutionResult {
1995                entrypoint: entrypoint_result,
1996                first_nullifier: protocol_nullifier,
1997                expiration_timestamp,
1998                public_function_calldata: public_function_calldata.clone(),
1999            },
2000            contract_class_log_fields,
2001            public_function_calldata,
2002        })
2003    }
2004
2005    /// Public functions are executed by the sequencer, not the PXE. This method
2006    /// creates a minimal `PrivateExecutionResult` that enqueues the public call
2007    /// so the simulated kernel can package it correctly.
2008    fn build_public_call_execution(
2009        artifact: &aztec_core::abi::ContractArtifact,
2010        function_name: &str,
2011        encoded_args: &[Fr],
2012        contract_address: AztecAddress,
2013        origin: AztecAddress,
2014        first_nullifier: Fr,
2015        hide_msg_sender: bool,
2016        is_static_call: bool,
2017    ) -> Result<
2018        (
2019            crate::execution::execution_result::PrivateExecutionResult,
2020            Vec<aztec_core::tx::ContractClassLogFields>,
2021            Vec<aztec_core::tx::HashedValues>,
2022        ),
2023        Error,
2024    > {
2025        use crate::execution::execution_result::{
2026            PrivateCallExecutionResult, PrivateExecutionResult, PublicCallRequestData,
2027        };
2028
2029        let args = encoded_args.to_vec();
2030
2031        // Build calldata: function selector + args
2032        let func = artifact.find_function(function_name)?;
2033        let selector_fr: Fr = func
2034            .selector
2035            .ok_or_else(|| Error::InvalidData("public function has no selector".into()))?
2036            .into();
2037        let mut calldata = vec![selector_fr];
2038        calldata.extend_from_slice(&args);
2039        let hashed = aztec_core::tx::HashedValues::from_calldata(calldata);
2040        let calldata_hash = hashed.hash();
2041        let msg_sender = if hide_msg_sender {
2042            AztecAddress::zero()
2043        } else {
2044            origin
2045        };
2046
2047        let entrypoint = PrivateCallExecutionResult {
2048            contract_address: origin,
2049            public_call_requests: vec![PublicCallRequestData {
2050                contract_address,
2051                msg_sender,
2052                is_static_call,
2053                calldata_hash,
2054                counter: 2,
2055            }],
2056            start_side_effect_counter: 2,
2057            min_revertible_side_effect_counter: 2,
2058            end_side_effect_counter: 3,
2059            ..Default::default()
2060        };
2061
2062        let exec_result = PrivateExecutionResult {
2063            entrypoint,
2064            first_nullifier,
2065            expiration_timestamp: 0,
2066            public_function_calldata: vec![hashed.clone()],
2067        };
2068
2069        Ok((exec_result, vec![], vec![hashed]))
2070    }
2071
2072    async fn is_registered_account(&self, address: &AztecAddress) -> Result<bool, Error> {
2073        let Some(complete) = self.address_store.get(address).await? else {
2074            return Ok(false);
2075        };
2076        let accounts = self.key_store.get_accounts().await?;
2077        Ok(accounts.contains(&complete.public_keys.hash()))
2078    }
2079}
2080
2081fn public_function_signatures(artifact: &ContractArtifact) -> Vec<String> {
2082    artifact
2083        .functions
2084        .iter()
2085        .filter(|function| function.function_type == FunctionType::Public)
2086        .map(|function| {
2087            let params = function
2088                .parameters
2089                .iter()
2090                .map(|param| abi_type_signature(&param.typ))
2091                .collect::<Vec<_>>()
2092                .join(",");
2093            format!("{}({params})", function.name)
2094        })
2095        .collect()
2096}
2097
2098#[async_trait]
2099impl<N: AztecNode + Clone + 'static> Pxe for EmbeddedPxe<N> {
2100    async fn get_synced_block_header(&self) -> Result<BlockHeader, Error> {
2101        let anchor = self.get_anchor_block_header().await?;
2102        Ok(BlockHeader { data: anchor.data })
2103    }
2104
2105    async fn get_contract_instance(
2106        &self,
2107        address: &AztecAddress,
2108    ) -> Result<Option<ContractInstanceWithAddress>, Error> {
2109        // Check local store first
2110        if let Some(inst) = self.contract_store.get_instance(address).await? {
2111            return Ok(Some(inst));
2112        }
2113        // Fall through to node
2114        self.node.get_contract(address).await
2115    }
2116
2117    async fn get_contract_artifact(&self, id: &Fr) -> Result<Option<ContractArtifact>, Error> {
2118        self.contract_store.get_artifact(id).await
2119    }
2120
2121    async fn get_contracts(&self) -> Result<Vec<AztecAddress>, Error> {
2122        self.contract_store.get_contract_addresses().await
2123    }
2124
2125    async fn register_account(
2126        &self,
2127        secret_key: &Fr,
2128        partial_address: &Fr,
2129    ) -> Result<CompleteAddress, Error> {
2130        tracing::debug!("registering account");
2131
2132        // Derive keys and store in key store
2133        let _derived = self.key_store.add_account(secret_key).await?;
2134
2135        // Derive complete address
2136        let complete =
2137            complete_address_from_secret_key_and_partial_address(secret_key, partial_address)?;
2138
2139        // Store in address store
2140        self.address_store.add(&complete).await?;
2141
2142        tracing::debug!(address = %complete.address, "account registered");
2143        Ok(complete)
2144    }
2145
2146    async fn get_registered_accounts(&self) -> Result<Vec<CompleteAddress>, Error> {
2147        let accounts = self.key_store.get_accounts().await?;
2148        let complete_addresses = self.address_store.get_all().await?;
2149        Ok(complete_addresses
2150            .into_iter()
2151            .filter(|complete| accounts.contains(&complete.public_keys.hash()))
2152            .collect())
2153    }
2154
2155    async fn register_sender(&self, sender: &AztecAddress) -> Result<AztecAddress, Error> {
2156        if self.is_registered_account(sender).await? {
2157            return Ok(*sender);
2158        }
2159        self.sender_store.add(sender).await?;
2160        Ok(*sender)
2161    }
2162
2163    async fn get_senders(&self) -> Result<Vec<AztecAddress>, Error> {
2164        self.sender_store.get_all().await
2165    }
2166
2167    async fn remove_sender(&self, sender: &AztecAddress) -> Result<(), Error> {
2168        self.sender_store.remove(sender).await
2169    }
2170
2171    async fn register_contract_class(&self, artifact: &ContractArtifact) -> Result<(), Error> {
2172        tracing::debug!(name = %artifact.name, "registering contract class");
2173        self.contract_store.add_class(artifact).await?;
2174        Ok(())
2175    }
2176
2177    async fn register_contract(&self, request: RegisterContractRequest) -> Result<(), Error> {
2178        tracing::debug!(address = %request.instance.address, "registering contract");
2179
2180        if let Some(ref artifact) = request.artifact {
2181            let computed_class_id = compute_contract_class_id_from_artifact(artifact)?;
2182            if computed_class_id != request.instance.inner.current_contract_class_id {
2183                return Err(Error::InvalidData(format!(
2184                    "artifact class id {} does not match instance class id {}",
2185                    computed_class_id, request.instance.inner.current_contract_class_id
2186                )));
2187            }
2188
2189            let computed_address = compute_contract_address_from_instance(&request.instance.inner)?;
2190            if computed_address != request.instance.address {
2191                return Err(Error::InvalidData(format!(
2192                    "artifact instance address {} does not match computed contract address {}",
2193                    request.instance.address, computed_address
2194                )));
2195            }
2196
2197            let public_function_signatures = public_function_signatures(artifact);
2198            if !public_function_signatures.is_empty() {
2199                self.node
2200                    .register_contract_function_signatures(&public_function_signatures)
2201                    .await?;
2202            }
2203        } else if self
2204            .contract_store
2205            .get_artifact(&request.instance.inner.current_contract_class_id)
2206            .await?
2207            .is_none()
2208        {
2209            return Err(Error::InvalidData(format!(
2210                "artifact not found for contract class {}",
2211                request.instance.inner.current_contract_class_id
2212            )));
2213        }
2214
2215        // Store the instance
2216        self.contract_store.add_instance(&request.instance).await?;
2217
2218        // Store the artifact if provided
2219        if let Some(ref artifact) = request.artifact {
2220            self.contract_store
2221                .add_artifact(&request.instance.inner.current_contract_class_id, artifact)
2222                .await?;
2223        }
2224
2225        Ok(())
2226    }
2227
2228    async fn update_contract(
2229        &self,
2230        address: &AztecAddress,
2231        artifact: &ContractArtifact,
2232    ) -> Result<(), Error> {
2233        self.contract_store.update_artifact(address, artifact).await
2234    }
2235
2236    async fn simulate_tx(
2237        &self,
2238        tx_request: &TxExecutionRequest,
2239        opts: SimulateTxOpts,
2240    ) -> Result<TxSimulationResult, Error> {
2241        self.sync_block_state().await?;
2242
2243        let anchor = self.get_anchor_block_header().await?;
2244        let protocol_nullifier = compute_protocol_nullifier(&Self::tx_request_hash(tx_request)?);
2245        let request: DecodedTxExecutionRequest = serde_json::from_value(tx_request.data.clone())?;
2246        let origin = request.origin;
2247        let decoded_calls = Self::decode_entrypoint_calls(&request)?;
2248        let mut bundles = Vec::with_capacity(decoded_calls.len());
2249        for call in &decoded_calls {
2250            bundles.push(
2251                self.execute_entrypoint_call_bundle(
2252                    tx_request,
2253                    call,
2254                    origin,
2255                    protocol_nullifier,
2256                    &anchor,
2257                    &opts.scopes,
2258                )
2259                .await?,
2260            );
2261        }
2262        let bundled_private_return_values: Vec<Vec<String>> = bundles
2263            .iter()
2264            .map(|bundle| {
2265                bundle
2266                    .simulated_return_values
2267                    .iter()
2268                    .map(|f| f.to_string())
2269                    .collect()
2270            })
2271            .collect();
2272        let aggregated = Self::aggregate_call_bundles(origin, bundles);
2273
2274        // Verify auth witness signatures.
2275        //
2276        // The oracle-based private execution does not evaluate Noir circuit
2277        // constraints, so Schnorr/ECDSA signature checks inside the account
2278        // contract entrypoint are skipped.  We replicate that check here so
2279        // that `simulate_tx` rejects transactions signed with an invalid key,
2280        // matching the behavior of the upstream TS ACIR simulator.
2281        self.verify_auth_witness_signatures(tx_request, origin)
2282            .await?;
2283
2284        // Process through simulated kernel
2285        let kernel_output = crate::kernel::SimulatedKernel::process(
2286            &aggregated.execution_result,
2287            aztec_core::kernel_types::TxConstantData::default(), // TODO: fill from block header
2288            &origin.0,
2289            0, // TODO: fill expiration timestamp
2290        )?;
2291
2292        let pi = &kernel_output.public_inputs;
2293
2294        // Extract private return values — mirrors TS `getPrivateReturnValues()`.
2295        //
2296        // The aggregated execution tree has three levels:
2297        //   1. Synthetic root (aggregated entrypoint)
2298        //   2. Account contract entrypoint (first nested_execution_results)
2299        //   3. User's function call (first nested of the account entrypoint)
2300        //
2301        // TS equivalent: `simulatedTx.getPrivateReturnValues().nested[0].values`
2302        // which is `entrypoint.nestedExecutionResults[0].returnValues`.
2303        //
2304        // In the Rust aggregated tree the account entrypoint sits at level [0],
2305        // so the user's call is at [0][0].
2306        // Extract private return values — mirrors TS `getPrivateReturnValues()`.
2307        //
2308        // For private functions with databus returns, the main circuit's ACIR
2309        // return witnesses are the full PCPI — the user's actual return values
2310        // come from the first inner ACIR sub-circuit call, stored in
2311        // `first_acir_call_return_values`.
2312        //
2313        // Walk the execution tree first; fall back to the bundle-level ACIR
2314        // call return values if the tree has nothing.
2315        let private_return_values: Vec<String> = {
2316            let from_tree: Vec<String> = aggregated
2317                .execution_result
2318                .entrypoint
2319                .nested_execution_results
2320                .first()
2321                .and_then(|ep| {
2322                    // If the entrypoint has nested calls (account contract path),
2323                    // the user's function is the first nested call.
2324                    if !ep.nested_execution_results.is_empty() {
2325                        ep.nested_execution_results.first()
2326                    } else {
2327                        // Direct execution: the entrypoint IS the user's function.
2328                        Some(ep)
2329                    }
2330                })
2331                .map(|r| r.return_values.iter().map(|f| f.to_string()).collect())
2332                .unwrap_or_default();
2333
2334            if from_tree.is_empty() && !aggregated.first_acir_call_return_values.is_empty() {
2335                // Databus return path: use first ACIR sub-circuit return values.
2336                aggregated
2337                    .first_acir_call_return_values
2338                    .iter()
2339                    .map(|f| f.to_string())
2340                    .collect()
2341            } else {
2342                from_tree
2343            }
2344        };
2345
2346        let return_values = if bundled_private_return_values.len() > 1 {
2347            serde_json::Value::Array(
2348                bundled_private_return_values
2349                    .into_iter()
2350                    .map(serde_json::to_value)
2351                    .collect::<Result<Vec<_>, _>>()
2352                    .map_err(Error::from)?,
2353            )
2354        } else {
2355            serde_json::to_value(private_return_values).map_err(Error::from)?
2356        };
2357
2358        Ok(TxSimulationResult {
2359            data: serde_json::json!({
2360                "returnValues": return_values,
2361                "gasUsed": {
2362                    "daGas": pi.gas_used.da_gas,
2363                    "l2Gas": pi.gas_used.l2_gas,
2364                },
2365                "isForPublic": pi.is_for_public(),
2366            }),
2367        })
2368    }
2369
2370    async fn prove_tx(
2371        &self,
2372        tx_request: &TxExecutionRequest,
2373        scopes: Vec<AztecAddress>,
2374    ) -> Result<TxProvingResult, Error> {
2375        self.sync_block_state().await?;
2376
2377        let anchor = self.get_anchor_block_header().await?;
2378        let tx_req_hash = Self::tx_request_hash(tx_request)?;
2379        let protocol_nullifier = compute_protocol_nullifier(&tx_req_hash);
2380        let origin = self.extract_origin(tx_request);
2381        let mut tx_constants = Self::build_tx_constant_data(
2382            &anchor,
2383            tx_request,
2384            self.vk_tree_root,
2385            self.protocol_contracts_hash,
2386        );
2387        Self::ensure_min_fees(&mut tx_constants, &anchor);
2388        let request: DecodedTxExecutionRequest = serde_json::from_value(tx_request.data.clone())?;
2389        let fee_payer = request.fee_payer.unwrap_or(origin);
2390        let decoded_calls = Self::decode_entrypoint_calls(&request)?;
2391
2392        for call in &decoded_calls {
2393            for _scope in &scopes {
2394                let _ = self
2395                    .contract_sync_service
2396                    .ensure_contract_synced(
2397                        &call.to,
2398                        &scopes,
2399                        &anchor.block_hash,
2400                        |contract, scopes| async move {
2401                            self.execute_sync_state_for_contract(contract, scopes).await
2402                        },
2403                    )
2404                    .await;
2405            }
2406        }
2407
2408        // If any call is public, run the account entrypoint through ACVM
2409        // (like the TS SDK). The Noir entrypoint handles public calls via
2410        // `enqueuePublicFunctionCall` which the oracle captures correctly.
2411        // The decode-and-dispatch path only works for private-only payloads.
2412        //
2413        // We detect public calls from the parsed entrypoint payload (the
2414        // `is_public` flag in the encoded call fields).
2415        let has_public_calls = {
2416            let entrypoint_args = request
2417                .args_of_calls
2418                .iter()
2419                .find(|hv| hv.hash == request.first_call_args_hash);
2420            if let Some(ea) = entrypoint_args {
2421                parse_encoded_calls(&ea.values)
2422                    .map(|calls| calls.iter().any(|c| c.is_public))
2423                    .unwrap_or(false)
2424            } else {
2425                false
2426            }
2427        };
2428
2429        let aggregated = if has_public_calls {
2430            match self
2431                .execute_entrypoint_via_acvm(
2432                    tx_request,
2433                    origin,
2434                    protocol_nullifier,
2435                    &anchor,
2436                    &scopes,
2437                )
2438                .await
2439            {
2440                Ok(bundle) => bundle,
2441                Err(Error::InvalidData(msg))
2442                    if msg.contains("account contract not found")
2443                        || msg.contains("account contract artifact not found") =>
2444                {
2445                    let mut bundles = Vec::with_capacity(decoded_calls.len());
2446                    for call in &decoded_calls {
2447                        bundles.push(
2448                            self.execute_entrypoint_call_bundle(
2449                                tx_request,
2450                                call,
2451                                origin,
2452                                protocol_nullifier,
2453                                &anchor,
2454                                &scopes,
2455                            )
2456                            .await?,
2457                        );
2458                    }
2459                    Self::aggregate_call_bundles(origin, bundles)
2460                }
2461                Err(err) => return Err(err),
2462            }
2463        } else {
2464            let mut bundles = Vec::with_capacity(decoded_calls.len());
2465            for call in &decoded_calls {
2466                bundles.push(
2467                    self.execute_entrypoint_call_bundle(
2468                        tx_request,
2469                        call,
2470                        origin,
2471                        protocol_nullifier,
2472                        &anchor,
2473                        &scopes,
2474                    )
2475                    .await?,
2476                );
2477            }
2478            Self::aggregate_call_bundles(origin, bundles)
2479        };
2480        let expiration_timestamp = if aggregated.execution_result.expiration_timestamp != 0 {
2481            aggregated.execution_result.expiration_timestamp
2482        } else {
2483            Self::compute_expiration(&anchor)
2484        };
2485
2486        // Verify auth witness signatures (same check as simulate_tx).
2487        self.verify_auth_witness_signatures(tx_request, origin)
2488            .await?;
2489
2490        // Sort public_function_calldata to match the counter-sorted order
2491        // of public call requests (the simulated kernel sorts requests by
2492        // counter, so the calldata must follow the same order).
2493        let sorted_calldata = {
2494            let requests = aggregated.execution_result.all_public_call_requests();
2495            let calldata = &aggregated.public_function_calldata;
2496            if requests.len() == calldata.len() && !calldata.is_empty() {
2497                let mut paired: Vec<_> =
2498                    requests.into_iter().zip(calldata.iter().cloned()).collect();
2499                paired.sort_by_key(|(req, _)| req.counter);
2500                paired.into_iter().map(|(_, cd)| cd).collect()
2501            } else {
2502                aggregated.public_function_calldata.clone()
2503            }
2504        };
2505
2506        // Process through simulated kernel
2507        let kernel_output = crate::kernel::SimulatedKernel::process(
2508            &aggregated.execution_result,
2509            tx_constants,
2510            &fee_payer.0,
2511            expiration_timestamp,
2512        )?;
2513        // Serialize kernel output to buffer and build TxProvingResult
2514        let tx_hash = aztec_core::tx::TxHash(kernel_output.public_inputs.hash().to_be_bytes());
2515        let public_inputs_buffer = kernel_output.public_inputs.to_buffer();
2516        let public_inputs =
2517            aztec_core::tx::PrivateKernelTailCircuitPublicInputs::from_bytes(public_inputs_buffer);
2518        // ChonkProof TS format: 4-byte BE field count + N×32-byte Fr fields
2519        let mut chonk_bytes =
2520            Vec::with_capacity(4 + aztec_core::constants::CHONK_PROOF_LENGTH * 32);
2521        chonk_bytes
2522            .extend_from_slice(&(aztec_core::constants::CHONK_PROOF_LENGTH as u32).to_be_bytes());
2523        chonk_bytes.resize(4 + aztec_core::constants::CHONK_PROOF_LENGTH * 32, 0);
2524        let chonk_proof = aztec_core::tx::ChonkProof::from_bytes(chonk_bytes);
2525
2526        Ok(TxProvingResult {
2527            tx_hash: Some(tx_hash),
2528            private_execution_result: serde_json::json!({}),
2529            public_inputs,
2530            chonk_proof,
2531            contract_class_log_fields: aggregated.contract_class_log_fields,
2532            public_function_calldata: sorted_calldata,
2533            stats: None,
2534        })
2535    }
2536
2537    async fn profile_tx(
2538        &self,
2539        tx_request: &TxExecutionRequest,
2540        opts: ProfileTxOpts,
2541    ) -> Result<TxProfileResult, Error> {
2542        self.sync_block_state().await?;
2543
2544        let anchor = self.get_anchor_block_header().await?;
2545        let protocol_nullifier = compute_protocol_nullifier(&Self::tx_request_hash(tx_request)?);
2546        let request: DecodedTxExecutionRequest = serde_json::from_value(tx_request.data.clone())?;
2547        let origin = request.origin;
2548        let decoded_calls = Self::decode_entrypoint_calls(&request)?;
2549        let fee_payer = request.fee_payer.unwrap_or(origin);
2550        let mut bundles = Vec::with_capacity(decoded_calls.len());
2551        for call in &decoded_calls {
2552            bundles.push(
2553                self.execute_entrypoint_call_bundle(
2554                    tx_request,
2555                    call,
2556                    origin,
2557                    protocol_nullifier,
2558                    &anchor,
2559                    &opts.scopes,
2560                )
2561                .await?,
2562            );
2563        }
2564        let aggregated = Self::aggregate_call_bundles(origin, bundles);
2565
2566        let mut tx_constants = Self::build_tx_constant_data(
2567            &anchor,
2568            tx_request,
2569            self.vk_tree_root,
2570            self.protocol_contracts_hash,
2571        );
2572        let expiration_timestamp = if aggregated.execution_result.expiration_timestamp != 0 {
2573            aggregated.execution_result.expiration_timestamp
2574        } else {
2575            Self::compute_expiration(&anchor)
2576        };
2577        Self::ensure_min_fees(&mut tx_constants, &anchor);
2578
2579        let _kernel_output = crate::kernel::SimulatedKernel::process(
2580            &aggregated.execution_result,
2581            tx_constants,
2582            &fee_payer.0,
2583            expiration_timestamp,
2584        )?;
2585
2586        let data = serde_json::json!({
2587            "expirationTimestamp": expiration_timestamp,
2588            "data": {
2589                "expirationTimestamp": expiration_timestamp,
2590            },
2591        });
2592
2593        Ok(TxProfileResult { data })
2594    }
2595
2596    async fn execute_utility(
2597        &self,
2598        call: &FunctionCall,
2599        opts: ExecuteUtilityOpts,
2600    ) -> Result<UtilityExecutionResult, Error> {
2601        self.sync_block_state().await?;
2602        // Always wipe the contract sync cache before utility execution.
2603        // This ensures we pick up nullifier-tree changes from recently
2604        // mined transactions (which may share the same block number).
2605        self.contract_sync_service.wipe().await;
2606        let anchor = self.get_anchor_block_header().await?;
2607
2608        // Look up the artifact for the target contract
2609        let contract_instance = self.contract_store.get_instance(&call.to).await?;
2610        let class_id = contract_instance
2611            .as_ref()
2612            .map(|i| i.inner.current_contract_class_id)
2613            .ok_or_else(|| Error::InvalidData(format!("contract not found: {}", call.to)))?;
2614
2615        let artifact = self
2616            .contract_store
2617            .get_artifact(&class_id)
2618            .await?
2619            .ok_or_else(|| {
2620                Error::InvalidData(format!("artifact not found for class {class_id}"))
2621            })?;
2622
2623        // Find the function by selector
2624        let function = artifact
2625            .find_function_by_selector(&call.selector)
2626            .or_else(|| {
2627                // Fallback: try finding by name from selector string
2628                artifact
2629                    .functions
2630                    .iter()
2631                    .find(|f| f.function_type == FunctionType::Utility)
2632            })
2633            .ok_or_else(|| {
2634                Error::InvalidData(format!(
2635                    "utility function with selector {} not found in {}",
2636                    call.selector, artifact.name
2637                ))
2638            })?;
2639
2640        let function_name = function.name.clone();
2641
2642        if function_name != "sync_state" {
2643            self.contract_sync_service
2644                .ensure_contract_synced(
2645                    &call.to,
2646                    &opts.scopes,
2647                    &anchor.block_hash,
2648                    |contract, scopes| async move {
2649                        self.execute_sync_state_for_contract(contract, scopes).await
2650                    },
2651                )
2652                .await?;
2653        }
2654
2655        // Encode arguments as field elements (recursively flatten structs/arrays)
2656        let args: Vec<Fr> = call
2657            .args
2658            .iter()
2659            .flat_map(Self::abi_value_to_fields)
2660            .collect();
2661
2662        // Create utility oracle
2663        let mut oracle = crate::execution::UtilityExecutionOracle::new(
2664            &self.node,
2665            &self.contract_store,
2666            &self.key_store,
2667            &self.note_store,
2668            &self.address_store,
2669            &self.capsule_store,
2670            &self.sender_store,
2671            &self.sender_tagging_store,
2672            &self.recipient_tagging_store,
2673            &self.private_event_store,
2674            &self.anchor_block_store,
2675            anchor.data.clone(),
2676            call.to,
2677            opts.scopes.clone(),
2678        );
2679
2680        // Set auth witnesses if provided
2681        let auth_witness_pairs: Vec<(Fr, Vec<Fr>)> = opts
2682            .authwits
2683            .iter()
2684            .map(|aw| (aw.request_hash, aw.fields.clone()))
2685            .collect();
2686        oracle.set_auth_witnesses(auth_witness_pairs);
2687
2688        // Execute via ACVM
2689        let result = crate::execution::AcvmExecutor::execute_utility(
2690            &artifact,
2691            &function_name,
2692            &args,
2693            &mut oracle,
2694        )
2695        .await?;
2696
2697        Ok(UtilityExecutionResult {
2698            result: result.return_values,
2699            stats: None,
2700        })
2701    }
2702
2703    async fn get_private_events(
2704        &self,
2705        event_selector: &EventSelector,
2706        filter: PrivateEventFilter,
2707    ) -> Result<Vec<PackedPrivateEvent>, Error> {
2708        // Phase 3: Private event retrieval
2709        //
2710        // Matching the TS PXE.getPrivateEvents flow:
2711        // 1. Sync block state
2712        // 2. Get anchor block number
2713        // 3. Ensure contract synced (when ACVM is available)
2714        // 4. Validate filter
2715        // 5. Query PrivateEventStore
2716        // 6. Convert to PackedPrivateEvent
2717
2718        self.sync_block_state().await?;
2719
2720        let anchor_block_number = self.get_anchor_block_number().await?;
2721
2722        // Ensure contract is synced for the filter's contract address
2723        // (when ACVM is available, this runs the contract's sync_state function)
2724        let anchor_hash = self
2725            .get_anchor_block_header()
2726            .await
2727            .map(|h| h.block_hash)
2728            .unwrap_or_default();
2729        self.contract_sync_service
2730            .ensure_contract_synced(
2731                &filter.contract_address,
2732                &filter.scopes,
2733                &anchor_hash,
2734                |contract, scopes| async move {
2735                    self.execute_sync_state_for_contract(contract, scopes).await
2736                },
2737            )
2738            .await?;
2739
2740        // Validate and sanitize the filter
2741        let validator = PrivateEventFilterValidator::new(anchor_block_number);
2742        let query_filter = validator.validate(&filter)?;
2743
2744        tracing::debug!(
2745            contract = %filter.contract_address,
2746            from_block = ?query_filter.from_block,
2747            to_block = ?query_filter.to_block,
2748            "getting private events"
2749        );
2750
2751        // Query the store
2752        let stored_events = self
2753            .private_event_store
2754            .get_private_events(event_selector, &query_filter)
2755            .await?;
2756
2757        // Convert StoredPrivateEvent → PackedPrivateEvent
2758        let packed_events: Vec<PackedPrivateEvent> = stored_events
2759            .into_iter()
2760            .map(|e| {
2761                let l2_block_hash =
2762                    aztec_pxe_client::BlockHash::from_hex(&e.l2_block_hash).unwrap_or_default();
2763
2764                PackedPrivateEvent {
2765                    packed_event: e.msg_content,
2766                    tx_hash: e.tx_hash,
2767                    l2_block_number: e.l2_block_number,
2768                    l2_block_hash,
2769                    event_selector: e.event_selector,
2770                }
2771            })
2772            .collect();
2773
2774        Ok(packed_events)
2775    }
2776
2777    async fn stop(&self) -> Result<(), Error> {
2778        tracing::debug!("EmbeddedPxe stopped");
2779        Ok(())
2780    }
2781}
2782
2783#[cfg(test)]
2784mod tests {
2785    use super::*;
2786    use aztec_core::types::{ContractInstance, PublicKeys};
2787    use std::sync::Mutex;
2788
2789    fn sample_public_artifact() -> ContractArtifact {
2790        ContractArtifact::from_json(
2791            r#"{
2792                "name": "Counter",
2793                "functions": [
2794                    {
2795                        "name": "constructor",
2796                        "function_type": "private",
2797                        "is_initializer": true,
2798                        "is_static": false,
2799                        "parameters": [],
2800                        "return_types": [],
2801                        "selector": "0xe5fb6c81",
2802                        "bytecode": "0x01"
2803                    },
2804                    {
2805                        "name": "increment",
2806                        "function_type": "public",
2807                        "is_initializer": false,
2808                        "is_static": false,
2809                        "parameters": [
2810                            { "name": "value", "type": { "kind": "field" } }
2811                        ],
2812                        "return_types": [],
2813                        "selector": "0x12345678",
2814                        "bytecode": "0x01"
2815                    }
2816                ]
2817            }"#,
2818        )
2819        .unwrap()
2820    }
2821
2822    /// A minimal mock AztecNode for testing.
2823    #[derive(Clone)]
2824    struct MockNode {
2825        registered_signatures: Arc<Mutex<Vec<String>>>,
2826    }
2827
2828    impl Default for MockNode {
2829        fn default() -> Self {
2830            Self {
2831                registered_signatures: Arc::new(Mutex::new(vec![])),
2832            }
2833        }
2834    }
2835
2836    #[async_trait]
2837    impl AztecNode for MockNode {
2838        async fn get_node_info(&self) -> Result<aztec_node_client::NodeInfo, Error> {
2839            Ok(aztec_node_client::NodeInfo {
2840                node_version: "mock".into(),
2841                l1_chain_id: 1,
2842                rollup_version: 1,
2843                enr: None,
2844                l1_contract_addresses: serde_json::Value::Null,
2845                protocol_contract_addresses: serde_json::Value::Null,
2846                real_proofs: false,
2847                l2_circuits_vk_tree_root: None,
2848                l2_protocol_contracts_hash: None,
2849            })
2850        }
2851        async fn get_block_number(&self) -> Result<u64, Error> {
2852            Ok(1)
2853        }
2854        async fn get_proven_block_number(&self) -> Result<u64, Error> {
2855            Ok(1)
2856        }
2857        async fn get_tx_receipt(
2858            &self,
2859            _tx_hash: &aztec_core::tx::TxHash,
2860        ) -> Result<aztec_core::tx::TxReceipt, Error> {
2861            Err(Error::InvalidData("mock".into()))
2862        }
2863        async fn get_tx_effect(
2864            &self,
2865            _tx_hash: &aztec_core::tx::TxHash,
2866        ) -> Result<Option<serde_json::Value>, Error> {
2867            Ok(None)
2868        }
2869        async fn get_tx_by_hash(
2870            &self,
2871            _tx_hash: &aztec_core::tx::TxHash,
2872        ) -> Result<Option<serde_json::Value>, Error> {
2873            Ok(None)
2874        }
2875        async fn get_public_logs(
2876            &self,
2877            _filter: aztec_node_client::PublicLogFilter,
2878        ) -> Result<aztec_node_client::PublicLogsResponse, Error> {
2879            Ok(aztec_node_client::PublicLogsResponse {
2880                logs: vec![],
2881                max_logs_hit: false,
2882            })
2883        }
2884        async fn send_tx(&self, _tx: &serde_json::Value) -> Result<(), Error> {
2885            Err(Error::InvalidData("mock".into()))
2886        }
2887        async fn get_contract(
2888            &self,
2889            _address: &AztecAddress,
2890        ) -> Result<Option<ContractInstanceWithAddress>, Error> {
2891            Ok(None)
2892        }
2893        async fn get_contract_class(&self, _id: &Fr) -> Result<Option<serde_json::Value>, Error> {
2894            Ok(None)
2895        }
2896        async fn get_block_header(&self, _block_number: u64) -> Result<serde_json::Value, Error> {
2897            Ok(serde_json::json!({"globalVariables": {"blockNumber": 1}, "blockHash": "0x01"}))
2898        }
2899        async fn get_block(&self, _block_number: u64) -> Result<Option<serde_json::Value>, Error> {
2900            Ok(None)
2901        }
2902        async fn get_note_hash_membership_witness(
2903            &self,
2904            _block_number: u64,
2905            _note_hash: &Fr,
2906        ) -> Result<Option<serde_json::Value>, Error> {
2907            Ok(None)
2908        }
2909        async fn get_nullifier_membership_witness(
2910            &self,
2911            _block_number: u64,
2912            _nullifier: &Fr,
2913        ) -> Result<Option<serde_json::Value>, Error> {
2914            Ok(None)
2915        }
2916        async fn get_low_nullifier_membership_witness(
2917            &self,
2918            _block_number: u64,
2919            _nullifier: &Fr,
2920        ) -> Result<Option<serde_json::Value>, Error> {
2921            Ok(None)
2922        }
2923        async fn get_public_storage_at(
2924            &self,
2925            _block_number: u64,
2926            _contract: &AztecAddress,
2927            _slot: &Fr,
2928        ) -> Result<Fr, Error> {
2929            Ok(Fr::zero())
2930        }
2931        async fn get_public_data_witness(
2932            &self,
2933            _block_number: u64,
2934            _leaf_slot: &Fr,
2935        ) -> Result<Option<serde_json::Value>, Error> {
2936            Ok(None)
2937        }
2938        async fn get_l1_to_l2_message_membership_witness(
2939            &self,
2940            _block_number: u64,
2941            _entry_key: &Fr,
2942        ) -> Result<Option<serde_json::Value>, Error> {
2943            Ok(None)
2944        }
2945        async fn simulate_public_calls(
2946            &self,
2947            _tx: &serde_json::Value,
2948            _skip_fee_enforcement: bool,
2949        ) -> Result<serde_json::Value, Error> {
2950            Ok(serde_json::Value::Null)
2951        }
2952        async fn is_valid_tx(
2953            &self,
2954            _tx: &serde_json::Value,
2955        ) -> Result<aztec_node_client::TxValidationResult, Error> {
2956            Ok(aztec_node_client::TxValidationResult::Valid)
2957        }
2958        async fn get_private_logs_by_tags(&self, _tags: &[Fr]) -> Result<serde_json::Value, Error> {
2959            Ok(serde_json::json!([]))
2960        }
2961        async fn get_public_logs_by_tags_from_contract(
2962            &self,
2963            _contract: &AztecAddress,
2964            _tags: &[Fr],
2965        ) -> Result<serde_json::Value, Error> {
2966            Ok(serde_json::json!([]))
2967        }
2968        async fn register_contract_function_signatures(
2969            &self,
2970            signatures: &[String],
2971        ) -> Result<(), Error> {
2972            self.registered_signatures
2973                .lock()
2974                .unwrap()
2975                .extend(signatures.iter().cloned());
2976            Ok(())
2977        }
2978        async fn get_block_hash_membership_witness(
2979            &self,
2980            _block_number: u64,
2981            _block_hash: &Fr,
2982        ) -> Result<Option<serde_json::Value>, Error> {
2983            Ok(None)
2984        }
2985        async fn find_leaves_indexes(
2986            &self,
2987            _block_number: u64,
2988            _tree_id: &str,
2989            _leaves: &[Fr],
2990        ) -> Result<Vec<Option<u64>>, Error> {
2991            Ok(vec![])
2992        }
2993    }
2994
2995    #[tokio::test]
2996    async fn create_and_register_account() {
2997        let pxe = EmbeddedPxe::create_ephemeral(MockNode::default())
2998            .await
2999            .unwrap();
3000        let sk = Fr::from(8923u64);
3001        let partial = Fr::from(243523u64);
3002        let complete = pxe.register_account(&sk, &partial).await.unwrap();
3003        assert_eq!(complete.partial_address, partial);
3004
3005        let accounts = pxe.get_registered_accounts().await.unwrap();
3006        assert_eq!(accounts.len(), 1);
3007        assert_eq!(accounts[0].address, complete.address);
3008    }
3009
3010    #[tokio::test]
3011    async fn register_and_retrieve_contract() {
3012        use aztec_pxe_client::RegisterContractRequest;
3013
3014        let pxe = EmbeddedPxe::create_ephemeral(MockNode::default())
3015            .await
3016            .unwrap();
3017        let artifact = sample_public_artifact();
3018        let class_id = compute_contract_class_id_from_artifact(&artifact).unwrap();
3019        let inner = ContractInstance {
3020            version: 1,
3021            salt: Fr::from(1u64),
3022            deployer: AztecAddress::zero(),
3023            current_contract_class_id: class_id,
3024            original_contract_class_id: class_id,
3025            initialization_hash: Fr::zero(),
3026            public_keys: PublicKeys::default(),
3027        };
3028        let address = compute_contract_address_from_instance(&inner).unwrap();
3029        let instance = ContractInstanceWithAddress { address, inner };
3030
3031        pxe.register_contract_class(&artifact).await.unwrap();
3032
3033        pxe.register_contract(RegisterContractRequest {
3034            instance: instance.clone(),
3035            artifact: None,
3036        })
3037        .await
3038        .unwrap();
3039
3040        let retrieved = pxe.get_contract_instance(&instance.address).await.unwrap();
3041        assert!(retrieved.is_some());
3042
3043        let contracts = pxe.get_contracts().await.unwrap();
3044        assert_eq!(contracts.len(), 1);
3045    }
3046
3047    #[tokio::test]
3048    async fn sender_management() {
3049        let pxe = EmbeddedPxe::create_ephemeral(MockNode::default())
3050            .await
3051            .unwrap();
3052        let sender = AztecAddress::from(99u64);
3053
3054        pxe.register_sender(&sender).await.unwrap();
3055        let senders = pxe.get_senders().await.unwrap();
3056        assert_eq!(senders.len(), 1);
3057
3058        pxe.remove_sender(&sender).await.unwrap();
3059        let senders = pxe.get_senders().await.unwrap();
3060        assert!(senders.is_empty());
3061    }
3062
3063    #[tokio::test]
3064    async fn block_header_sync() {
3065        let pxe = EmbeddedPxe::create_ephemeral(MockNode::default())
3066            .await
3067            .unwrap();
3068        let header = pxe.get_synced_block_header().await.unwrap();
3069        assert!(header.data.is_object());
3070    }
3071
3072    #[tokio::test]
3073    async fn register_sender_ignores_registered_accounts() {
3074        let pxe = EmbeddedPxe::create_ephemeral(MockNode::default())
3075            .await
3076            .unwrap();
3077        let sk = Fr::from(77u64);
3078        let partial = Fr::from(123u64);
3079        let complete = pxe.register_account(&sk, &partial).await.unwrap();
3080
3081        pxe.register_sender(&complete.address).await.unwrap();
3082
3083        assert!(pxe.get_senders().await.unwrap().is_empty());
3084    }
3085
3086    #[tokio::test]
3087    async fn register_contract_validates_artifact_and_registers_public_signatures() {
3088        use aztec_pxe_client::RegisterContractRequest;
3089
3090        let node = MockNode::default();
3091        let artifact = sample_public_artifact();
3092        let class_id = compute_contract_class_id_from_artifact(&artifact).unwrap();
3093        let inner = ContractInstance {
3094            version: 1,
3095            salt: Fr::from(5u64),
3096            deployer: AztecAddress::zero(),
3097            current_contract_class_id: class_id,
3098            original_contract_class_id: class_id,
3099            initialization_hash: Fr::zero(),
3100            public_keys: PublicKeys::default(),
3101        };
3102        let address = compute_contract_address_from_instance(&inner).unwrap();
3103        let instance = ContractInstanceWithAddress { address, inner };
3104
3105        let pxe = EmbeddedPxe::create_ephemeral(node).await.unwrap();
3106        pxe.register_contract(RegisterContractRequest {
3107            instance,
3108            artifact: Some(artifact),
3109        })
3110        .await
3111        .unwrap();
3112
3113        let registered = pxe.node().registered_signatures.lock().unwrap().clone();
3114        assert_eq!(registered, vec!["increment(Field)".to_owned()]);
3115    }
3116
3117    #[tokio::test]
3118    async fn register_contract_rejects_missing_artifact_for_unknown_class() {
3119        use aztec_pxe_client::RegisterContractRequest;
3120
3121        let pxe = EmbeddedPxe::create_ephemeral(MockNode::default())
3122            .await
3123            .unwrap();
3124        let instance = ContractInstanceWithAddress {
3125            address: AztecAddress::from(42u64),
3126            inner: ContractInstance {
3127                version: 1,
3128                salt: Fr::from(1u64),
3129                deployer: AztecAddress::zero(),
3130                current_contract_class_id: Fr::from(999u64),
3131                original_contract_class_id: Fr::from(999u64),
3132                initialization_hash: Fr::zero(),
3133                public_keys: PublicKeys::default(),
3134            },
3135        };
3136
3137        let err = pxe
3138            .register_contract(RegisterContractRequest {
3139                instance,
3140                artifact: None,
3141            })
3142            .await
3143            .unwrap_err();
3144
3145        assert!(err.to_string().contains("artifact not found"));
3146    }
3147
3148    #[tokio::test]
3149    async fn get_private_events_returns_empty_when_no_events() {
3150        let pxe = EmbeddedPxe::create_ephemeral(MockNode::default())
3151            .await
3152            .unwrap();
3153        let events = pxe
3154            .get_private_events(
3155                &EventSelector(Fr::from(1u64)),
3156                PrivateEventFilter {
3157                    contract_address: AztecAddress::from(1u64),
3158                    scopes: vec![AztecAddress::from(99u64)],
3159                    ..Default::default()
3160                },
3161            )
3162            .await
3163            .unwrap();
3164        assert!(events.is_empty());
3165    }
3166
3167    #[tokio::test]
3168    async fn get_private_events_rejects_empty_scopes() {
3169        let pxe = EmbeddedPxe::create_ephemeral(MockNode::default())
3170            .await
3171            .unwrap();
3172        let result = pxe
3173            .get_private_events(
3174                &EventSelector(Fr::from(1u64)),
3175                PrivateEventFilter {
3176                    contract_address: AztecAddress::from(1u64),
3177                    scopes: vec![],
3178                    ..Default::default()
3179                },
3180            )
3181            .await;
3182        assert!(result.is_err());
3183    }
3184
3185    #[tokio::test]
3186    async fn anchor_block_store_is_populated_after_create() {
3187        let pxe = EmbeddedPxe::create_ephemeral(MockNode::default())
3188            .await
3189            .unwrap();
3190        let anchor = pxe.anchor_block_store().get_block_header().await.unwrap();
3191        assert!(anchor.is_some());
3192    }
3193}