aztec_pxe/execution/
utility_oracle.rs

1//! Oracle for utility (view/unconstrained) function execution.
2
3use aztec_core::constants::domain_separator;
4use aztec_core::error::Error;
5use aztec_core::fee::GasFees;
6use aztec_core::grumpkin;
7use aztec_core::hash::poseidon2_hash;
8use aztec_core::kernel_types::{
9    AppendOnlyTreeSnapshot, BlockHeader, GlobalVariables, PartialStateReference, StateReference,
10};
11use aztec_core::tx::TxHash;
12use aztec_core::types::{AztecAddress, Fq, Fr};
13use aztec_node_client::AztecNode;
14
15use super::acvm_executor::OracleCallback;
16use crate::stores::note_store::{NoteFilter, NoteStatus, StoredNote};
17use crate::stores::{
18    AddressStore, AnchorBlockStore, CapsuleStore, ContractStore, KeyStore, NoteStore,
19    PrivateEventStore, RecipientTaggingStore, SenderStore, SenderTaggingStore,
20};
21use crate::sync::event_service::EventService;
22use crate::sync::log_service::{LogRetrievalRequest, LogService};
23use crate::sync::note_service::NoteService;
24
25/// Oracle for utility function execution (read-only, no side effects).
26pub struct UtilityExecutionOracle<'a, N: AztecNode> {
27    node: &'a N,
28    contract_store: &'a ContractStore,
29    key_store: &'a KeyStore,
30    note_store: &'a NoteStore,
31    address_store: &'a AddressStore,
32    capsule_store: &'a CapsuleStore,
33    sender_store: &'a SenderStore,
34    sender_tagging_store: &'a SenderTaggingStore,
35    recipient_tagging_store: &'a RecipientTaggingStore,
36    private_event_store: &'a PrivateEventStore,
37    anchor_block_store: &'a AnchorBlockStore,
38    block_header: serde_json::Value,
39    contract_address: AztecAddress,
40    scopes: Vec<AztecAddress>,
41    /// Auth witnesses available for this execution.
42    auth_witnesses: Vec<(Fr, Vec<Fr>)>,
43}
44
45fn decode_base64_sibling_path(encoded: &str) -> Result<Vec<Fr>, Error> {
46    use base64::Engine;
47
48    let bytes = base64::engine::general_purpose::STANDARD
49        .decode(encoded)
50        .map_err(|e| Error::InvalidData(format!("invalid siblingPath base64: {e}")))?;
51
52    let payload = if bytes.len() >= 4 {
53        let declared_len =
54            u32::from_be_bytes(bytes[..4].try_into().expect("length prefix is 4 bytes")) as usize;
55        let payload = &bytes[4..];
56        if payload.len() == declared_len.saturating_mul(32) {
57            payload
58        } else if bytes.len() % 32 == 0 {
59            bytes.as_slice()
60        } else {
61            return Err(Error::InvalidData(format!(
62                "siblingPath payload length mismatch: declared {declared_len} elements, got {} bytes",
63                payload.len()
64            )));
65        }
66    } else {
67        bytes.as_slice()
68    };
69
70    Ok(payload
71        .chunks(32)
72        .map(|chunk| {
73            let mut padded = [0u8; 32];
74            let start = 32usize.saturating_sub(chunk.len());
75            padded[start..].copy_from_slice(chunk);
76            Fr::from(padded)
77        })
78        .collect())
79}
80
81fn parse_field_string(value: &str) -> Result<Fr, Error> {
82    if value.starts_with("0x") {
83        Fr::from_hex(value)
84    } else {
85        value
86            .parse::<u128>()
87            .map(Fr::from)
88            .map_err(|_| Error::InvalidData(format!("unsupported field string value: {value}")))
89    }
90}
91
92impl<'a, N: AztecNode> UtilityExecutionOracle<'a, N> {
93    /// Map Noir NoteStatus enum values: ACTIVE = 1, ACTIVE_OR_NULLIFIED = 2.
94    fn note_status_from_field(value: Fr) -> Result<NoteStatus, Error> {
95        match value.to_usize() as u64 {
96            1 => Ok(NoteStatus::Active),
97            2 => Ok(NoteStatus::ActiveOrNullified),
98            other => Err(Error::InvalidData(format!("unknown note status: {other}"))),
99        }
100    }
101
102    fn pack_hinted_note(note: &StoredNote) -> Result<Vec<Fr>, Error> {
103        let mut packed = note.note_data.clone();
104        packed.push(note.contract_address.0);
105        packed.push(note.owner.0);
106        packed.push(note.randomness);
107        packed.push(note.storage_slot);
108        let stage = if note.is_pending {
109            if note.note_nonce == Fr::zero() {
110                1u64
111            } else {
112                2u64
113            }
114        } else {
115            if note.note_nonce == Fr::zero() {
116                return Err(Error::InvalidData(
117                    "cannot pack settled note with zero note_nonce".into(),
118                ));
119            }
120            3u64
121        };
122        packed.push(Fr::from(stage));
123        packed.push(note.note_nonce);
124        Ok(packed)
125    }
126
127    fn pack_bounded_vec_of_arrays(
128        arrays: &[Vec<Fr>],
129        max_len: usize,
130        nested_len: usize,
131    ) -> Result<Vec<Vec<Fr>>, Error> {
132        if arrays.len() > max_len {
133            return Err(Error::InvalidData(format!(
134                "bounded vec overflow: {} > {max_len}",
135                arrays.len()
136            )));
137        }
138
139        let mut flattened = Vec::with_capacity(max_len.saturating_mul(nested_len));
140        for array in arrays {
141            if array.len() != nested_len {
142                return Err(Error::InvalidData(format!(
143                    "packed hinted note length mismatch: {} != {nested_len}",
144                    array.len()
145                )));
146            }
147            flattened.extend_from_slice(array);
148        }
149
150        flattened.resize(max_len.saturating_mul(nested_len), Fr::zero());
151        Ok(vec![flattened, vec![Fr::from(arrays.len() as u64)]])
152    }
153
154    pub fn new(
155        node: &'a N,
156        contract_store: &'a ContractStore,
157        key_store: &'a KeyStore,
158        note_store: &'a NoteStore,
159        address_store: &'a AddressStore,
160        capsule_store: &'a CapsuleStore,
161        sender_store: &'a SenderStore,
162        sender_tagging_store: &'a SenderTaggingStore,
163        recipient_tagging_store: &'a RecipientTaggingStore,
164        private_event_store: &'a PrivateEventStore,
165        anchor_block_store: &'a AnchorBlockStore,
166        block_header: serde_json::Value,
167        contract_address: AztecAddress,
168        scopes: Vec<AztecAddress>,
169    ) -> Self {
170        Self {
171            node,
172            contract_store,
173            key_store,
174            note_store,
175            address_store,
176            capsule_store,
177            sender_store,
178            sender_tagging_store,
179            recipient_tagging_store,
180            private_event_store,
181            anchor_block_store,
182            block_header,
183            contract_address,
184            scopes,
185            auth_witnesses: Vec::new(),
186        }
187    }
188
189    /// Set auth witnesses for this execution context.
190    pub fn set_auth_witnesses(&mut self, witnesses: Vec<(Fr, Vec<Fr>)>) {
191        self.auth_witnesses = witnesses;
192    }
193
194    /// Handle an ACVM foreign call for a utility function.
195    ///
196    /// Supports both prefixed names (from compiled Noir bytecode) and
197    /// legacy unprefixed names.
198    pub async fn handle_foreign_call(
199        &self,
200        name: &str,
201        args: Vec<Vec<Fr>>,
202    ) -> Result<Vec<Vec<Fr>>, Error> {
203        // Strip the common prefixes used by compiled Noir bytecode
204        let stripped = name
205            .strip_prefix("utility")
206            .or_else(|| name.strip_prefix("private"))
207            .unwrap_or(name);
208
209        let handler = if !stripped.is_empty() {
210            let mut chars = stripped.chars();
211            let first = chars.next().unwrap().to_lowercase().to_string();
212            format!("{first}{}", chars.as_str())
213        } else {
214            name.to_owned()
215        };
216
217        match handler.as_str() {
218            // Storage
219            "getPublicStorageAt" | "storageRead" => self.get_public_storage_at(&args).await,
220            "getContractInstance" => self.get_contract_instance(&args).await,
221
222            // Notes
223            "getNotes" => self.get_notes(&args).await,
224            "checkNullifierExists" => self.check_nullifier_exists(&args).await,
225
226            // Keys
227            "getPublicKeysAndPartialAddress" | "tryGetPublicKeysAndPartialAddress" => {
228                self.get_public_keys_and_partial_address(&args).await
229            }
230            "getKeyValidationRequest" | "getSecretKey" => {
231                self.get_key_validation_request(&args).await
232            }
233
234            // Block header
235            "getBlockHeader" => self.get_block_header(&args),
236            "getUtilityContext" => self.get_utility_context(),
237
238            // Auth witnesses
239            "getAuthWitness" => self.get_auth_witness(&args),
240
241            // Membership witnesses
242            "getNoteHashMembershipWitness" => Ok(vec![vec![]]),
243            "getNullifierMembershipWitness" => self.get_nullifier_membership_witness(&args).await,
244            "getLowNullifierMembershipWitness" => {
245                self.get_low_nullifier_membership_witness(&args).await
246            }
247            "getBlockHashMembershipWitness" => Ok(vec![vec![]]),
248            "getPublicDataWitness" => self.get_public_data_witness(&args).await,
249            "getL1ToL2MembershipWitness" => Ok(vec![vec![]]),
250
251            // Misc
252            "getRandomField" => Ok(vec![vec![Fr::random()]]),
253            "assertCompatibleOracleVersion" => Ok(vec![]),
254            "log" => Ok(vec![]),
255            "aes128Decrypt" => self.aes128_decrypt(&args),
256            "getSharedSecret" => self.get_shared_secret(&args).await,
257
258            // Capsules
259            "loadCapsule" | "getCapsule" => self.load_capsule(&args).await,
260            "storeCapsule" => self.store_capsule(&args).await,
261            "deleteCapsule" => self.delete_capsule(&args).await,
262            "copyCapsule" => self.copy_capsule(&args).await,
263
264            // Tagging and log discovery
265            "fetchTaggedLogs" => self.fetch_tagged_logs(&args).await,
266            "bulkRetrieveLogs" => self.bulk_retrieve_logs(&args).await,
267            "validateAndStoreEnqueuedNotesAndEvents" => {
268                self.validate_and_store_enqueued_notes_and_events(&args)
269                    .await
270            }
271            "emitOffchainEffect" => Ok(vec![]),
272
273            _ => {
274                tracing::warn!(
275                    oracle = name,
276                    handler = handler.as_str(),
277                    "unknown utility oracle call"
278                );
279                Ok(vec![])
280            }
281        }
282    }
283
284    fn fr_at(val: &serde_json::Value, path: &str) -> Fr {
285        match val.pointer(path) {
286            Some(serde_json::Value::String(s)) => Fr::from_hex(s).unwrap_or(Fr::zero()),
287            Some(serde_json::Value::Number(n)) => Fr::from(n.as_u64().unwrap_or(0)),
288            _ => Fr::zero(),
289        }
290    }
291
292    fn u64_at(val: &serde_json::Value, path: &str) -> u64 {
293        match val.pointer(path) {
294            Some(serde_json::Value::Number(n)) => n.as_u64().unwrap_or(0),
295            Some(serde_json::Value::String(s)) => {
296                if let Some(hex) = s.strip_prefix("0x") {
297                    u64::from_str_radix(hex, 16).unwrap_or(0)
298                } else {
299                    s.parse::<u64>().unwrap_or(0)
300                }
301            }
302            _ => 0,
303        }
304    }
305
306    fn u128_at(val: &serde_json::Value, path: &str) -> u128 {
307        match val.pointer(path) {
308            Some(serde_json::Value::Number(n)) => n.as_u64().unwrap_or(0) as u128,
309            Some(serde_json::Value::String(s)) => {
310                if let Some(hex) = s.strip_prefix("0x") {
311                    u128::from_str_radix(hex, 16).unwrap_or(0)
312                } else {
313                    s.parse::<u128>().unwrap_or(0)
314                }
315            }
316            _ => 0,
317        }
318    }
319
320    fn eth_at(val: &serde_json::Value, path: &str) -> aztec_core::types::EthAddress {
321        match val.pointer(path).and_then(|v| v.as_str()) {
322            Some(s) => {
323                let fr = Fr::from_hex(s).unwrap_or(Fr::zero());
324                let bytes = fr.to_be_bytes();
325                let mut addr = [0u8; 20];
326                addr.copy_from_slice(&bytes[12..32]);
327                aztec_core::types::EthAddress(addr)
328            }
329            None => aztec_core::types::EthAddress::default(),
330        }
331    }
332
333    fn snapshot_at(val: &serde_json::Value, prefix: &str) -> AppendOnlyTreeSnapshot {
334        AppendOnlyTreeSnapshot {
335            root: Self::fr_at(val, &format!("{prefix}/root")),
336            next_available_leaf_index: Self::u64_at(
337                val,
338                &format!("{prefix}/nextAvailableLeafIndex"),
339            ) as u32,
340        }
341    }
342
343    fn parse_block_header(&self) -> BlockHeader {
344        let h = &self.block_header;
345        BlockHeader {
346            last_archive: Self::snapshot_at(h, "/lastArchive"),
347            state: StateReference {
348                l1_to_l2_message_tree: Self::snapshot_at(h, "/state/l1ToL2MessageTree"),
349                partial: PartialStateReference {
350                    note_hash_tree: Self::snapshot_at(h, "/state/partial/noteHashTree"),
351                    nullifier_tree: Self::snapshot_at(h, "/state/partial/nullifierTree"),
352                    public_data_tree: Self::snapshot_at(h, "/state/partial/publicDataTree"),
353                },
354            },
355            sponge_blob_hash: Self::fr_at(h, "/spongeBlobHash"),
356            global_variables: GlobalVariables {
357                chain_id: Self::fr_at(h, "/globalVariables/chainId"),
358                version: Self::fr_at(h, "/globalVariables/version"),
359                block_number: Self::u64_at(h, "/globalVariables/blockNumber"),
360                slot_number: Self::u64_at(h, "/globalVariables/slotNumber"),
361                timestamp: Self::u64_at(h, "/globalVariables/timestamp"),
362                coinbase: Self::eth_at(h, "/globalVariables/coinbase"),
363                fee_recipient: AztecAddress(Self::fr_at(h, "/globalVariables/feeRecipient")),
364                gas_fees: GasFees {
365                    fee_per_da_gas: Self::u128_at(h, "/globalVariables/gasFees/feePerDaGas"),
366                    fee_per_l2_gas: Self::u128_at(h, "/globalVariables/gasFees/feePerL2Gas"),
367                },
368            },
369            total_fees: Self::fr_at(h, "/totalFees"),
370            total_mana_used: Self::fr_at(h, "/totalManaUsed"),
371        }
372    }
373
374    fn get_utility_context(&self) -> Result<Vec<Vec<Fr>>, Error> {
375        let mut outputs: Vec<Vec<Fr>> = self
376            .parse_block_header()
377            .to_fields()
378            .into_iter()
379            .map(|f| vec![f])
380            .collect();
381        outputs.push(vec![self.contract_address.0]);
382        Ok(outputs)
383    }
384
385    fn get_block_header(&self, _args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
386        Ok(self
387            .parse_block_header()
388            .to_fields()
389            .into_iter()
390            .map(|f| vec![f])
391            .collect())
392    }
393
394    async fn get_public_storage_at(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
395        fn slot_with_offset(start_slot: Fr, offset: usize) -> Fr {
396            let mut bytes = start_slot.to_be_bytes();
397            let mut carry = offset as u128;
398            for byte in bytes.iter_mut().rev() {
399                if carry == 0 {
400                    break;
401                }
402                let sum = u128::from(*byte) + (carry & 0xff);
403                *byte = (sum & 0xff) as u8;
404                carry = (carry >> 8) + (sum >> 8);
405            }
406            Fr::from(bytes)
407        }
408
409        // Newer Noir utility storage reads use the `utilityStorageRead`
410        // signature `(block_hash, contract, start_slot, number_of_elements)`.
411        // Older bytecode still calls into the 2-argument single-slot variant.
412        let (block_hash, contract, start_slot, number_of_elements) = if args.len() >= 4 {
413            let block_hash = args
414                .first()
415                .and_then(|v| v.first())
416                .copied()
417                .unwrap_or_else(Fr::zero);
418            let contract = args
419                .get(1)
420                .and_then(|v| v.first())
421                .ok_or_else(|| Error::InvalidData("missing contract address".into()))?;
422            let start_slot = args
423                .get(2)
424                .and_then(|v| v.first())
425                .ok_or_else(|| Error::InvalidData("missing storage slot".into()))?;
426            let count = args
427                .get(3)
428                .and_then(|v| v.first())
429                .copied()
430                .unwrap_or_else(Fr::zero)
431                .to_usize();
432            (Some(block_hash), contract, start_slot, count.max(1))
433        } else {
434            let contract = args
435                .first()
436                .and_then(|v| v.first())
437                .ok_or_else(|| Error::InvalidData("missing contract address".into()))?;
438            let slot = args
439                .get(1)
440                .and_then(|v| v.first())
441                .ok_or_else(|| Error::InvalidData("missing storage slot".into()))?;
442            (None, contract, slot, 1)
443        };
444
445        let contract_addr = AztecAddress(*contract);
446        let mut values = Vec::with_capacity(number_of_elements);
447        for offset in 0..number_of_elements {
448            let slot = slot_with_offset(*start_slot, offset);
449            let value = match block_hash.as_ref() {
450                Some(block_hash) => {
451                    self.node
452                        .get_public_storage_at_by_hash(block_hash, &contract_addr, &slot)
453                        .await?
454                }
455                None => {
456                    self.node
457                        .get_public_storage_at(0, &contract_addr, &slot)
458                        .await?
459                }
460            };
461            values.push(value);
462        }
463
464        Ok(vec![values])
465    }
466
467    async fn get_contract_instance(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
468        let address = args
469            .first()
470            .and_then(|v| v.first())
471            .ok_or_else(|| Error::InvalidData("missing address".into()))?;
472        let addr = AztecAddress(*address);
473
474        let inst = self.contract_store.get_instance(&addr).await?;
475        let inst = match inst {
476            Some(i) => Some(i),
477            None => self.node.get_contract(&addr).await?,
478        };
479
480        match inst {
481            Some(inst) => Ok(super::oracle::contract_instance_to_fields(&inst.inner)),
482            None => Ok(vec![vec![Fr::zero()]; 16]),
483        }
484    }
485
486    async fn get_notes(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
487        let owner = match (
488            args.first()
489                .and_then(|v| v.first())
490                .copied()
491                .unwrap_or(Fr::zero()),
492            args.get(1)
493                .and_then(|v| v.first())
494                .copied()
495                .unwrap_or(Fr::zero()),
496        ) {
497            (flag, value) if flag != Fr::zero() => Some(AztecAddress(value)),
498            _ => None,
499        };
500        let storage_slot = args
501            .get(2)
502            .and_then(|v| v.first())
503            .copied()
504            .ok_or_else(|| Error::InvalidData("getNotes: missing storage_slot".into()))?;
505        let limit = args
506            .get(13)
507            .and_then(|v| v.first())
508            .copied()
509            .unwrap_or(Fr::zero())
510            .to_usize();
511        let offset = args
512            .get(14)
513            .and_then(|v| v.first())
514            .copied()
515            .unwrap_or(Fr::zero())
516            .to_usize();
517        let status = Self::note_status_from_field(
518            args.get(15)
519                .and_then(|v| v.first())
520                .copied()
521                .unwrap_or(Fr::zero()),
522        )?;
523        let max_notes = args
524            .get(16)
525            .and_then(|v| v.first())
526            .copied()
527            .unwrap_or(Fr::zero())
528            .to_usize();
529        let packed_hinted_note_length = args
530            .get(17)
531            .and_then(|v| v.first())
532            .copied()
533            .unwrap_or(Fr::zero())
534            .to_usize();
535
536        let mut notes = self
537            .note_store
538            .get_notes(&NoteFilter {
539                contract_address: Some(self.contract_address),
540                storage_slot: Some(storage_slot),
541                owner,
542                status,
543                scopes: self.scopes.clone(),
544                ..Default::default()
545            })
546            .await?;
547
548        // Apply select-clause filtering (comparators).
549        let selects = super::pick_notes::parse_select_clauses(args);
550        notes = super::pick_notes::select_notes(notes, &selects);
551
552        tracing::trace!(
553            contract = %self.contract_address,
554            ?owner,
555            slot = %storage_slot,
556            scopes = self.scopes.len(),
557            found = notes.len(),
558            "utility_get_notes"
559        );
560        if offset >= notes.len() {
561            notes.clear();
562        } else if offset > 0 {
563            notes = notes.split_off(offset);
564        }
565
566        if limit > 0 && notes.len() > limit {
567            notes.truncate(limit);
568        }
569        if notes.len() > max_notes {
570            notes.truncate(max_notes);
571        }
572
573        let packed = notes
574            .iter()
575            .map(Self::pack_hinted_note)
576            .collect::<Result<Vec<_>, _>>()?;
577
578        Self::pack_bounded_vec_of_arrays(&packed, max_notes, packed_hinted_note_length)
579    }
580
581    async fn store_capsule(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
582        let contract_address =
583            AztecAddress(*args.first().and_then(|v| v.first()).ok_or_else(|| {
584                Error::InvalidData("storeCapsule: missing contract address".into())
585            })?);
586        let slot = *args
587            .get(1)
588            .and_then(|v| v.first())
589            .ok_or_else(|| Error::InvalidData("storeCapsule: missing slot".into()))?;
590        let capsule = args.get(2).cloned().unwrap_or_default();
591
592        self.ensure_contract_db_access(&contract_address)?;
593        self.capsule_store
594            .store_capsule(&contract_address, &slot, &capsule)
595            .await?;
596        Ok(vec![])
597    }
598
599    async fn load_capsule(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
600        let contract_address =
601            AztecAddress(*args.first().and_then(|v| v.first()).ok_or_else(|| {
602                Error::InvalidData("loadCapsule: missing contract address".into())
603            })?);
604        let slot = *args
605            .get(1)
606            .and_then(|v| v.first())
607            .ok_or_else(|| Error::InvalidData("loadCapsule: missing slot".into()))?;
608        let array_len = args
609            .get(2)
610            .and_then(|v| v.first())
611            .copied()
612            .unwrap_or_else(Fr::zero)
613            .to_usize();
614
615        self.ensure_contract_db_access(&contract_address)?;
616        let maybe_values = self
617            .capsule_store
618            .load_capsule(&contract_address, &slot)
619            .await?;
620        let is_some = maybe_values.is_some();
621        let mut values = maybe_values.unwrap_or_default();
622        values.resize(array_len, Fr::zero());
623        Ok(vec![vec![Fr::from(is_some)], values])
624    }
625
626    async fn delete_capsule(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
627        let contract_address =
628            AztecAddress(*args.first().and_then(|v| v.first()).ok_or_else(|| {
629                Error::InvalidData("deleteCapsule: missing contract address".into())
630            })?);
631        let slot = *args
632            .get(1)
633            .and_then(|v| v.first())
634            .ok_or_else(|| Error::InvalidData("deleteCapsule: missing slot".into()))?;
635
636        self.ensure_contract_db_access(&contract_address)?;
637        self.capsule_store
638            .delete_capsule(&contract_address, &slot)
639            .await?;
640        Ok(vec![])
641    }
642
643    async fn copy_capsule(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
644        let contract_address =
645            AztecAddress(*args.first().and_then(|v| v.first()).ok_or_else(|| {
646                Error::InvalidData("copyCapsule: missing contract address".into())
647            })?);
648        let src_slot = *args
649            .get(1)
650            .and_then(|v| v.first())
651            .ok_or_else(|| Error::InvalidData("copyCapsule: missing src slot".into()))?;
652        let dst_slot = *args
653            .get(2)
654            .and_then(|v| v.first())
655            .ok_or_else(|| Error::InvalidData("copyCapsule: missing dst slot".into()))?;
656        let num_entries = args
657            .get(3)
658            .and_then(|v| v.first())
659            .copied()
660            .unwrap_or_else(Fr::zero)
661            .to_usize();
662
663        self.ensure_contract_db_access(&contract_address)?;
664        self.capsule_store
665            .copy_capsule(&contract_address, &src_slot, &dst_slot, num_entries)
666            .await?;
667        Ok(vec![])
668    }
669
670    async fn fetch_tagged_logs(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
671        let pending_tagged_log_array_base_slot =
672            *args.first().and_then(|v| v.first()).ok_or_else(|| {
673                Error::InvalidData("fetchTaggedLogs: missing capsule array base slot".into())
674            })?;
675
676        if self.scopes.is_empty() {
677            return Ok(vec![]);
678        }
679
680        let log_service = LogService::new(
681            self.node,
682            self.sender_store,
683            self.sender_tagging_store,
684            self.recipient_tagging_store,
685            self.capsule_store,
686        );
687
688        for scope in &self.scopes {
689            let secrets = self.tagging_secrets_for_recipient(scope).await?;
690            if secrets.is_empty() {
691                continue;
692            }
693            let logs = log_service
694                .fetch_tagged_logs(&self.contract_address, scope, &secrets)
695                .await?;
696            if logs.is_empty() {
697                continue;
698            }
699            let serialized = logs
700                .into_iter()
701                .map(|log| serialize_pending_tagged_log(&log, scope))
702                .collect::<Result<Vec<_>, _>>()?;
703            self.capsule_store
704                .append_to_capsule_array(
705                    &self.contract_address,
706                    &pending_tagged_log_array_base_slot,
707                    &serialized,
708                )
709                .await?;
710        }
711
712        Ok(vec![])
713    }
714
715    async fn bulk_retrieve_logs(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
716        let contract_address =
717            AztecAddress(*args.first().and_then(|v| v.first()).ok_or_else(|| {
718                Error::InvalidData("bulkRetrieveLogs: missing contract address".into())
719            })?);
720        let requests_slot = *args
721            .get(1)
722            .and_then(|v| v.first())
723            .ok_or_else(|| Error::InvalidData("bulkRetrieveLogs: missing requests slot".into()))?;
724        let responses_slot = *args
725            .get(2)
726            .and_then(|v| v.first())
727            .ok_or_else(|| Error::InvalidData("bulkRetrieveLogs: missing responses slot".into()))?;
728
729        self.ensure_contract_db_access(&contract_address)?;
730
731        let requests = self
732            .capsule_store
733            .read_capsule_array(&contract_address, &requests_slot)
734            .await?
735            .into_iter()
736            .map(parse_log_retrieval_request)
737            .collect::<Result<Vec<_>, _>>()?;
738
739        let log_service = LogService::new(
740            self.node,
741            self.sender_store,
742            self.sender_tagging_store,
743            self.recipient_tagging_store,
744            self.capsule_store,
745        );
746        let maybe_responses = log_service.bulk_retrieve_logs(&requests).await?;
747
748        self.capsule_store
749            .set_capsule_array(&contract_address, &requests_slot, &[])
750            .await?;
751
752        let serialized = maybe_responses
753            .into_iter()
754            .map(|logs| serialize_log_retrieval_option(logs.first()))
755            .collect::<Result<Vec<_>, _>>()?;
756        self.capsule_store
757            .set_capsule_array(&contract_address, &responses_slot, &serialized)
758            .await?;
759
760        Ok(vec![])
761    }
762
763    async fn validate_and_store_enqueued_notes_and_events(
764        &self,
765        args: &[Vec<Fr>],
766    ) -> Result<Vec<Vec<Fr>>, Error> {
767        let contract_address =
768            AztecAddress(*args.first().and_then(|v| v.first()).ok_or_else(|| {
769                Error::InvalidData(
770                    "validateAndStoreEnqueuedNotesAndEvents: missing contract address".into(),
771                )
772            })?);
773        let note_requests_slot = *args.get(1).and_then(|v| v.first()).ok_or_else(|| {
774            Error::InvalidData(
775                "validateAndStoreEnqueuedNotesAndEvents: missing note requests slot".into(),
776            )
777        })?;
778        let event_requests_slot = *args.get(2).and_then(|v| v.first()).ok_or_else(|| {
779            Error::InvalidData(
780                "validateAndStoreEnqueuedNotesAndEvents: missing event requests slot".into(),
781            )
782        })?;
783
784        self.ensure_contract_db_access(&contract_address)?;
785
786        let note_requests = self
787            .capsule_store
788            .read_capsule_array(&contract_address, &note_requests_slot)
789            .await?;
790        let note_service = NoteService::new(self.node, self.note_store);
791        for fields in note_requests {
792            let request = parse_note_validation_request(&fields)?;
793            note_service
794                .validate_and_store_note(
795                    &crate::stores::note_store::StoredNote {
796                        contract_address: request.contract_address,
797                        owner: request.owner,
798                        storage_slot: request.storage_slot,
799                        randomness: request.randomness,
800                        note_nonce: request.note_nonce,
801                        note_hash: request.note_hash,
802                        siloed_nullifier: aztec_core::hash::silo_nullifier(
803                            &request.contract_address,
804                            &request.nullifier,
805                        ),
806                        note_data: request.content,
807                        nullified: false,
808                        is_pending: false,
809                        nullification_block_number: None,
810                        leaf_index: None,
811                        block_number: None,
812                        tx_index_in_block: None,
813                        note_index_in_tx: None,
814                        scopes: vec![request.recipient],
815                    },
816                    &request.recipient,
817                )
818                .await?;
819        }
820
821        let event_requests = self
822            .capsule_store
823            .read_capsule_array(&contract_address, &event_requests_slot)
824            .await?;
825        let event_service =
826            EventService::new(self.node, self.private_event_store, self.anchor_block_store);
827        for fields in event_requests {
828            let request = parse_event_validation_request(&fields)?;
829            event_service
830                .validate_and_store_event(
831                    &request.contract_address,
832                    &request.event_type_id,
833                    request.randomness,
834                    request.serialized_event,
835                    request.event_commitment,
836                    request.tx_hash,
837                    &request.recipient,
838                )
839                .await?;
840        }
841
842        self.capsule_store
843            .set_capsule_array(&contract_address, &note_requests_slot, &[])
844            .await?;
845        self.capsule_store
846            .set_capsule_array(&contract_address, &event_requests_slot, &[])
847            .await?;
848
849        Ok(vec![])
850    }
851
852    async fn check_nullifier_exists(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
853        let inner_nullifier = args
854            .first()
855            .and_then(|v| v.first())
856            .ok_or_else(|| Error::InvalidData("missing nullifier".into()))?;
857        // Silo before looking up — the tree stores siloed nullifiers
858        let siloed = aztec_core::hash::silo_nullifier(&self.contract_address, inner_nullifier);
859        let witness = self
860            .node
861            .get_nullifier_membership_witness(0, &siloed)
862            .await?;
863        Ok(vec![vec![Fr::from(witness.is_some())]])
864    }
865
866    /// Query the node for a nullifier membership witness.
867    ///
868    /// The Noir oracle passes `[block_number, nullifier]`. The nullifier is
869    /// already siloed (matching what the kernel stores). We query at "latest"
870    /// (block 0) and return the same 5-slot format as
871    /// `get_low_nullifier_membership_witness`.
872    async fn get_nullifier_membership_witness(
873        &self,
874        args: &[Vec<Fr>],
875    ) -> Result<Vec<Vec<Fr>>, Error> {
876        let _block_number = args
877            .first()
878            .and_then(|v| v.first())
879            .copied()
880            .unwrap_or(Fr::zero());
881        let nullifier = args.get(1).and_then(|v| v.first()).ok_or_else(|| {
882            Error::InvalidData("getNullifierMembershipWitness: missing nullifier".into())
883        })?;
884
885        let witness_json = self
886            .node
887            .get_nullifier_membership_witness(0, nullifier)
888            .await?;
889
890        if let Some(json) = witness_json {
891            let index = Self::parse_field_or_number(json.get("index"));
892
893            let preimage = json.get("leafPreimage").unwrap_or(&json);
894            let leaf = preimage.get("leaf").unwrap_or(preimage);
895            let nullifier_val = leaf
896                .get("nullifier")
897                .and_then(|v| v.as_str())
898                .and_then(|s| Fr::from_hex(s).ok())
899                .unwrap_or(Fr::zero());
900            let next_nullifier = preimage
901                .get("nextKey")
902                .or_else(|| preimage.get("nextNullifier"))
903                .and_then(|v| v.as_str())
904                .and_then(|s| Fr::from_hex(s).ok())
905                .unwrap_or(Fr::zero());
906            let next_index = Self::parse_field_or_number(preimage.get("nextIndex"));
907
908            let path = json
909                .get("siblingPath")
910                .and_then(|v| v.as_str())
911                .and_then(|s| decode_base64_sibling_path(s).ok())
912                .unwrap_or_else(|| vec![Fr::zero(); 42]);
913
914            let mut path = path;
915            path.resize(42, Fr::zero());
916
917            Ok(vec![
918                vec![index],
919                vec![nullifier_val],
920                vec![next_nullifier],
921                vec![next_index],
922                path,
923            ])
924        } else {
925            Ok(vec![
926                vec![Fr::zero()],
927                vec![Fr::zero()],
928                vec![Fr::zero()],
929                vec![Fr::zero()],
930                vec![Fr::zero(); 42],
931            ])
932        }
933    }
934
935    async fn get_public_data_witness(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
936        fn fr_at(value: &serde_json::Value, path: &str) -> Result<Fr, Error> {
937            let raw = value.pointer(path).ok_or_else(|| {
938                Error::InvalidData(format!("public data witness missing field at {path}"))
939            })?;
940            if let Some(s) = raw.as_str() {
941                return parse_field_string(s).map_err(|_| {
942                    Error::InvalidData(format!(
943                        "public data witness field at {path} has unsupported string value: {s}"
944                    ))
945                });
946            }
947            if let Some(n) = raw.as_u64() {
948                return Ok(Fr::from(n));
949            }
950            Err(Error::InvalidData(format!(
951                "public data witness field at {path} has unsupported shape: {raw:?}"
952            )))
953        }
954
955        let block_hash = args
956            .first()
957            .and_then(|v| v.first())
958            .copied()
959            .unwrap_or_else(Fr::zero);
960        let leaf_slot = args
961            .get(1)
962            .and_then(|v| v.first())
963            .ok_or_else(|| Error::InvalidData("missing leaf slot".into()))?;
964        let witness = self
965            .node
966            .get_public_data_witness_by_hash(&block_hash, leaf_slot)
967            .await?;
968        let Some(witness) = witness else {
969            return Ok(vec![
970                vec![Fr::zero()],
971                vec![Fr::zero()],
972                vec![Fr::zero()],
973                vec![Fr::zero()],
974                vec![Fr::zero()],
975                vec![Fr::zero(); aztec_core::constants::PUBLIC_DATA_TREE_HEIGHT],
976            ]);
977        };
978
979        let sibling_path = match witness.pointer("/siblingPath") {
980            Some(serde_json::Value::Array(entries)) => entries
981                .iter()
982                .map(|entry| {
983                    if let Some(s) = entry.as_str() {
984                        parse_field_string(s).map_err(|_| {
985                            Error::InvalidData(format!(
986                                "public data witness siblingPath entry has unsupported string value: {s}"
987                            ))
988                        })
989                    } else if let Some(n) = entry.as_u64() {
990                        Ok(Fr::from(n))
991                    } else {
992                        Err(Error::InvalidData(format!(
993                            "public data witness siblingPath entry has unsupported shape: {entry:?}"
994                        )))
995                    }
996                })
997                .collect::<Result<Vec<_>, _>>()?,
998            Some(serde_json::Value::String(encoded)) => {
999                decode_base64_sibling_path(encoded)?
1000            }
1001            _ => {
1002                return Err(Error::InvalidData(
1003                    "public data witness missing siblingPath".into(),
1004                ))
1005            }
1006        };
1007
1008        let mut sibling_path = sibling_path;
1009        sibling_path.resize(aztec_core::constants::PUBLIC_DATA_TREE_HEIGHT, Fr::zero());
1010        sibling_path.truncate(aztec_core::constants::PUBLIC_DATA_TREE_HEIGHT);
1011
1012        Ok(vec![
1013            vec![fr_at(&witness, "/index")?],
1014            vec![fr_at(&witness, "/leafPreimage/leaf/slot")?],
1015            vec![fr_at(&witness, "/leafPreimage/leaf/value")?],
1016            vec![fr_at(&witness, "/leafPreimage/nextKey")?],
1017            vec![fr_at(&witness, "/leafPreimage/nextIndex")?],
1018            sibling_path,
1019        ])
1020    }
1021
1022    /// Return Option<[Field; 13]> with 4 points (x, y, is_infinite) + partial_address.
1023    async fn get_public_keys_and_partial_address(
1024        &self,
1025        args: &[Vec<Fr>],
1026    ) -> Result<Vec<Vec<Fr>>, Error> {
1027        let address = AztecAddress(
1028            *args
1029                .first()
1030                .and_then(|v| v.first())
1031                .ok_or_else(|| Error::InvalidData("missing address arg".into()))?,
1032        );
1033
1034        let Some(complete) = self.address_store.get(&address).await? else {
1035            return Ok(vec![vec![Fr::zero()], vec![Fr::zero(); 13]]);
1036        };
1037
1038        let pk = &complete.public_keys;
1039        let mut fields = Vec::with_capacity(13);
1040        for point in [
1041            &pk.master_nullifier_public_key,
1042            &pk.master_incoming_viewing_public_key,
1043            &pk.master_outgoing_viewing_public_key,
1044            &pk.master_tagging_public_key,
1045        ] {
1046            fields.push(point.x);
1047            fields.push(point.y);
1048            fields.push(Fr::from(point.is_infinite));
1049        }
1050        fields.push(complete.partial_address);
1051        Ok(vec![vec![Fr::from(true)], fields])
1052    }
1053
1054    /// Return `NullifierMembershipWitness` from the node.
1055    ///
1056    /// The Noir struct has 5 deserialization slots:
1057    /// - index (1 field)
1058    /// - leaf_preimage.nullifier (1 field)
1059    /// - leaf_preimage.next_nullifier (1 field)
1060    /// - leaf_preimage.next_index (1 field)
1061    /// - path (NULLIFIER_TREE_HEIGHT = 42 fields)
1062    /// Parse a JSON value as a field element (hex string) or a number.
1063    fn parse_field_or_number(val: Option<&serde_json::Value>) -> Fr {
1064        val.and_then(|v| {
1065            if let Some(s) = v.as_str() {
1066                parse_field_string(s).ok()
1067            } else {
1068                v.as_u64().map(Fr::from)
1069            }
1070        })
1071        .unwrap_or(Fr::zero())
1072    }
1073
1074    async fn get_low_nullifier_membership_witness(
1075        &self,
1076        args: &[Vec<Fr>],
1077    ) -> Result<Vec<Vec<Fr>>, Error> {
1078        let _block_hash = args
1079            .first()
1080            .and_then(|v| v.first())
1081            .copied()
1082            .unwrap_or(Fr::zero());
1083        let nullifier = args.get(1).and_then(|v| v.first()).ok_or_else(|| {
1084            Error::InvalidData("getLowNullifierMembershipWitness: missing nullifier".into())
1085        })?;
1086
1087        let witness_json = self
1088            .node
1089            .get_low_nullifier_membership_witness(0, nullifier)
1090            .await?;
1091
1092        // Parse the JSON response from the node into the 5-slot format
1093        // expected by the Noir `NullifierMembershipWitness` struct.
1094        //
1095        // Node response format:
1096        // { "index": "N", "leafPreimage": { "leaf": { "nullifier": "0x..." },
1097        //   "nextKey": "0x...", "nextIndex": "N" }, "siblingPath": "<base64>" }
1098        if let Some(json) = witness_json {
1099            let index = Self::parse_field_or_number(json.get("index"));
1100
1101            let preimage = json.get("leafPreimage").unwrap_or(&json);
1102            let leaf = preimage.get("leaf").unwrap_or(preimage);
1103            let nullifier_val = leaf
1104                .get("nullifier")
1105                .and_then(|v| v.as_str())
1106                .and_then(|s| Fr::from_hex(s).ok())
1107                .unwrap_or(Fr::zero());
1108            let next_nullifier = preimage
1109                .get("nextKey")
1110                .or_else(|| preimage.get("nextNullifier"))
1111                .and_then(|v| v.as_str())
1112                .and_then(|s| Fr::from_hex(s).ok())
1113                .unwrap_or(Fr::zero());
1114            let next_index = Self::parse_field_or_number(preimage.get("nextIndex"));
1115
1116            // siblingPath is base64-encoded binary: 42 x 32-byte BE field elements
1117            let path = json
1118                .get("siblingPath")
1119                .and_then(|v| v.as_str())
1120                .and_then(|s| decode_base64_sibling_path(s).ok())
1121                .unwrap_or_else(|| vec![Fr::zero(); 42]);
1122
1123            let mut path = path;
1124            path.resize(42, Fr::zero());
1125
1126            Ok(vec![
1127                vec![index],
1128                vec![nullifier_val],
1129                vec![next_nullifier],
1130                vec![next_index],
1131                path,
1132            ])
1133        } else {
1134            // Return zeros if witness not found
1135            Ok(vec![
1136                vec![Fr::zero()],
1137                vec![Fr::zero()],
1138                vec![Fr::zero()],
1139                vec![Fr::zero()],
1140                vec![Fr::zero(); 42],
1141            ])
1142        }
1143    }
1144
1145    /// Return `KeyValidationRequest { pk_m: Point, sk_app: Field }` (4 fields).
1146    ///
1147    /// Enforces scope isolation: only keys belonging to accounts in the
1148    /// current execution scopes are accessible.
1149    async fn get_key_validation_request(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1150        use aztec_core::hash::poseidon2_hash;
1151
1152        let pk_m_hash = *args
1153            .first()
1154            .and_then(|v| v.first())
1155            .ok_or_else(|| Error::InvalidData("missing pk_m_hash".into()))?;
1156
1157        // Check scope: ensure the key owner is within the current scopes
1158        let mut key_in_scope = false;
1159        for scope in &self.scopes {
1160            if let Some(complete) = self.address_store.get(scope).await? {
1161                let pk = &complete.public_keys;
1162                for point in [
1163                    &pk.master_nullifier_public_key,
1164                    &pk.master_incoming_viewing_public_key,
1165                    &pk.master_outgoing_viewing_public_key,
1166                    &pk.master_tagging_public_key,
1167                ] {
1168                    let hash = poseidon2_hash(&[point.x, point.y, Fr::from(point.is_infinite)]);
1169                    if hash == pk_m_hash {
1170                        key_in_scope = true;
1171                        break;
1172                    }
1173                }
1174                if key_in_scope {
1175                    break;
1176                }
1177            }
1178        }
1179        if !key_in_scope {
1180            return Err(Error::InvalidData("Key validation request denied".into()));
1181        }
1182
1183        match self
1184            .key_store
1185            .get_key_validation_request(&pk_m_hash, &self.contract_address)
1186            .await?
1187        {
1188            Some((pk_m, sk_app)) => Ok(vec![
1189                vec![pk_m.x],
1190                vec![pk_m.y],
1191                vec![Fr::from(pk_m.is_infinite)],
1192                vec![sk_app],
1193            ]),
1194            None => Ok(vec![
1195                vec![Fr::zero()],
1196                vec![Fr::zero()],
1197                vec![Fr::zero()],
1198                vec![Fr::zero()],
1199            ]),
1200        }
1201    }
1202
1203    /// Compute shared secret: `address_secret * ephPk`.
1204    async fn get_shared_secret(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1205        use aztec_core::grumpkin;
1206
1207        let recipient = AztecAddress(
1208            *args
1209                .first()
1210                .and_then(|v| v.first())
1211                .ok_or_else(|| Error::InvalidData("getSharedSecret: missing recipient".into()))?,
1212        );
1213        let eph_pk_x = *args
1214            .get(1)
1215            .and_then(|v| v.first())
1216            .ok_or_else(|| Error::InvalidData("getSharedSecret: missing eph_pk.x".into()))?;
1217        let eph_pk_y = *args
1218            .get(2)
1219            .and_then(|v| v.first())
1220            .ok_or_else(|| Error::InvalidData("getSharedSecret: missing eph_pk.y".into()))?;
1221        let eph_pk_is_infinite = args
1222            .get(3)
1223            .and_then(|v| v.first())
1224            .map(|f| f.to_usize() != 0)
1225            .unwrap_or(false);
1226
1227        if eph_pk_is_infinite {
1228            return Ok(vec![
1229                vec![Fr::zero()],
1230                vec![Fr::zero()],
1231                vec![Fr::from(true)],
1232            ]);
1233        }
1234
1235        let Some(complete) = self.address_store.get(&recipient).await? else {
1236            return Err(Error::InvalidData(format!(
1237                "getSharedSecret: recipient {recipient} not in address store"
1238            )));
1239        };
1240        let pk_hash = complete.public_keys.hash();
1241        let Some(ivsk) = self
1242            .key_store
1243            .get_master_incoming_viewing_secret_key(&pk_hash)
1244            .await?
1245        else {
1246            return Err(Error::InvalidData(format!(
1247                "getSharedSecret: ivsk not found for {recipient}"
1248            )));
1249        };
1250
1251        let preaddress = aztec_core::hash::poseidon2_hash_with_separator(
1252            &[pk_hash, complete.partial_address],
1253            aztec_core::constants::domain_separator::CONTRACT_ADDRESS_V1,
1254        );
1255        let address_secret = compute_address_secret(preaddress, ivsk);
1256
1257        let eph_pk = aztec_core::types::Point {
1258            x: eph_pk_x,
1259            y: eph_pk_y,
1260            is_infinite: false,
1261        };
1262        let shared = grumpkin::scalar_mul(&address_secret, &eph_pk);
1263
1264        Ok(vec![
1265            vec![shared.x],
1266            vec![shared.y],
1267            vec![Fr::from(shared.is_infinite)],
1268        ])
1269    }
1270
1271    /// AES-128 CBC decryption oracle.
1272    fn aes128_decrypt(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1273        use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
1274        type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
1275
1276        let ct_storage = args.first().cloned().unwrap_or_default();
1277        let max_len = ct_storage.len();
1278        let ct_len = args
1279            .get(1)
1280            .and_then(|v| v.first())
1281            .map(|f| f.to_usize())
1282            .unwrap_or(0);
1283        let iv_fields = args.get(2).cloned().unwrap_or_default();
1284        let key_fields = args.get(3).cloned().unwrap_or_default();
1285
1286        let ciphertext: Vec<u8> = ct_storage
1287            .iter()
1288            .take(ct_len)
1289            .map(|f| f.to_usize() as u8)
1290            .collect();
1291        let iv: [u8; 16] = iv_fields
1292            .iter()
1293            .take(16)
1294            .map(|f| f.to_usize() as u8)
1295            .collect::<Vec<_>>()
1296            .try_into()
1297            .unwrap_or([0u8; 16]);
1298        let key: [u8; 16] = key_fields
1299            .iter()
1300            .take(16)
1301            .map(|f| f.to_usize() as u8)
1302            .collect::<Vec<_>>()
1303            .try_into()
1304            .unwrap_or([0u8; 16]);
1305
1306        let plaintext = match Aes128CbcDec::new(&key.into(), &iv.into())
1307            .decrypt_padded_vec_mut::<Pkcs7>(&ciphertext)
1308        {
1309            Ok(pt) => pt,
1310            Err(e) => {
1311                return Err(Error::InvalidData(format!("aes128 decrypt error: {e}")));
1312            }
1313        };
1314
1315        let pt_len = plaintext.len();
1316        let mut storage: Vec<Fr> = plaintext.iter().map(|&b| Fr::from(u64::from(b))).collect();
1317        storage.resize(max_len, Fr::zero());
1318        Ok(vec![storage, vec![Fr::from(pt_len as u64)]])
1319    }
1320
1321    fn get_auth_witness(&self, args: &[Vec<Fr>]) -> Result<Vec<Vec<Fr>>, Error> {
1322        let message_hash = args
1323            .first()
1324            .and_then(|v| v.first())
1325            .ok_or_else(|| Error::InvalidData("missing message hash".into()))?;
1326        for (hash, witness) in &self.auth_witnesses {
1327            if hash == message_hash {
1328                return Ok(vec![witness.clone()]);
1329            }
1330        }
1331        Err(Error::InvalidData(format!(
1332            "Unknown auth witness for message hash {message_hash}"
1333        )))
1334    }
1335
1336    fn ensure_contract_db_access(&self, contract_address: &AztecAddress) -> Result<(), Error> {
1337        if *contract_address != self.contract_address {
1338            return Err(Error::InvalidData(format!(
1339                "contract {} is not allowed to access {}'s PXE DB",
1340                contract_address, self.contract_address
1341            )));
1342        }
1343        Ok(())
1344    }
1345
1346    async fn tagging_secrets_for_recipient(
1347        &self,
1348        recipient: &AztecAddress,
1349    ) -> Result<Vec<Fr>, Error> {
1350        let Some(complete_address) = self.address_store.get(recipient).await? else {
1351            return Ok(vec![]);
1352        };
1353
1354        let pk_hash = complete_address.public_keys.hash();
1355        let Some(ivsk) = self
1356            .key_store
1357            .get_master_incoming_viewing_secret_key(&pk_hash)
1358            .await?
1359        else {
1360            return Ok(vec![]);
1361        };
1362
1363        let mut senders = self.sender_store.get_all().await?;
1364        // Also include all addresses in the address store as potential
1365        // senders — registered accounts are skipped by register_sender()
1366        // but can still be senders for tag computation.
1367        for addr in self.address_store.get_all().await? {
1368            if !senders.contains(&addr.address) {
1369                senders.push(addr.address);
1370            }
1371        }
1372        if !senders.contains(recipient) {
1373            senders.push(*recipient);
1374        }
1375
1376        let mut secrets = Vec::with_capacity(senders.len());
1377        for sender in senders {
1378            secrets.push(compute_directional_tagging_secret(
1379                &complete_address,
1380                ivsk,
1381                &sender,
1382                &self.contract_address,
1383                recipient,
1384            )?);
1385        }
1386        Ok(secrets)
1387    }
1388}
1389
1390const MAX_NOTE_PACKED_LEN: usize = 8;
1391const MAX_EVENT_SERIALIZED_LEN: usize = 10;
1392const MAX_NOTE_HASHES_PER_TX: usize = 64;
1393const PRIVATE_LOG_SIZE_IN_FIELDS: usize = aztec_core::constants::PRIVATE_LOG_SIZE_IN_FIELDS;
1394const PRIVATE_LOG_CIPHERTEXT_LEN: usize = 15;
1395
1396#[derive(Debug)]
1397struct ParsedNoteValidationRequest {
1398    contract_address: AztecAddress,
1399    owner: AztecAddress,
1400    storage_slot: Fr,
1401    randomness: Fr,
1402    note_nonce: Fr,
1403    content: Vec<Fr>,
1404    note_hash: Fr,
1405    nullifier: Fr,
1406    #[allow(dead_code)]
1407    tx_hash: TxHash,
1408    recipient: AztecAddress,
1409}
1410
1411#[derive(Debug)]
1412struct ParsedEventValidationRequest {
1413    contract_address: AztecAddress,
1414    event_type_id: aztec_core::abi::EventSelector,
1415    randomness: Fr,
1416    serialized_event: Vec<Fr>,
1417    event_commitment: Fr,
1418    tx_hash: TxHash,
1419    recipient: AztecAddress,
1420}
1421
1422fn parse_log_retrieval_request(fields: Vec<Fr>) -> Result<LogRetrievalRequest, Error> {
1423    if fields.len() < 2 {
1424        return Err(Error::InvalidData("log retrieval request too short".into()));
1425    }
1426    Ok(LogRetrievalRequest {
1427        is_public: true,
1428        contract_address: Some(AztecAddress(fields[0])),
1429        tag: fields[1],
1430    })
1431}
1432
1433fn serialize_bounded_vec(values: &[Fr], max_length: usize) -> Result<Vec<Fr>, Error> {
1434    if values.len() > max_length {
1435        return Err(Error::InvalidData(format!(
1436            "bounded vec overflow: {} > {}",
1437            values.len(),
1438            max_length
1439        )));
1440    }
1441    let mut storage = values.to_vec();
1442    storage.resize(max_length, Fr::zero());
1443    storage.push(Fr::from(values.len() as u64));
1444    Ok(storage)
1445}
1446
1447fn serialize_log_retrieval_option(
1448    log: Option<&crate::sync::log_service::TaggedLog>,
1449) -> Result<Vec<Fr>, Error> {
1450    let mut out = Vec::new();
1451    match log {
1452        Some(log) => {
1453            out.push(Fr::from(true));
1454            let payload = if log.data.is_empty() {
1455                &[][..]
1456            } else {
1457                &log.data[1..]
1458            };
1459            out.extend(serialize_bounded_vec(
1460                payload,
1461                MAX_NOTE_PACKED_LEN.max(PRIVATE_LOG_CIPHERTEXT_LEN),
1462            )?);
1463            out.push(tx_hash_to_field(&log.tx_hash)?);
1464            out.extend(serialize_bounded_vec(
1465                &log.note_hashes,
1466                MAX_NOTE_HASHES_PER_TX,
1467            )?);
1468            out.push(log.first_nullifier);
1469        }
1470        None => {
1471            out.push(Fr::zero());
1472            out.extend(vec![
1473                Fr::zero();
1474                MAX_NOTE_PACKED_LEN.max(PRIVATE_LOG_CIPHERTEXT_LEN) + 1
1475            ]);
1476            out.push(Fr::zero());
1477            out.extend(vec![Fr::zero(); MAX_NOTE_HASHES_PER_TX + 1]);
1478            out.push(Fr::zero());
1479        }
1480    }
1481    Ok(out)
1482}
1483
1484fn serialize_pending_tagged_log(
1485    log: &crate::sync::log_service::TaggedLog,
1486    recipient: &AztecAddress,
1487) -> Result<Vec<Fr>, Error> {
1488    let mut out = serialize_bounded_vec(&log.data, PRIVATE_LOG_SIZE_IN_FIELDS)?;
1489    out.push(tx_hash_to_field(&log.tx_hash)?);
1490    out.extend(serialize_bounded_vec(
1491        &log.note_hashes,
1492        MAX_NOTE_HASHES_PER_TX,
1493    )?);
1494    out.push(log.first_nullifier);
1495    out.push(recipient.0);
1496    Ok(out)
1497}
1498
1499fn parse_note_validation_request(fields: &[Fr]) -> Result<ParsedNoteValidationRequest, Error> {
1500    if fields.len() < 5 + MAX_NOTE_PACKED_LEN + 5 {
1501        return Err(Error::InvalidData(
1502            "note validation request too short".into(),
1503        ));
1504    }
1505    let contract_address = AztecAddress(fields[0]);
1506    let owner = AztecAddress(fields[1]);
1507    let storage_slot = fields[2];
1508    let randomness = fields[3];
1509    let note_nonce = fields[4];
1510    let content_len = fields[5 + MAX_NOTE_PACKED_LEN]
1511        .to_usize()
1512        .min(MAX_NOTE_PACKED_LEN);
1513    let content = fields[5..5 + MAX_NOTE_PACKED_LEN][..content_len].to_vec();
1514    let note_hash = fields[5 + MAX_NOTE_PACKED_LEN + 1];
1515    let nullifier = fields[5 + MAX_NOTE_PACKED_LEN + 2];
1516    let tx_hash = tx_hash_from_field(fields[5 + MAX_NOTE_PACKED_LEN + 3]);
1517    let recipient = AztecAddress(fields[5 + MAX_NOTE_PACKED_LEN + 4]);
1518    Ok(ParsedNoteValidationRequest {
1519        contract_address,
1520        owner,
1521        storage_slot,
1522        randomness,
1523        note_nonce,
1524        content,
1525        note_hash,
1526        nullifier,
1527        tx_hash,
1528        recipient,
1529    })
1530}
1531
1532fn parse_event_validation_request(fields: &[Fr]) -> Result<ParsedEventValidationRequest, Error> {
1533    if fields.len() < 3 + MAX_EVENT_SERIALIZED_LEN + 4 {
1534        return Err(Error::InvalidData(
1535            "event validation request too short".into(),
1536        ));
1537    }
1538    let contract_address = AztecAddress(fields[0]);
1539    let event_type_id = aztec_core::abi::EventSelector(fields[1]);
1540    let randomness = fields[2];
1541    let event_len = fields[3 + MAX_EVENT_SERIALIZED_LEN]
1542        .to_usize()
1543        .min(MAX_EVENT_SERIALIZED_LEN);
1544    let serialized_event = fields[3..3 + MAX_EVENT_SERIALIZED_LEN][..event_len].to_vec();
1545    let event_commitment = fields[3 + MAX_EVENT_SERIALIZED_LEN + 1];
1546    let tx_hash = tx_hash_from_field(fields[3 + MAX_EVENT_SERIALIZED_LEN + 2]);
1547    let recipient = AztecAddress(fields[3 + MAX_EVENT_SERIALIZED_LEN + 3]);
1548    Ok(ParsedEventValidationRequest {
1549        contract_address,
1550        event_type_id,
1551        randomness,
1552        serialized_event,
1553        event_commitment,
1554        tx_hash,
1555        recipient,
1556    })
1557}
1558
1559fn tx_hash_from_field(field: Fr) -> TxHash {
1560    TxHash(field.to_be_bytes())
1561}
1562
1563fn tx_hash_to_field(tx_hash: &TxHash) -> Result<Fr, Error> {
1564    Fr::from_hex(&tx_hash.to_string())
1565}
1566
1567pub(crate) fn compute_directional_tagging_secret(
1568    local_address: &aztec_core::types::CompleteAddress,
1569    local_ivsk: Fq,
1570    external_address: &AztecAddress,
1571    app: &AztecAddress,
1572    recipient: &AztecAddress,
1573) -> Result<Fr, Error> {
1574    let public_keys_hash = local_address.public_keys.hash();
1575    let preaddress = aztec_core::hash::poseidon2_hash_with_separator(
1576        &[public_keys_hash, local_address.partial_address],
1577        domain_separator::CONTRACT_ADDRESS_V1,
1578    );
1579    let address_secret = compute_address_secret(preaddress, local_ivsk);
1580    let external_point = grumpkin::point_from_x(external_address.0)?;
1581    let shared_secret = grumpkin::scalar_mul(&address_secret, &external_point);
1582    let app_tagging_secret = poseidon2_hash(&[shared_secret.x, shared_secret.y, app.0]);
1583    Ok(poseidon2_hash(&[app_tagging_secret, recipient.0]))
1584}
1585
1586fn compute_address_secret(preaddress: Fr, ivsk: Fq) -> Fq {
1587    let candidate = Fq(ivsk.0 + Fq::from_be_bytes_mod_order(&preaddress.to_be_bytes()).0);
1588    let address_point_candidate = grumpkin::scalar_mul(&candidate, &grumpkin::generator());
1589    if grumpkin::has_positive_y(&address_point_candidate) {
1590        candidate
1591    } else {
1592        Fq(-candidate.0)
1593    }
1594}
1595
1596#[async_trait::async_trait]
1597impl<'a, N: AztecNode + Send + Sync + 'static> OracleCallback for UtilityExecutionOracle<'a, N> {
1598    async fn handle_foreign_call(
1599        &mut self,
1600        function: &str,
1601        inputs: Vec<Vec<Fr>>,
1602    ) -> Result<Vec<Vec<Fr>>, Error> {
1603        UtilityExecutionOracle::handle_foreign_call(self, function, inputs).await
1604    }
1605}