aztec_contract/
authwit.rs

1//! Authorization witness helpers for public authwits and validity checking.
2//!
3//! Provides [`SetPublicAuthWitInteraction`] for setting public authwits in the
4//! AuthRegistry, and [`lookup_validity`] for checking authwit validity in both
5//! private and public contexts.
6
7use aztec_core::abi::{AbiValue, FunctionSelector, FunctionType};
8use aztec_core::constants::protocol_contract_address;
9use aztec_core::error::Error;
10use aztec_core::hash::{
11    compute_auth_wit_message_hash, compute_inner_auth_wit_hash_from_action, ChainInfo,
12    MessageHashOrIntent,
13};
14use aztec_core::tx::{AuthWitness, ExecutionPayload, FunctionCall};
15use aztec_core::types::{AztecAddress, Fr};
16
17use crate::wallet::{
18    ExecuteUtilityOptions, ProfileOptions, SendOptions, SendResult, SimulateOptions,
19    TxProfileResult, TxSimulationResult, Wallet,
20};
21
22// ---------------------------------------------------------------------------
23// SetPublicAuthWitInteraction
24// ---------------------------------------------------------------------------
25
26/// Convenience interaction for setting a public authwit in the AuthRegistry.
27///
28/// Wraps a call to `AuthRegistry.set_authorized(message_hash, authorize)`.
29/// Automatically enforces that only the authorizer (`from`) is the sender.
30///
31/// Mirrors TS `SetPublicAuthwitContractInteraction`.
32pub struct SetPublicAuthWitInteraction<'a, W> {
33    wallet: &'a W,
34    from: AztecAddress,
35    call: FunctionCall,
36}
37
38impl<'a, W: Wallet> SetPublicAuthWitInteraction<'a, W> {
39    /// Create a new interaction for setting a public authwit.
40    ///
41    /// Computes the message hash from the intent and chain info,
42    /// then constructs a call to `AuthRegistry.set_authorized(hash, authorized)`.
43    pub async fn create(
44        wallet: &'a W,
45        from: AztecAddress,
46        message_hash_or_intent: MessageHashOrIntent,
47        authorized: bool,
48    ) -> Result<Self, Error> {
49        let chain_info = wallet.get_chain_info().await?;
50        let message_hash = compute_auth_wit_message_hash(&message_hash_or_intent, &chain_info);
51
52        let call = FunctionCall {
53            to: protocol_contract_address::auth_registry(),
54            selector: FunctionSelector::from_signature("set_authorized(Field,bool)"),
55            args: vec![AbiValue::Field(message_hash), AbiValue::Boolean(authorized)],
56            function_type: FunctionType::Public,
57            is_static: false,
58            hide_msg_sender: false,
59        };
60
61        Ok(Self { wallet, from, call })
62    }
63
64    /// Build the execution payload.
65    pub fn request(&self) -> ExecutionPayload {
66        ExecutionPayload {
67            calls: vec![self.call.clone()],
68            ..Default::default()
69        }
70    }
71
72    /// Simulate the interaction (sender is always `from`).
73    pub async fn simulate(&self, mut opts: SimulateOptions) -> Result<TxSimulationResult, Error> {
74        opts.from = self.from;
75        self.wallet.simulate_tx(self.request(), opts).await
76    }
77
78    /// Send the interaction (sender is always `from`).
79    pub async fn send(&self, mut opts: SendOptions) -> Result<SendResult, Error> {
80        opts.from = self.from;
81        self.wallet.send_tx(self.request(), opts).await
82    }
83
84    /// Profile the interaction (sender is always `from`).
85    pub async fn profile(&self, mut opts: ProfileOptions) -> Result<TxProfileResult, Error> {
86        opts.from = self.from;
87        self.wallet.profile_tx(self.request(), opts).await
88    }
89}
90
91// ---------------------------------------------------------------------------
92// lookup_validity
93// ---------------------------------------------------------------------------
94
95/// Result of an authwit validity check.
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct AuthWitValidity {
98    /// Whether the authwit is valid in private context (signature check).
99    pub is_valid_in_private: bool,
100    /// Whether the authwit is valid in public context (AuthRegistry check).
101    pub is_valid_in_public: bool,
102}
103
104/// Check whether an authorization witness is valid in both private and public contexts.
105///
106/// - **Private:** Simulates a `lookup_validity(consumer, inner_hash)` utility call
107///   on the `on_behalf_of` account contract, passing the witness. If simulation
108///   succeeds and returns `true`, the authwit is valid privately.
109///
110/// - **Public:** Simulates a `utility_is_consumable(address, message_hash)` utility call
111///   on the AuthRegistry protocol contract. If it returns `true`, the authwit is
112///   valid publicly.
113///
114/// Mirrors TS `lookupValidity(wallet, onBehalfOf, intent, witness)`.
115pub async fn lookup_validity<W: Wallet>(
116    wallet: &W,
117    on_behalf_of: &AztecAddress,
118    intent: &MessageHashOrIntent,
119    witness: &AuthWitness,
120) -> Result<AuthWitValidity, Error> {
121    let chain_info = wallet.get_chain_info().await?;
122
123    // Extract inner_hash and consumer from the intent
124    let (inner_hash, consumer) = match intent {
125        MessageHashOrIntent::Intent { caller, call } => {
126            let inner = compute_inner_auth_wit_hash_from_action(caller, call);
127            (inner, call.to)
128        }
129        MessageHashOrIntent::InnerHash {
130            consumer,
131            inner_hash,
132        } => (*inner_hash, *consumer),
133        MessageHashOrIntent::Hash { hash } => {
134            // For raw hashes, we can only check public validity.
135            // Private check requires knowing the consumer, which a raw hash doesn't provide.
136            let is_valid_in_public =
137                check_public_validity(wallet, on_behalf_of, hash, &chain_info).await;
138            return Ok(AuthWitValidity {
139                is_valid_in_private: false,
140                is_valid_in_public,
141            });
142        }
143    };
144
145    // Private validity check
146    let is_valid_in_private =
147        check_private_validity(wallet, on_behalf_of, &consumer, &inner_hash, witness).await;
148
149    // Public validity check
150    let message_hash = compute_auth_wit_message_hash(intent, &chain_info);
151    let is_valid_in_public =
152        check_public_validity(wallet, on_behalf_of, &message_hash, &chain_info).await;
153
154    Ok(AuthWitValidity {
155        is_valid_in_private,
156        is_valid_in_public,
157    })
158}
159
160/// Check private validity by calling `lookup_validity` on the account contract.
161async fn check_private_validity<W: Wallet>(
162    wallet: &W,
163    on_behalf_of: &AztecAddress,
164    consumer: &AztecAddress,
165    inner_hash: &Fr,
166    witness: &AuthWitness,
167) -> bool {
168    let call = FunctionCall {
169        to: *on_behalf_of,
170        selector: FunctionSelector::from_signature("lookup_validity((Field),Field)"),
171        args: vec![AbiValue::Field(consumer.0), AbiValue::Field(*inner_hash)],
172        function_type: FunctionType::Utility,
173        is_static: true,
174        hide_msg_sender: false,
175    };
176
177    let opts = ExecuteUtilityOptions {
178        scope: *on_behalf_of,
179        auth_witnesses: vec![witness.clone()],
180    };
181
182    match wallet.execute_utility(call, opts).await {
183        Ok(result) => parse_boolean_result(&result.result),
184        Err(_) => false,
185    }
186}
187
188/// Check public validity by reading the AuthRegistry's public storage directly.
189///
190/// The `approved_actions` storage in the AuthRegistry is a `Map<AztecAddress, Map<Field, bool>>`.
191/// Storage slot = `poseidon2(poseidon2(base_slot=1, on_behalf_of), message_hash)`.
192async fn check_public_validity<W: Wallet>(
193    wallet: &W,
194    on_behalf_of: &AztecAddress,
195    message_hash: &Fr,
196    _chain_info: &ChainInfo,
197) -> bool {
198    use aztec_core::hash::poseidon2_hash_with_separator;
199
200    // approved_actions is the second storage variable → base slot = 2
201    // (reject_all is at slot 1)
202    let base_slot = Fr::from(2u64);
203    // Map slot derivation uses DOM_SEP__PUBLIC_STORAGE_MAP_SLOT = 4015149901
204    const MAP_SLOT_SEP: u32 = 4_015_149_901;
205    let intermediate = poseidon2_hash_with_separator(&[base_slot, on_behalf_of.0], MAP_SLOT_SEP);
206    let storage_slot = poseidon2_hash_with_separator(&[intermediate, *message_hash], MAP_SLOT_SEP);
207
208    match wallet
209        .get_public_storage_at(&protocol_contract_address::auth_registry(), &storage_slot)
210        .await
211    {
212        Ok(value) => value != Fr::zero(),
213        Err(_) => false,
214    }
215}
216
217/// Parse a boolean from a JSON value returned by utility execution.
218fn parse_boolean_result(value: &serde_json::Value) -> bool {
219    // The result may be a boolean, a number (0/1), a hex-encoded field element,
220    // or an array wrapping one of those.
221    match value {
222        serde_json::Value::Bool(b) => *b,
223        serde_json::Value::Number(n) => n.as_u64() == Some(1),
224        serde_json::Value::String(s) => {
225            // Try to parse as a field element — nonzero means true
226            s != "0x0000000000000000000000000000000000000000000000000000000000000000"
227                && s != "0"
228                && s != "false"
229        }
230        serde_json::Value::Array(arr) => {
231            // Unwrap single-element arrays (common for utility return values)
232            arr.first().map_or(false, parse_boolean_result)
233        }
234        _ => false,
235    }
236}
237
238#[cfg(test)]
239#[allow(clippy::expect_used)]
240mod tests {
241    use super::*;
242    use crate::wallet::MockWallet;
243
244    fn sample_chain_info() -> ChainInfo {
245        ChainInfo {
246            chain_id: Fr::from(31337u64),
247            version: Fr::from(1u64),
248        }
249    }
250
251    #[tokio::test]
252    async fn set_public_auth_wit_targets_auth_registry() {
253        let wallet = MockWallet::new(sample_chain_info());
254        let from = AztecAddress(Fr::from(1u64));
255
256        let interaction = SetPublicAuthWitInteraction::create(
257            &wallet,
258            from,
259            MessageHashOrIntent::Hash {
260                hash: Fr::from(42u64),
261            },
262            true,
263        )
264        .await
265        .expect("create interaction");
266
267        let payload = interaction.request();
268        assert_eq!(payload.calls.len(), 1);
269        assert_eq!(
270            payload.calls[0].to,
271            protocol_contract_address::auth_registry()
272        );
273        assert_eq!(payload.calls[0].function_type, FunctionType::Public);
274    }
275
276    #[tokio::test]
277    async fn set_public_auth_wit_enforces_from() {
278        let wallet = MockWallet::new(sample_chain_info());
279        let from = AztecAddress(Fr::from(1u64));
280
281        let interaction = SetPublicAuthWitInteraction::create(
282            &wallet,
283            from,
284            MessageHashOrIntent::Hash {
285                hash: Fr::from(42u64),
286            },
287            true,
288        )
289        .await
290        .expect("create interaction");
291
292        // When simulating, the `from` address should be overridden
293        let opts = SimulateOptions::default();
294        let _result = interaction.simulate(opts).await.expect("simulate");
295        // MockWallet always succeeds; the key assertion is that it compiled and ran
296    }
297
298    #[tokio::test]
299    async fn set_public_auth_wit_can_profile() {
300        let wallet = MockWallet::new(sample_chain_info());
301        let from = AztecAddress(Fr::from(1u64));
302
303        let interaction = SetPublicAuthWitInteraction::create(
304            &wallet,
305            from,
306            MessageHashOrIntent::Hash {
307                hash: Fr::from(42u64),
308            },
309            true,
310        )
311        .await
312        .expect("create interaction");
313
314        let _result = interaction
315            .profile(ProfileOptions::default())
316            .await
317            .expect("profile");
318    }
319
320    #[tokio::test]
321    async fn lookup_validity_with_hash_returns_false_private() {
322        let wallet = MockWallet::new(sample_chain_info());
323        let on_behalf_of = AztecAddress(Fr::from(1u64));
324        let intent = MessageHashOrIntent::Hash {
325            hash: Fr::from(42u64),
326        };
327        let witness = AuthWitness::default();
328
329        let validity = lookup_validity(&wallet, &on_behalf_of, &intent, &witness)
330            .await
331            .expect("lookup validity");
332
333        // Raw hash can't be checked privately (no consumer info)
334        assert!(!validity.is_valid_in_private);
335        // MockWallet returns Null for utility execution, which parses as false
336        assert!(!validity.is_valid_in_public);
337    }
338
339    #[tokio::test]
340    async fn lookup_validity_with_intent() {
341        let wallet = MockWallet::new(sample_chain_info());
342        let on_behalf_of = AztecAddress(Fr::from(1u64));
343        let caller = AztecAddress(Fr::from(2u64));
344        let call = FunctionCall {
345            to: AztecAddress(Fr::from(3u64)),
346            selector: FunctionSelector::from_hex("0xaabbccdd").expect("valid"),
347            args: vec![AbiValue::Field(Fr::from(100u64))],
348            function_type: FunctionType::Private,
349            is_static: false,
350            hide_msg_sender: false,
351        };
352        let intent = MessageHashOrIntent::Intent { caller, call };
353        let witness = AuthWitness::default();
354
355        let validity = lookup_validity(&wallet, &on_behalf_of, &intent, &witness)
356            .await
357            .expect("lookup validity");
358
359        // MockWallet returns Null, so both should be false
360        assert!(!validity.is_valid_in_private);
361        assert!(!validity.is_valid_in_public);
362    }
363
364    #[test]
365    fn parse_boolean_result_variants() {
366        assert!(parse_boolean_result(&serde_json::Value::Bool(true)));
367        assert!(!parse_boolean_result(&serde_json::Value::Bool(false)));
368        assert!(parse_boolean_result(&serde_json::json!(1)));
369        assert!(!parse_boolean_result(&serde_json::json!(0)));
370        assert!(!parse_boolean_result(&serde_json::Value::Null));
371    }
372}