diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/helpers.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/helpers.nr index b7ebba92e7d7..71e4341f8afa 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/helpers.nr @@ -91,8 +91,20 @@ pub(crate) comptime fn create_authorize_once_check(f: FunctionDefinition, is_pri let nonce_check_quote = f"{nonce_arg_name_quoted} == 0".quoted_contents(); let fn_call = if is_private { - let args_len = f.parameters().len(); - quote { aztec::authwit::auth::assert_current_call_valid_authwit::<$args_len> } + let params = f.parameters(); + let serialized_len_quote = if params.len() == 0 { + quote { 0 } + } else { + let params_quote_without_parentheses = params + .map(|(_, param_type): (Quoted, Type)| { + quote { + <$param_type as aztec::protocol::traits::Serialize>::N + } + }) + .join(quote {+}); + quote { ($params_quote_without_parentheses) } + }; + quote { aztec::authwit::auth::assert_current_call_valid_authwit::<($serialized_len_quote)> } } else { quote { aztec::authwit::auth::assert_current_call_valid_authwit_public } }; diff --git a/noir-projects/noir-contracts/contracts/test/auth_wit_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/auth_wit_test_contract/src/main.nr index aad212668b38..6f78d6b58c3a 100644 --- a/noir-projects/noir-contracts/contracts/test/auth_wit_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/auth_wit_test_contract/src/main.nr @@ -4,10 +4,17 @@ use aztec::macros::aztec; pub contract AuthWitTest { use aztec::{ authwit::auth::{assert_inner_hash_valid_authwit, assert_inner_hash_valid_authwit_public}, - macros::functions::external, - protocol::address::AztecAddress, + macros::functions::{authorize_once, external}, + protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}, }; + #[derive(Serialize, Deserialize)] + pub struct MultiFieldStruct { + pub a: Field, + pub b: Field, + pub c: Field, + } + #[external("private")] fn consume(on_behalf_of: AztecAddress, inner_hash: Field) { assert_inner_hash_valid_authwit(self.context, on_behalf_of, inner_hash); @@ -17,4 +24,16 @@ pub contract AuthWitTest { fn consume_public(on_behalf_of: AztecAddress, inner_hash: Field) { assert_inner_hash_valid_authwit_public(self.context, on_behalf_of, inner_hash); } + + /// Regression test for #[authorize_once] with a struct parameter that serializes to 3 fields. + /// The macro must emit assert_current_call_valid_authwit::<6> + /// (from:1 + data:3 + amount:1 + nonce:1), not ::<4> (parameter count). + #[authorize_once("from", "authwit_nonce")] + #[external("private")] + fn auth_with_struct( + from: AztecAddress, + _data: MultiFieldStruct, + _amount: Field, + authwit_nonce: Field, + ) {} } diff --git a/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts b/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts index 13d359ff5b46..ee283d050d64 100644 --- a/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts +++ b/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts @@ -5,12 +5,15 @@ import type { Logger } from '@aztec/aztec.js/log'; import type { AztecNode } from '@aztec/aztec.js/node'; import { AMMContract } from '@aztec/noir-contracts.js/AMM'; import { type TokenContract, TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; +import { AuthWitTestContract, AuthWitTestContractArtifact } from '@aztec/noir-test-contracts.js/AuthWitTest'; +import { GenericProxyContract } from '@aztec/noir-test-contracts.js/GenericProxy'; import { PendingNoteHashesContract } from '@aztec/noir-test-contracts.js/PendingNoteHashes'; import { type AbiDecoded, decodeFromAbi, getFunctionArtifact } from '@aztec/stdlib/abi'; import { computeOuterAuthWitHash } from '@aztec/stdlib/auth-witness'; import { jest } from '@jest/globals'; +import { simulateThroughAuthwitProxy } from './fixtures/authwit_proxy.js'; import { deployToken, mintTokensToPrivate } from './fixtures/token_utils.js'; import { setup } from './fixtures/utils.js'; import type { TestWallet } from './test-wallet/test_wallet.js'; @@ -328,6 +331,63 @@ describe('Kernelless simulation', () => { }); }); + describe('authorize_once with multi-field struct parameters', () => { + let authWitTestContract: AuthWitTestContract; + let proxy: GenericProxyContract; + + beforeAll(async () => { + [{ contract: authWitTestContract }, { contract: proxy }] = await Promise.all([ + AuthWitTestContract.deploy(wallet).send({ from: adminAddress }), + GenericProxyContract.deploy(wallet).send({ from: adminAddress }), + ]); + }); + + it('emits offchain effect with correct serialized args length for struct parameters', async () => { + const structData = { a: Fr.random(), b: Fr.random(), c: Fr.random() }; + const amount = Fr.random(); + const nonce = Fr.random(); + + // This function uses a struct with 3 fields as parameter, and the #[authorize_once] macro should correctly + // account for this and emit a CallAuthorizationRequest with the correct serialized length, rather than + // just the arguments length + const interaction = authWitTestContract.methods.auth_with_struct(adminAddress, structData, amount, nonce); + + wallet.setSimulationMode('kernelless-override'); + const { offchainEffects } = await simulateThroughAuthwitProxy(proxy, interaction, { + from: adminAddress, + includeMetadata: true, + }); + + expect(offchainEffects.length).toBe(1); + + const callAuthRequest = await CallAuthorizationRequest.fromFields(offchainEffects[0].data); + + expect(offchainEffects[0].contractAddress).toEqual(authWitTestContract.address); + + // The macro should emit 6 arguments in total before decoding: from (1) + struct (3) + amount (1) + nonce (1) + expect(callAuthRequest.args).toHaveLength(6); + + expect(callAuthRequest.onBehalfOf).toEqual(adminAddress); + expect(callAuthRequest.msgSender).toEqual(proxy.address); + + const functionAbi = await getFunctionArtifact(AuthWitTestContractArtifact, callAuthRequest.functionSelector); + const decodedArgs = decodeFromAbi( + functionAbi.parameters.map(param => param.type), + callAuthRequest.args, + ) as AbiDecoded[]; + + expect(decodedArgs).toHaveLength(4); + expect(decodedArgs[0]).toEqual(adminAddress); + expect(decodedArgs[1]).toEqual({ + a: structData.a.toBigInt(), + b: structData.b.toBigInt(), + c: structData.c.toBigInt(), + }); + expect(decodedArgs[2]).toEqual(amount.toBigInt()); + expect(decodedArgs[3]).toEqual(nonce.toBigInt()); + }); + }); + describe('read request verification', () => { let pendingNoteHashesContract: PendingNoteHashesContract; diff --git a/yarn-project/end-to-end/src/fixtures/authwit_proxy.ts b/yarn-project/end-to-end/src/fixtures/authwit_proxy.ts index 044d7f9ae34e..a98ecaf26082 100644 --- a/yarn-project/end-to-end/src/fixtures/authwit_proxy.ts +++ b/yarn-project/end-to-end/src/fixtures/authwit_proxy.ts @@ -17,6 +17,10 @@ async function buildProxyCall(proxy: GenericProxyContract, action: ContractFunct return proxy.methods.forward_private_3(call.to, call.selector, call.args); } else if (argCount === 4) { return proxy.methods.forward_private_4(call.to, call.selector, call.args); + } else if (argCount === 5) { + return proxy.methods.forward_private_5(call.to, call.selector, call.args); + } else if (argCount === 6) { + return proxy.methods.forward_private_6(call.to, call.selector, call.args); } throw new Error(`No forward_private_${argCount} method on proxy`); }