aztec_account/
schnorr.rs

1//! Schnorr account contract — a real account implementation with Schnorr signing.
2//!
3//! This is the Rust equivalent of the TS `SchnorrAccountContract`. It uses
4//! the Grumpkin-based Schnorr signature scheme for transaction authorization
5//! and routes calls through the `DefaultAccountEntrypoint`.
6
7use async_trait::async_trait;
8
9use aztec_core::abi::{
10    AbiParameter, AbiType, AbiValue, ContractArtifact, FunctionArtifact, FunctionType,
11};
12use aztec_core::fee::GasSettings;
13use aztec_core::hash::{compute_auth_wit_message_hash, ChainInfo};
14use aztec_core::tx::{AuthWitness, ExecutionPayload};
15use aztec_core::types::{AztecAddress, CompleteAddress, Fr, Point};
16use aztec_core::Error;
17
18use crate::entrypoint::account_entrypoint::AccountFeePaymentMethodOptions;
19use aztec_crypto::keys::{derive_public_key_from_secret_key, derive_signing_key};
20use aztec_crypto::schnorr::schnorr_sign;
21
22use crate::account::{
23    Account, AccountContract, AuthorizationProvider, EntrypointOptions, TxExecutionRequest,
24};
25use crate::entrypoint::{DefaultAccountEntrypoint, DefaultAccountEntrypointOptions};
26use crate::wallet::{ChainInfo as WalletChainInfo, MessageHashOrIntent};
27
28fn resolve_message_hash(intent: &MessageHashOrIntent, chain_info: &WalletChainInfo) -> Fr {
29    match intent {
30        MessageHashOrIntent::Hash { hash } => *hash,
31        MessageHashOrIntent::Intent { .. } | MessageHashOrIntent::InnerHash { .. } => {
32            let core_chain_info = ChainInfo {
33                chain_id: chain_info.chain_id,
34                version: chain_info.version,
35            };
36            compute_auth_wit_message_hash(intent, &core_chain_info)
37        }
38    }
39}
40
41// ---------------------------------------------------------------------------
42// SchnorrAuthorizationProvider
43// ---------------------------------------------------------------------------
44
45/// Authorization provider that signs messages with Schnorr on Grumpkin.
46pub struct SchnorrAuthorizationProvider {
47    signing_key: aztec_core::types::GrumpkinScalar,
48}
49
50#[async_trait]
51impl AuthorizationProvider for SchnorrAuthorizationProvider {
52    async fn create_auth_wit(
53        &self,
54        intent: MessageHashOrIntent,
55        chain_info: &WalletChainInfo,
56    ) -> Result<AuthWitness, Error> {
57        let message_hash = resolve_message_hash(&intent, chain_info);
58        let signature = schnorr_sign(&self.signing_key, &message_hash);
59
60        Ok(AuthWitness {
61            request_hash: message_hash,
62            fields: signature.to_fields(),
63        })
64    }
65}
66
67// ---------------------------------------------------------------------------
68// SchnorrAccount
69// ---------------------------------------------------------------------------
70
71/// An account that uses Schnorr signing and the default account entrypoint.
72pub struct SchnorrAccount {
73    address: CompleteAddress,
74    signing_key: aztec_core::types::GrumpkinScalar,
75    entrypoint: DefaultAccountEntrypoint,
76}
77
78#[async_trait]
79impl AuthorizationProvider for SchnorrAccount {
80    async fn create_auth_wit(
81        &self,
82        intent: MessageHashOrIntent,
83        chain_info: &WalletChainInfo,
84    ) -> Result<AuthWitness, Error> {
85        let message_hash = resolve_message_hash(&intent, chain_info);
86        let signature = schnorr_sign(&self.signing_key, &message_hash);
87
88        Ok(AuthWitness {
89            request_hash: message_hash,
90            fields: signature.to_fields(),
91        })
92    }
93}
94
95#[async_trait]
96impl Account for SchnorrAccount {
97    fn complete_address(&self) -> &CompleteAddress {
98        &self.address
99    }
100
101    fn address(&self) -> AztecAddress {
102        self.address.address
103    }
104
105    async fn create_tx_execution_request(
106        &self,
107        exec: ExecutionPayload,
108        gas_settings: GasSettings,
109        chain_info: &WalletChainInfo,
110        options: EntrypointOptions,
111    ) -> Result<TxExecutionRequest, Error> {
112        let core_chain_info = ChainInfo {
113            chain_id: chain_info.chain_id,
114            version: chain_info.version,
115        };
116        let mut ep_opts = DefaultAccountEntrypointOptions::default();
117        if let Some(method) = options.fee_payment_method {
118            ep_opts.fee_payment_method_options = match method {
119                1 => AccountFeePaymentMethodOptions::PreexistingFeeJuice,
120                2 => AccountFeePaymentMethodOptions::FeeJuiceWithClaim,
121                _ => AccountFeePaymentMethodOptions::External,
122            };
123        }
124        self.entrypoint
125            .create_tx_execution_request(exec, gas_settings, &core_chain_info, &ep_opts)
126            .await
127    }
128
129    async fn wrap_execution_payload(
130        &self,
131        exec: ExecutionPayload,
132        _options: EntrypointOptions,
133    ) -> Result<ExecutionPayload, Error> {
134        let chain_info = ChainInfo {
135            chain_id: Fr::from(0u64),
136            version: Fr::from(0u64),
137        };
138        self.entrypoint
139            .wrap_execution_payload(
140                exec,
141                &chain_info,
142                &DefaultAccountEntrypointOptions::default(),
143            )
144            .await
145    }
146}
147
148// ---------------------------------------------------------------------------
149// SchnorrAccountContract
150// ---------------------------------------------------------------------------
151
152/// A Schnorr-based account contract.
153///
154/// This is the primary account contract implementation, equivalent to the TS
155/// `SchnorrAccountContract`. It uses a Grumpkin signing key for Schnorr
156/// signatures and routes transactions through the `DefaultAccountEntrypoint`.
157///
158/// # Example
159///
160/// ```ignore
161/// use aztec_rs::account::{AccountManager, SchnorrAccountContract};
162/// use aztec_rs::types::Fr;
163/// use aztec_rs::wallet::{ChainInfo, MockWallet};
164///
165/// # async fn example() -> Result<(), aztec_rs::Error> {
166/// let wallet = MockWallet::new(ChainInfo {
167///     chain_id: Fr::from(31337u64),
168///     version: Fr::from(1u64),
169/// });
170/// let secret_key = Fr::from(12345u64);
171/// let contract = SchnorrAccountContract::new(secret_key);
172/// let manager = AccountManager::create(wallet, secret_key, Box::new(contract), None::<Fr>).await?;
173/// # Ok(())
174/// # }
175/// ```
176pub struct SchnorrAccountContract {
177    secret_key: Fr,
178    signing_key: aztec_core::types::GrumpkinScalar,
179    signing_public_key: Point,
180}
181
182impl SchnorrAccountContract {
183    /// Create a new Schnorr account contract from a secret key.
184    ///
185    /// Derives the signing key pair from the secret key using the standard
186    /// Aztec key derivation.
187    pub fn new(secret_key: Fr) -> Self {
188        let signing_key = derive_signing_key(&secret_key);
189        let signing_public_key = derive_public_key_from_secret_key(&signing_key);
190        Self {
191            secret_key,
192            signing_key,
193            signing_public_key,
194        }
195    }
196
197    /// Create a new Schnorr account contract with an explicit signing key.
198    ///
199    /// Unlike [`new`](Self::new), this does **not** derive the signing key
200    /// from the secret key. This is the Rust equivalent of the TS
201    /// `new SchnorrAccountContract(signingKey)` + `getSchnorrAccountContractAddress(secret, salt, signingKey)`.
202    ///
203    /// Use this when multiple accounts share the same encryption key (secret)
204    /// but have different signing keys.
205    pub fn new_with_signing_key(
206        secret_key: Fr,
207        signing_key: aztec_core::types::GrumpkinScalar,
208    ) -> Self {
209        let signing_public_key = derive_public_key_from_secret_key(&signing_key);
210        Self {
211            secret_key,
212            signing_key,
213            signing_public_key,
214        }
215    }
216
217    /// Returns the Schnorr signing public key.
218    pub fn signing_public_key(&self) -> &Point {
219        &self.signing_public_key
220    }
221
222    /// Returns the secret key used for key derivation.
223    pub fn secret_key(&self) -> Fr {
224        self.secret_key
225    }
226
227    fn constructor_artifact() -> FunctionArtifact {
228        // Parameters must match the compiled Noir artifact exactly (flat fields,
229        // not a struct) so that the computed selector matches the one stored in
230        // the PXE's compiled artifact.
231        FunctionArtifact {
232            name: "constructor".to_owned(),
233            function_type: FunctionType::Private,
234            is_initializer: true,
235            is_static: false,
236            parameters: vec![
237                AbiParameter {
238                    name: "signing_pub_key_x".to_owned(),
239                    typ: AbiType::Field,
240                    visibility: None,
241                },
242                AbiParameter {
243                    name: "signing_pub_key_y".to_owned(),
244                    typ: AbiType::Field,
245                    visibility: None,
246                },
247            ],
248            return_types: vec![],
249            selector: None,
250            bytecode: None,
251            verification_key_hash: None,
252            verification_key: None,
253            custom_attributes: None,
254            is_unconstrained: None,
255            debug_symbols: None,
256            error_types: None,
257            is_only_self: None,
258        }
259    }
260}
261
262use crate::account::InitializationSpec;
263
264#[async_trait]
265impl AccountContract for SchnorrAccountContract {
266    async fn contract_artifact(&self) -> Result<ContractArtifact, Error> {
267        let entrypoint_abi = DefaultAccountEntrypoint::entrypoint_abi();
268
269        Ok(ContractArtifact {
270            name: "SchnorrAccount".to_owned(),
271            functions: vec![Self::constructor_artifact(), entrypoint_abi],
272            outputs: None,
273            file_map: None,
274            context_inputs_sizes: None,
275        })
276    }
277
278    async fn initialization_function_and_args(&self) -> Result<Option<InitializationSpec>, Error> {
279        Ok(Some(InitializationSpec {
280            constructor_name: "constructor".to_owned(),
281            constructor_args: vec![
282                AbiValue::Field(self.signing_public_key.x),
283                AbiValue::Field(self.signing_public_key.y),
284            ],
285        }))
286    }
287
288    fn account(&self, address: CompleteAddress) -> Box<dyn Account> {
289        let auth = self.auth_witness_provider(address.clone());
290        let entrypoint = DefaultAccountEntrypoint::new(address.address, auth);
291        Box::new(SchnorrAccount {
292            address,
293            signing_key: self.signing_key,
294            entrypoint,
295        })
296    }
297
298    fn auth_witness_provider(&self, _address: CompleteAddress) -> Box<dyn AuthorizationProvider> {
299        Box::new(SchnorrAuthorizationProvider {
300            signing_key: self.signing_key,
301        })
302    }
303}
304
305#[cfg(test)]
306#[allow(clippy::unwrap_used, clippy::expect_used)]
307mod tests {
308    use super::*;
309    use crate::account::AccountManager;
310    use crate::wallet::MockWallet;
311    use aztec_core::abi::{FunctionSelector, FunctionType};
312    use aztec_core::tx::FunctionCall;
313    use aztec_core::types::AztecAddress;
314
315    fn sample_chain_info() -> WalletChainInfo {
316        WalletChainInfo {
317            chain_id: Fr::from(31337u64),
318            version: Fr::from(1u64),
319        }
320    }
321
322    #[test]
323    fn new_derives_keys() {
324        let contract = SchnorrAccountContract::new(Fr::from(12345u64));
325        let pk = contract.signing_public_key();
326        assert!(!pk.is_zero());
327        assert!(!pk.is_infinite);
328    }
329
330    #[tokio::test]
331    async fn contract_artifact_has_constructor_and_entrypoint() {
332        let contract = SchnorrAccountContract::new(Fr::from(12345u64));
333        let artifact = contract.contract_artifact().await.expect("artifact");
334        assert_eq!(artifact.name, "SchnorrAccount");
335        assert_eq!(artifact.functions.len(), 2);
336        assert_eq!(artifact.functions[0].name, "constructor");
337        assert!(artifact.functions[0].is_initializer);
338        assert_eq!(artifact.functions[1].name, "entrypoint");
339    }
340
341    #[tokio::test]
342    async fn initialization_spec_contains_public_key() {
343        let contract = SchnorrAccountContract::new(Fr::from(12345u64));
344        let spec = contract
345            .initialization_function_and_args()
346            .await
347            .expect("init spec")
348            .expect("should have spec");
349        assert_eq!(spec.constructor_name, "constructor");
350        assert_eq!(spec.constructor_args.len(), 2);
351    }
352
353    #[tokio::test]
354    async fn auth_provider_creates_real_signature() {
355        let contract = SchnorrAccountContract::new(Fr::from(12345u64));
356        let addr = CompleteAddress::default();
357        let provider = contract.auth_witness_provider(addr);
358        let chain_info = sample_chain_info();
359
360        let wit = provider
361            .create_auth_wit(
362                MessageHashOrIntent::Hash {
363                    hash: Fr::from(42u64),
364                },
365                &chain_info,
366            )
367            .await
368            .expect("create auth wit");
369
370        // Real Schnorr signature: 64 fields (one per byte)
371        assert_eq!(wit.fields.len(), 64);
372        // request_hash should match input hash (Hash variant is passthrough)
373        assert_eq!(wit.request_hash, Fr::from(42u64));
374    }
375
376    #[tokio::test]
377    async fn signature_is_verifiable() {
378        let secret = Fr::from(8923u64);
379        let contract = SchnorrAccountContract::new(secret);
380        let addr = CompleteAddress::default();
381        let provider = contract.auth_witness_provider(addr);
382        let chain_info = sample_chain_info();
383
384        let message = Fr::from(999u64);
385        let wit = provider
386            .create_auth_wit(MessageHashOrIntent::Hash { hash: message }, &chain_info)
387            .await
388            .expect("create auth wit");
389
390        // Reconstruct the signature bytes from fields
391        let sig_bytes: Vec<u8> = wit.fields.iter().map(|f| f.to_usize() as u8).collect();
392        let mut sig_arr = [0u8; 64];
393        sig_arr.copy_from_slice(&sig_bytes);
394        let sig = aztec_crypto::schnorr::SchnorrSignature::from_bytes(&sig_arr);
395
396        // Verify against the public key
397        let pk = contract.signing_public_key();
398        assert!(aztec_crypto::schnorr::schnorr_verify(pk, &message, &sig));
399    }
400
401    #[tokio::test]
402    async fn intent_variant_is_hashed_before_signing() {
403        let secret = Fr::from(4242u64);
404        let contract = SchnorrAccountContract::new(secret);
405        let provider = contract.auth_witness_provider(CompleteAddress::default());
406        let chain_info = sample_chain_info();
407        let intent = MessageHashOrIntent::Intent {
408            caller: AztecAddress::from(1u64),
409            call: FunctionCall {
410                to: AztecAddress::from(2u64),
411                selector: FunctionSelector::from_hex("0x11223344").expect("valid selector"),
412                args: vec![AbiValue::Field(Fr::from(7u64))],
413                function_type: FunctionType::Private,
414                is_static: false,
415                hide_msg_sender: false,
416            },
417        };
418
419        let wit = provider
420            .create_auth_wit(intent.clone(), &chain_info)
421            .await
422            .expect("create auth wit");
423
424        let expected = compute_auth_wit_message_hash(
425            &intent,
426            &ChainInfo {
427                chain_id: chain_info.chain_id,
428                version: chain_info.version,
429            },
430        );
431        assert_eq!(wit.request_hash, expected);
432    }
433
434    #[tokio::test]
435    async fn account_manager_integration() {
436        let wallet = MockWallet::new(sample_chain_info());
437        let secret = Fr::from(12345u64);
438        let contract = SchnorrAccountContract::new(secret);
439
440        let manager = AccountManager::create(wallet, secret, Box::new(contract), None::<Fr>)
441            .await
442            .expect("create manager");
443
444        assert_ne!(manager.address(), AztecAddress::zero());
445        assert!(manager.has_initializer());
446
447        let account = manager.account().await.expect("get account");
448        assert_eq!(account.address(), manager.address());
449    }
450
451    #[tokio::test]
452    async fn deploy_method_builds_payload() {
453        let wallet = MockWallet::new(sample_chain_info());
454        let secret = Fr::from(12345u64);
455        let contract = SchnorrAccountContract::new(secret);
456
457        let manager = AccountManager::create(wallet, secret, Box::new(contract), None::<Fr>)
458            .await
459            .expect("create manager");
460
461        let deploy = manager.deploy_method().await.expect("deploy method");
462        let opts = crate::account::DeployAccountOptions {
463            skip_registration: true,
464            ..Default::default()
465        };
466        let payload = deploy.request(&opts).await.expect("deploy payload");
467        assert!(!payload.calls.is_empty());
468    }
469}