From bef65c38c58427325b4481ab794f0fb4f12196b0 Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:39:06 +0000 Subject: [PATCH] feat: add inclusion check l1->l2 (#4141) Fixes https://github.com/AztecProtocol/aztec-packages/issues/1383 - Adding more tests - Adding assertion strings - Remove `root` from oracle response as it is not needed --- .../src/core/libraries/ConstantsGen.sol | 2 +- .../acir-simulator/src/acvm/oracle/oracle.ts | 4 +- .../src/acvm/oracle/typed_oracle.ts | 14 +- .../acir-simulator/src/acvm/serialize.ts | 7 +- .../src/client/private_execution.test.ts | 274 ++++++++++++++--- .../src/client/view_data_oracle.ts | 3 +- .../acir-simulator/src/public/index.test.ts | 287 ++++++++++++++---- .../src/public/public_execution_context.ts | 4 +- yarn-project/aztec-nr/aztec/src/messaging.nr | 25 +- .../aztec/src/messaging/l1_to_l2_message.nr | 6 +- .../messaging/l1_to_l2_message_getter_data.nr | 8 +- .../circuit-types/src/l1_to_l2_message.ts | 5 + yarn-project/circuits.js/src/constants.gen.ts | 2 +- .../src/e2e_cross_chain_messaging.test.ts | 6 +- .../e2e_public_cross_chain_messaging.test.ts | 4 +- .../src/crates/types/src/constants.nr | 2 +- 16 files changed, 502 insertions(+), 151 deletions(-) diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index 4b0aecbc3ce..e8eb5f3c5c0 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -62,7 +62,7 @@ library Constants { uint256 internal constant ARGS_HASH_CHUNK_LENGTH = 32; uint256 internal constant ARGS_HASH_CHUNK_COUNT = 16; uint256 internal constant L1_TO_L2_MESSAGE_LENGTH = 8; - uint256 internal constant L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH = 26; + uint256 internal constant L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH = 25; uint256 internal constant MAX_NOTE_FIELDS_LENGTH = 20; uint256 internal constant GET_NOTE_ORACLE_RETURN_LENGTH = 23; uint256 internal constant MAX_NOTES_PER_PAGE = 10; diff --git a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts index 10a78d70875..020e1738776 100644 --- a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts @@ -232,8 +232,8 @@ export class Oracle { } async getL1ToL2Message([msgKey]: ACVMField[]): Promise { - const { root, ...message } = await this.typedOracle.getL1ToL2Message(fromACVMField(msgKey)); - return toAcvmL1ToL2MessageLoadOracleInputs(message, root); + const { ...message } = await this.typedOracle.getL1ToL2Message(fromACVMField(msgKey)); + return toAcvmL1ToL2MessageLoadOracleInputs(message); } async getPortalContractAddress([aztecAddress]: ACVMField[]): Promise { diff --git a/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts index 621a62db10f..27d9f8d5f30 100644 --- a/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts @@ -48,7 +48,7 @@ export interface NoteData { } /** - * The partial data for L1 to L2 Messages provided by other data sources. + * The data for L1 to L2 Messages provided by other data sources. */ export interface MessageLoadOracleInputs { /** @@ -66,16 +66,6 @@ export interface MessageLoadOracleInputs { index: bigint; } -/** - * The data required by Aztec.nr to validate L1 to L2 Messages. - */ -export interface L1ToL2MessageOracleReturnData extends MessageLoadOracleInputs { - /** - * The current root of the l1 to l2 message tree. - */ - root: Fr; -} - /** * Oracle with typed parameters and typed return values. * Methods that require read and/or write will have to be implemented based on the context (public, private, or view) @@ -167,7 +157,7 @@ export abstract class TypedOracle { throw new Error('Not available.'); } - getL1ToL2Message(_msgKey: Fr): Promise { + getL1ToL2Message(_msgKey: Fr): Promise { throw new Error('Not available.'); } diff --git a/yarn-project/acir-simulator/src/acvm/serialize.ts b/yarn-project/acir-simulator/src/acvm/serialize.ts index 1877bf264d9..5e844ff8a7c 100644 --- a/yarn-project/acir-simulator/src/acvm/serialize.ts +++ b/yarn-project/acir-simulator/src/acvm/serialize.ts @@ -199,18 +199,13 @@ export function toAcvmEnqueuePublicFunctionResult(item: PublicCallRequest): ACVM /** * Converts the result of loading messages to ACVM fields. * @param messageLoadOracleInputs - The result of loading messages to convert. - * @param l1ToL2MessageTreeRoot - The L1 to L2 message tree root * @returns The Message Oracle Fields. */ -export function toAcvmL1ToL2MessageLoadOracleInputs( - messageLoadOracleInputs: MessageLoadOracleInputs, - l1ToL2MessageTreeRoot: Fr, -): ACVMField[] { +export function toAcvmL1ToL2MessageLoadOracleInputs(messageLoadOracleInputs: MessageLoadOracleInputs): ACVMField[] { return [ ...messageLoadOracleInputs.message.map(f => toACVMField(f)), toACVMField(messageLoadOracleInputs.index), ...messageLoadOracleInputs.siblingPath.map(f => toACVMField(f)), - toACVMField(l1ToL2MessageTreeRoot), ]; } diff --git a/yarn-project/acir-simulator/src/client/private_execution.test.ts b/yarn-project/acir-simulator/src/client/private_execution.test.ts index 01b8c34ac75..775433dbeb8 100644 --- a/yarn-project/acir-simulator/src/client/private_execution.test.ts +++ b/yarn-project/acir-simulator/src/client/private_execution.test.ts @@ -1,4 +1,4 @@ -import { Note, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; +import { L1ToL2Message, Note, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; import { BlockHeader, CallContext, @@ -86,7 +86,7 @@ describe('Private Execution test suite', () => { l1ToL2Messages: L1_TO_L2_MSG_TREE_HEIGHT, }; - const trees: { [name: keyof typeof treeHeights]: AppendOnlyTree } = {}; + let trees: { [name: keyof typeof treeHeights]: AppendOnlyTree } = {}; const txContextFields: FieldsOf = { isContractDeploymentTx: false, isFeePaymentTx: false, @@ -143,7 +143,7 @@ describe('Private Execution test suite', () => { await trees[name].appendLeaves(leaves.map(l => l.toBuffer())); // Update root. - const newRoot = trees[name].getRoot(false); + const newRoot = trees[name].getRoot(true); const prevRoots = blockHeader.toBuffer(); const rootIndex = name === 'noteHash' ? 0 : 32 * 3; const newRoots = Buffer.concat([prevRoots.subarray(0, rootIndex), newRoot, prevRoots.subarray(rootIndex + 32)]); @@ -177,6 +177,7 @@ describe('Private Execution test suite', () => { }); beforeEach(() => { + trees = {}; oracle = mock(); oracle.getNullifierKeyPair.mockImplementation((accountAddress: AztecAddress, contractAddress: AztecAddress) => { if (accountAddress.equals(ownerCompleteAddress.address)) { @@ -476,53 +477,248 @@ describe('Private Execution test suite', () => { }); }); - it('Should be able to consume a dummy cross chain message', async () => { - const bridgedAmount = 100n; + describe('L1 to L2', () => { const artifact = getFunctionArtifact(TestContractArtifact, 'consume_mint_private_message'); + const canceller = EthAddress.random(); + let bridgedAmount = 100n; - const secretForL1ToL2MessageConsumption = new Fr(1n); const secretHashForRedeemingNotes = new Fr(2n); - const canceller = EthAddress.random(); - const preimage = buildL1ToL2Message( - getFunctionSelector('mint_private(bytes32,uint256,address)').substring(2), - [secretHashForRedeemingNotes, new Fr(bridgedAmount), canceller.toField()], - contractAddress, - secretForL1ToL2MessageConsumption, - ); + let secretForL1ToL2MessageConsumption = new Fr(1n); - // stub message key - const messageKey = Fr.random(); - const tree = await insertLeaves([messageKey], 'l1ToL2Messages'); + let crossChainMsgRecipient: AztecAddress | undefined; + let crossChainMsgSender: EthAddress | undefined; + let messageKey: Fr | undefined; - oracle.getL1ToL2Message.mockImplementation(async () => { - return Promise.resolve({ - message: preimage.toFieldArray(), - index: 0n, - siblingPath: (await tree.getSiblingPath(0n, false)).toFieldArray(), + let preimage: L1ToL2Message; + + let args: Fr[]; + + beforeEach(() => { + bridgedAmount = 100n; + secretForL1ToL2MessageConsumption = new Fr(2n); + + crossChainMsgRecipient = undefined; + crossChainMsgSender = undefined; + messageKey = undefined; + }); + + const computePreimage = () => + buildL1ToL2Message( + getFunctionSelector('mint_private(bytes32,uint256,address)').substring(2), + [secretHashForRedeemingNotes, new Fr(bridgedAmount), canceller.toField()], + crossChainMsgRecipient ?? contractAddress, + secretForL1ToL2MessageConsumption, + ); + + const computeArgs = () => + encodeArguments(artifact, [ + secretHashForRedeemingNotes, + bridgedAmount, + canceller.toField(), + messageKey ?? preimage.hash(), + secretForL1ToL2MessageConsumption, + ]); + + const mockOracles = async () => { + const tree = await insertLeaves([messageKey ?? preimage.hash()], 'l1ToL2Messages'); + oracle.getL1ToL2Message.mockImplementation(async () => { + return Promise.resolve({ + message: preimage.toFieldArray(), + index: 0n, + siblingPath: (await tree.getSiblingPath(0n, false)).toFieldArray(), + }); }); + }; + + it('Should be able to consume a dummy cross chain message', async () => { + preimage = computePreimage(); + + args = computeArgs(); + + await mockOracles(); + // Update state + oracle.getBlockHeader.mockResolvedValue(blockHeader); + + const result = await runSimulator({ + contractAddress, + artifact, + args, + portalContractAddress: crossChainMsgSender ?? preimage.sender.sender, + txContext: { version: new Fr(1n), chainId: new Fr(1n) }, + }); + + // Check a nullifier has been inserted + const newNullifiers = sideEffectArrayToValueArray( + nonEmptySideEffects(result.callStackItem.publicInputs.newNullifiers), + ); + + expect(newNullifiers).toHaveLength(1); }); - const args = [ - secretHashForRedeemingNotes, - bridgedAmount, - canceller.toField(), - messageKey, - secretForL1ToL2MessageConsumption, - ]; - const result = await runSimulator({ - contractAddress, - artifact, - args, - portalContractAddress: preimage.sender.sender, - txContext: { version: new Fr(1n), chainId: new Fr(1n) }, + it('Message not matching requested key', async () => { + messageKey = Fr.random(); + + preimage = computePreimage(); + + args = computeArgs(); + + await mockOracles(); + // Update state + oracle.getBlockHeader.mockResolvedValue(blockHeader); + + await expect( + runSimulator({ + contractAddress, + artifact, + args, + portalContractAddress: crossChainMsgSender ?? preimage.sender.sender, + txContext: { version: new Fr(1n), chainId: new Fr(1n) }, + }), + ).rejects.toThrowError('Message not matching requested key'); }); - // Check a nullifier has been inserted - const newNullifiers = sideEffectArrayToValueArray( - nonEmptySideEffects(result.callStackItem.publicInputs.newNullifiers), - ); + it('Invalid membership proof', async () => { + preimage = computePreimage(); - expect(newNullifiers).toHaveLength(1); + args = computeArgs(); + + await mockOracles(); + + await expect( + runSimulator({ + contractAddress, + artifact, + args, + portalContractAddress: crossChainMsgSender ?? preimage.sender.sender, + txContext: { version: new Fr(1n), chainId: new Fr(1n) }, + }), + ).rejects.toThrowError('Message not in state'); + }); + + it('Invalid recipient', async () => { + crossChainMsgRecipient = AztecAddress.random(); + + preimage = computePreimage(); + + args = computeArgs(); + + await mockOracles(); + // Update state + oracle.getBlockHeader.mockResolvedValue(blockHeader); + + await expect( + runSimulator({ + contractAddress, + artifact, + args, + portalContractAddress: crossChainMsgSender ?? preimage.sender.sender, + txContext: { version: new Fr(1n), chainId: new Fr(1n) }, + }), + ).rejects.toThrowError('Invalid recipient'); + }); + + it('Invalid sender', async () => { + crossChainMsgSender = EthAddress.random(); + preimage = computePreimage(); + + args = computeArgs(); + + await mockOracles(); + // Update state + oracle.getBlockHeader.mockResolvedValue(blockHeader); + + await expect( + runSimulator({ + contractAddress, + artifact, + args, + portalContractAddress: crossChainMsgSender ?? preimage.sender.sender, + txContext: { version: new Fr(1n), chainId: new Fr(1n) }, + }), + ).rejects.toThrowError('Invalid sender'); + }); + + it('Invalid chainid', async () => { + preimage = computePreimage(); + + args = computeArgs(); + + await mockOracles(); + // Update state + oracle.getBlockHeader.mockResolvedValue(blockHeader); + + await expect( + runSimulator({ + contractAddress, + artifact, + args, + portalContractAddress: crossChainMsgSender ?? preimage.sender.sender, + txContext: { version: new Fr(1n), chainId: new Fr(2n) }, + }), + ).rejects.toThrowError('Invalid Chainid'); + }); + + it('Invalid version', async () => { + preimage = computePreimage(); + + args = computeArgs(); + + await mockOracles(); + // Update state + oracle.getBlockHeader.mockResolvedValue(blockHeader); + + await expect( + runSimulator({ + contractAddress, + artifact, + args, + portalContractAddress: crossChainMsgSender ?? preimage.sender.sender, + txContext: { version: new Fr(2n), chainId: new Fr(1n) }, + }), + ).rejects.toThrowError('Invalid Version'); + }); + + it('Invalid content', async () => { + preimage = computePreimage(); + + bridgedAmount = bridgedAmount + 1n; // Invalid amount + args = computeArgs(); + + await mockOracles(); + // Update state + oracle.getBlockHeader.mockResolvedValue(blockHeader); + + await expect( + runSimulator({ + contractAddress, + artifact, + args, + portalContractAddress: crossChainMsgSender ?? preimage.sender.sender, + txContext: { version: new Fr(1n), chainId: new Fr(1n) }, + }), + ).rejects.toThrowError('Invalid Content'); + }); + + it('Invalid Secret', async () => { + preimage = computePreimage(); + + secretForL1ToL2MessageConsumption = Fr.random(); + args = computeArgs(); + + await mockOracles(); + // Update state + oracle.getBlockHeader.mockResolvedValue(blockHeader); + + await expect( + runSimulator({ + contractAddress, + artifact, + args, + portalContractAddress: crossChainMsgSender ?? preimage.sender.sender, + txContext: { version: new Fr(1n), chainId: new Fr(1n) }, + }), + ).rejects.toThrowError('Invalid message secret'); + }); }); it('Should be able to consume a dummy public to private message', async () => { diff --git a/yarn-project/acir-simulator/src/client/view_data_oracle.ts b/yarn-project/acir-simulator/src/client/view_data_oracle.ts index 3b6128e3221..3fb79b16b6f 100644 --- a/yarn-project/acir-simulator/src/client/view_data_oracle.ts +++ b/yarn-project/acir-simulator/src/client/view_data_oracle.ts @@ -240,8 +240,7 @@ export class ViewDataOracle extends TypedOracle { * @returns The l1 to l2 message data */ public async getL1ToL2Message(msgKey: Fr) { - const message = await this.db.getL1ToL2Message(msgKey); - return { ...message, root: this.blockHeader.l1ToL2MessageTreeRoot }; + return await this.db.getL1ToL2Message(msgKey); } /** diff --git a/yarn-project/acir-simulator/src/public/index.test.ts b/yarn-project/acir-simulator/src/public/index.test.ts index 05d9654ae47..739a4bfcd3e 100644 --- a/yarn-project/acir-simulator/src/public/index.test.ts +++ b/yarn-project/acir-simulator/src/public/index.test.ts @@ -1,3 +1,4 @@ +import { L1ToL2Message } from '@aztec/circuit-types'; import { BlockHeader, CallContext, FunctionData, GlobalVariables, L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/circuits.js'; import { FunctionArtifact, FunctionSelector, encodeArguments } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; @@ -353,69 +354,6 @@ describe('ACIR public execution simulator', () => { expect(result.newL2ToL1Messages[0].toBuffer()).toEqual(expectedNewMessageValue); }); - it('Should be able to consume an L1 to L2 message in the public context', async () => { - const mintPublicArtifact = TestContractArtifact.functions.find(f => f.name === 'consume_mint_public_message')!; - - // Set up cross chain message - const canceller = EthAddress.random(); - - const bridgedAmount = 20n; - const secret = new Fr(1n); - const recipient = AztecAddress.random(); - - const preimage = buildL1ToL2Message( - getFunctionSelector('mint_public(bytes32,uint256,address)').substring(2), - [recipient.toField(), new Fr(bridgedAmount), canceller.toField()], - contractAddress, - secret, - ); - - // Stub message key - const messageKey = Fr.random(); - const args = encodeArguments(mintPublicArtifact, [ - recipient.toField(), - bridgedAmount, - canceller.toField(), - messageKey, - secret, - ]); - - const callContext = CallContext.from({ - msgSender: AztecAddress.random(), - storageContractAddress: contractAddress, - portalContractAddress: preimage.sender.sender, - functionSelector: FunctionSelector.empty(), - isContractDeployment: false, - isDelegateCall: false, - isStaticCall: false, - startSideEffectCounter: 0, - }); - - publicContracts.getBytecode.mockResolvedValue(Buffer.from(mintPublicArtifact.bytecode, 'base64')); - publicState.storageRead.mockResolvedValue(Fr.ZERO); - - // Mock response - commitmentsDb.getL1ToL2Message.mockImplementation(async () => { - return await Promise.resolve({ - message: preimage.toFieldArray(), - index: 0n, - siblingPath: Array(L1_TO_L2_MSG_TREE_HEIGHT).fill(Fr.random()), - }); - }); - - const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - - const gv = new GlobalVariables( - new Fr(preimage.sender.chainId), - new Fr(preimage.recipient.version), - Fr.ZERO, - Fr.ZERO, - ); - const result = await executor.simulate(execution, gv); - - expect(result.newNullifiers.length).toEqual(1); - }); - it('Should be able to create a nullifier from the public context', async () => { const createNullifierPublicArtifact = TestContractArtifact.functions.find( f => f.name === 'create_nullifier_public', @@ -445,5 +383,228 @@ describe('ACIR public execution simulator', () => { const expectedNewMessageValue = pedersenHash(params.map(a => a.toBuffer())); expect(result.newNullifiers[0].value.toBuffer()).toEqual(expectedNewMessageValue); }); + + describe('L1 to L2 messages', () => { + const mintPublicArtifact = TestContractArtifact.functions.find(f => f.name === 'consume_mint_public_message')!; + + const canceller = EthAddress.random(); + const tokenRecipient = AztecAddress.random(); + let bridgedAmount = 20n; + let secret = new Fr(1); + + let crossChainMsgRecipient: AztecAddress | undefined; + let crossChainMsgSender: EthAddress | undefined; + let messageKey: Fr | undefined; + + let preimage: L1ToL2Message; + let globalVariables: GlobalVariables; + + let args: Fr[]; + let callContext: CallContext; + + beforeEach(() => { + bridgedAmount = 20n; + secret = new Fr(1); + + crossChainMsgRecipient = undefined; + crossChainMsgSender = undefined; + messageKey = undefined; + }); + + const computePreImage = () => + buildL1ToL2Message( + getFunctionSelector('mint_public(bytes32,uint256,address)').substring(2), + [tokenRecipient.toField(), new Fr(bridgedAmount), canceller.toField()], + crossChainMsgRecipient ?? contractAddress, + secret, + ); + + const computeArgs = () => + encodeArguments(mintPublicArtifact, [ + tokenRecipient.toField(), + bridgedAmount, + canceller.toField(), + messageKey ?? preimage.hash(), + secret, + ]); + + const computeCallContext = () => + CallContext.from({ + msgSender: AztecAddress.random(), + storageContractAddress: contractAddress, + portalContractAddress: crossChainMsgSender ?? preimage.sender.sender, + functionSelector: FunctionSelector.empty(), + isContractDeployment: false, + isDelegateCall: false, + isStaticCall: false, + startSideEffectCounter: 0, + }); + + const computeGlobalVariables = () => + new GlobalVariables(new Fr(preimage.sender.chainId), new Fr(preimage.recipient.version), Fr.ZERO, Fr.ZERO); + + const mockOracles = () => { + publicContracts.getBytecode.mockResolvedValue(Buffer.from(mintPublicArtifact.bytecode, 'base64')); + publicState.storageRead.mockResolvedValue(Fr.ZERO); + + const siblingPath = Array(L1_TO_L2_MSG_TREE_HEIGHT).fill(Fr.random()); + let root = messageKey ?? preimage.hash(); + for (const sibling of siblingPath) { + root = Fr.fromBuffer(pedersenHash([root.toBuffer(), sibling.toBuffer()])); + } + commitmentsDb.getL1ToL2Message.mockImplementation(async () => { + return await Promise.resolve({ + message: preimage.toFieldArray(), + index: 0n, + siblingPath, + }); + }); + + return root; + }; + + it('Should be able to consume an L1 to L2 message in the public context', async () => { + preimage = computePreImage(); + + args = computeArgs(); + callContext = computeCallContext(); + + // Prepare the state + blockHeader.l1ToL2MessageTreeRoot = mockOracles(); + globalVariables = computeGlobalVariables(); + + const execution: PublicExecution = { contractAddress, functionData, args, callContext }; + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, blockHeader); + const result = await executor.simulate(execution, globalVariables); + expect(result.newNullifiers.length).toEqual(1); + }); + + it('Message not matching requested key', async () => { + // Using a random value for the message key + messageKey = Fr.random(); + + preimage = computePreImage(); + args = computeArgs(); + callContext = computeCallContext(); + + // Prepare the state + blockHeader.l1ToL2MessageTreeRoot = mockOracles(); + globalVariables = computeGlobalVariables(); + + const execution: PublicExecution = { contractAddress, functionData, args, callContext }; + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, blockHeader); + await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError( + 'Message not matching requested key', + ); + }); + + it('Invalid membership proof', async () => { + preimage = computePreImage(); + args = computeArgs(); + callContext = computeCallContext(); + + // Mock oracles but don't update state + mockOracles(); + + // Prepare the state + globalVariables = computeGlobalVariables(); + + const execution: PublicExecution = { contractAddress, functionData, args, callContext }; + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, blockHeader); + await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Message not in state'); + }); + + it('Invalid recipient', async () => { + crossChainMsgRecipient = AztecAddress.random(); + preimage = computePreImage(); + args = computeArgs(); + callContext = computeCallContext(); + + // Prepare the state + blockHeader.l1ToL2MessageTreeRoot = mockOracles(); + globalVariables = computeGlobalVariables(); + + const execution: PublicExecution = { contractAddress, functionData, args, callContext }; + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, blockHeader); + await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid recipient'); + }); + + it('Invalid sender', async () => { + crossChainMsgSender = EthAddress.random(); + preimage = computePreImage(); + args = computeArgs(); + callContext = computeCallContext(); + + // Prepare the state + blockHeader.l1ToL2MessageTreeRoot = mockOracles(); + globalVariables = computeGlobalVariables(); + + const execution: PublicExecution = { contractAddress, functionData, args, callContext }; + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, blockHeader); + await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid sender'); + }); + + it('Invalid chainid', async () => { + preimage = computePreImage(); + args = computeArgs(); + callContext = computeCallContext(); + + // Prepare the state + blockHeader.l1ToL2MessageTreeRoot = mockOracles(); + globalVariables = computeGlobalVariables(); + globalVariables.chainId = Fr.random(); + + const execution: PublicExecution = { contractAddress, functionData, args, callContext }; + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, blockHeader); + await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid Chainid'); + }); + + it('Invalid version', async () => { + preimage = computePreImage(); + args = computeArgs(); + callContext = computeCallContext(); + + // Prepare the state + blockHeader.l1ToL2MessageTreeRoot = mockOracles(); + globalVariables = computeGlobalVariables(); + globalVariables.version = Fr.random(); + + const execution: PublicExecution = { contractAddress, functionData, args, callContext }; + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, blockHeader); + await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid Version'); + }); + + it('Invalid Content', async () => { + preimage = computePreImage(); + + bridgedAmount = bridgedAmount + 1n; // Invalid amount + args = computeArgs(); + callContext = computeCallContext(); + + // Prepare the state + blockHeader.l1ToL2MessageTreeRoot = mockOracles(); + globalVariables = computeGlobalVariables(); + + const execution: PublicExecution = { contractAddress, functionData, args, callContext }; + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, blockHeader); + await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid Content'); + }); + + it('Invalid secret', async () => { + preimage = computePreImage(); + + secret = Fr.random(); // Invalid secret + args = computeArgs(); + callContext = computeCallContext(); + + // Prepare the state + blockHeader.l1ToL2MessageTreeRoot = mockOracles(); + globalVariables = computeGlobalVariables(); + + const execution: PublicExecution = { contractAddress, functionData, args, callContext }; + executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, blockHeader); + await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError('Invalid message secret'); + }); + }); }); }); diff --git a/yarn-project/acir-simulator/src/public/public_execution_context.ts b/yarn-project/acir-simulator/src/public/public_execution_context.ts index 886c0c40434..6b2674886c8 100644 --- a/yarn-project/acir-simulator/src/public/public_execution_context.ts +++ b/yarn-project/acir-simulator/src/public/public_execution_context.ts @@ -102,9 +102,7 @@ export class PublicExecutionContext extends TypedOracle { * @returns The l1 to l2 message data */ public async getL1ToL2Message(msgKey: Fr) { - // l1 to l2 messages in public contexts TODO: https://github.com/AztecProtocol/aztec-packages/issues/616 - const message = await this.commitmentsDb.getL1ToL2Message(msgKey); - return { ...message, root: this.blockHeader.l1ToL2MessageTreeRoot }; + return await this.commitmentsDb.getL1ToL2Message(msgKey); } /** diff --git a/yarn-project/aztec-nr/aztec/src/messaging.nr b/yarn-project/aztec-nr/aztec/src/messaging.nr index ef659f198ec..78dc5d33eec 100644 --- a/yarn-project/aztec-nr/aztec/src/messaging.nr +++ b/yarn-project/aztec-nr/aztec/src/messaging.nr @@ -6,6 +6,8 @@ use l1_to_l2_message_getter_data::make_l1_to_l2_message_getter_data; use crate::abi::PublicContextInputs; use crate::oracle::get_l1_to_l2_message::get_l1_to_l2_message_call; +use dep::std::merkle::compute_merkle_root; + use dep::protocol_types::address::{ AztecAddress, EthAddress, @@ -25,23 +27,32 @@ pub fn process_l1_to_l2_message( let returned_message = get_l1_to_l2_message_call(msg_key); let l1_to_l2_message_data = make_l1_to_l2_message_getter_data(returned_message, 0, secret); - // Check tree roots against the inputs - assert(l1_to_l2_message_data.root == l1_to_l2_root); + // Check that the returned message is actually the message we looked up + let msg_hash = l1_to_l2_message_data.message.hash(); + assert(msg_hash == msg_key, "Message not matching requested key"); + + // Check that the message is in the tree + let root = compute_merkle_root( + msg_hash, + l1_to_l2_message_data.leaf_index, + l1_to_l2_message_data.sibling_path + ); + assert(root == l1_to_l2_root, "Message not in state"); // Validate this is the target contract - assert(l1_to_l2_message_data.message.recipient.eq(storage_contract_address)); + assert(l1_to_l2_message_data.message.recipient.eq(storage_contract_address), "Invalid recipient"); // Validate the sender is the portal contract - assert(l1_to_l2_message_data.message.sender.eq(portal_contract_address)); + assert(l1_to_l2_message_data.message.sender.eq(portal_contract_address), "Invalid sender"); // Validate the chain id is correct - assert(l1_to_l2_message_data.message.chainId == chain_id); + assert(l1_to_l2_message_data.message.chainId == chain_id, "Invalid Chainid"); // Validate the version is correct - assert(l1_to_l2_message_data.message.version == version); + assert(l1_to_l2_message_data.message.version == version, "Invalid Version"); // Validate the message hash is correct - assert(l1_to_l2_message_data.message.content == content); + assert(l1_to_l2_message_data.message.content == content, "Invalid Content"); // Validate the message secret is correct l1_to_l2_message_data.message.validate_message_secret(); diff --git a/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr b/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr index 39aeba68742..a4dfcfa4e52 100644 --- a/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr +++ b/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr @@ -49,10 +49,10 @@ impl L1ToL2Message { pub fn validate_message_secret(self: Self) { let recomputed_hash = pedersen_hash([self.secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET); - assert(self.secret_hash == recomputed_hash); + assert(self.secret_hash == recomputed_hash, "Invalid message secret"); } - fn message_hash(self: Self) -> Field { + fn hash(self: Self) -> Field { let mut hash_bytes: [u8; 256] = [0; 256]; let sender_bytes = self.sender.to_field().to_be_bytes(32); let chainId_bytes = self.chainId.to_be_bytes(32); @@ -81,7 +81,7 @@ impl L1ToL2Message { // The nullifier of a l1 to l2 message is the hash of the message salted with the secret and tree index // docs:start:l1_to_l2_message_compute_nullifier pub fn compute_nullifier(self: Self) -> Field { - let message_hash = self.message_hash(); + let message_hash = self.hash(); pedersen_hash([message_hash, self.secret, self.tree_index], GENERATOR_INDEX__NULLIFIER) } // docs:end:l1_to_l2_message_compute_nullifier diff --git a/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message_getter_data.nr b/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message_getter_data.nr index 882103f7fdf..34f21c60395 100644 --- a/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message_getter_data.nr +++ b/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message_getter_data.nr @@ -8,12 +8,11 @@ use crate::utils::arr_copy_slice; struct L1ToL2MessageGetterData { message: L1ToL2Message, sibling_path: [Field; L1_TO_L2_MSG_TREE_HEIGHT], - leaf_index: Field, - root: Field, + leaf_index: Field } pub fn l1_to_l2_message_getter_len() -> Field { - L1_TO_L2_MESSAGE_LENGTH + 1 + L1_TO_L2_MSG_TREE_HEIGHT + 1 + L1_TO_L2_MESSAGE_LENGTH + 1 + L1_TO_L2_MSG_TREE_HEIGHT } pub fn make_l1_to_l2_message_getter_data( @@ -32,7 +31,6 @@ pub fn make_l1_to_l2_message_getter_data( fields, [0; L1_TO_L2_MSG_TREE_HEIGHT], L1_TO_L2_MESSAGE_LENGTH + 1 - ), - root: fields[start + L1_TO_L2_MESSAGE_LENGTH + L1_TO_L2_MSG_TREE_HEIGHT + 1] + ) } } diff --git a/yarn-project/circuit-types/src/l1_to_l2_message.ts b/yarn-project/circuit-types/src/l1_to_l2_message.ts index a072e6c270b..2165d4bd49e 100644 --- a/yarn-project/circuit-types/src/l1_to_l2_message.ts +++ b/yarn-project/circuit-types/src/l1_to_l2_message.ts @@ -1,5 +1,6 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { toBigIntBE, toBufferBE } from '@aztec/foundation/bigint-buffer'; +import { sha256 } from '@aztec/foundation/crypto'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; @@ -116,6 +117,10 @@ export class L1ToL2Message { return serializeToBuffer(this.sender, this.recipient, this.content, this.secretHash, this.deadline, this.fee); } + hash(): Fr { + return Fr.fromBufferReduce(sha256(serializeToBuffer(...this.toFieldArray()))); + } + static fromBuffer(buffer: Buffer | BufferReader): L1ToL2Message { const reader = BufferReader.asReader(buffer); const sender = reader.readObject(L1Actor); diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index 79cd3856feb..39a05fee9ca 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -48,7 +48,7 @@ export const NUM_FIELDS_PER_SHA256 = 2; export const ARGS_HASH_CHUNK_LENGTH = 32; export const ARGS_HASH_CHUNK_COUNT = 16; export const L1_TO_L2_MESSAGE_LENGTH = 8; -export const L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH = 26; +export const L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH = 25; export const MAX_NOTE_FIELDS_LENGTH = 20; export const GET_NOTE_ORACLE_RETURN_LENGTH = 23; export const MAX_NOTES_PER_PAGE = 10; diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index 4f01c5b41e1..f3bb23e15d8 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -161,7 +161,7 @@ describe('e2e_cross_chain_messaging', () => { secretForL2MessageConsumption, ) .simulate(), - ).rejects.toThrowError("Cannot satisfy constraint 'l1_to_l2_message_data.message.content == content"); + ).rejects.toThrowError("Invalid Content 'l1_to_l2_message_data.message.content == content'"); // send the right one - const consumptionTx = l2Bridge @@ -234,8 +234,6 @@ describe('e2e_cross_chain_messaging', () => { .withWallet(user2Wallet) .methods.claim_public(ownerAddress, bridgeAmount, ethAccount, messageKey, secretForL2MessageConsumption) .simulate(), - ).rejects.toThrowError( - "Failed to solve brillig function, reason: explicit trap hit in brillig 'l1_to_l2_message_data.message.content == content'", - ); + ).rejects.toThrowError("Invalid Content 'l1_to_l2_message_data.message.content == content'"); }, 120_000); }); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index a6975b6b0f8..ea57f757f73 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -136,7 +136,7 @@ describe('e2e_public_cross_chain_messaging', () => { .withWallet(user2Wallet) .methods.claim_public(user2Wallet.getAddress(), bridgeAmount, ethAccount, messageKey, secret) .simulate(), - ).rejects.toThrow(); + ).rejects.toThrow("Invalid Content 'l1_to_l2_message_data.message.content == content'"); // user2 consumes owner's L1-> L2 message on bridge contract and mints public tokens on L2 logger("user2 consumes owner's message on L2 Publicly"); @@ -185,6 +185,6 @@ describe('e2e_public_cross_chain_messaging', () => { .withWallet(user2Wallet) .methods.claim_private(secretHash, bridgeAmount, ethAccount, messageKey, secret) .simulate(), - ).rejects.toThrowError("Cannot satisfy constraint 'l1_to_l2_message_data.message.content == content"); + ).rejects.toThrowError("Invalid Content 'l1_to_l2_message_data.message.content == content'"); }, 60_000); }); diff --git a/yarn-project/noir-protocol-circuits/src/crates/types/src/constants.nr b/yarn-project/noir-protocol-circuits/src/crates/types/src/constants.nr index c0b05053066..ae5fa0d5e19 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/types/src/constants.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/types/src/constants.nr @@ -86,7 +86,7 @@ global ARGS_HASH_CHUNK_COUNT: u32 = 16; // Move these constants to a noir file once the issue bellow is resolved: // https://github.com/noir-lang/noir/issues/1734 global L1_TO_L2_MESSAGE_LENGTH: Field = 8; -global L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH: Field = 26; +global L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH: Field = 25; global MAX_NOTE_FIELDS_LENGTH: Field = 20; // GET_NOTE_ORACLE_RETURN_LENGT = MAX_NOTE_FIELDS_LENGTH + 1 + 2 // The plus 1 is 1 extra field for nonce.