diff --git a/docs/docs/developers/debugging/sandbox-errors.md b/docs/docs/developers/debugging/sandbox-errors.md index 911654026b6..a67ecc3c0c8 100644 --- a/docs/docs/developers/debugging/sandbox-errors.md +++ b/docs/docs/developers/debugging/sandbox-errors.md @@ -177,7 +177,7 @@ Users may create a proof against a historical state in Aztec. The rollup circuit ## Archiver Errors -- "No L1 to L2 message found for message hash ${messageHash.toString()}" - happens when the L1 to L2 message doesn't exist or is "pending", when the user has sent a message on L1 via the Inbox contract but it has yet to be included in an L2 block by the sequencer - user has to wait for enough blocks to progress and for the archiver to sync the respective L2 block. You can get the sequencer to pick it up by doing 2 arbitrary transaction on L2 (eg. send DAI to yourself 2 times). This would give the sequencer a transaction to process and as a side effect it would consume 2 subtrees of new messages from the Inbox contract. 2 subtrees needs to be consumed and not just 1 because there is a 1 block lag to prevent the subtree from changing when the sequencer is proving. +- "No non-nullified L1 to L2 message found for message hash ${messageHash.toString()}" - happens when the L1 to L2 message doesn't exist or is "pending", when the user has sent a message on L1 via the Inbox contract but it has yet to be included in an L2 block by the sequencer - the user has to wait for enough blocks to progress and for the archiver to sync the respective L2 block. You can get the sequencer to pick it up by doing 2 arbitrary transactions on L2 (eg. send DAI to yourself 2 times). This would give the sequencer a transaction to process and as a side effect it would consume 2 subtrees of new messages from the Inbox contract. 2 subtrees need to be consumed and not just 1 because there is a 1 block lag to prevent the subtree from changing when the sequencer is proving. - "Block number mismatch: expected ${l2BlockNum} but got ${block.number}" - The archiver keeps track of the next expected L2 block number. It throws this error if it got a different one when trying to sync with the rollup contract's events on L1. diff --git a/noir-projects/aztec-nr/aztec/src/context/private_context.nr b/noir-projects/aztec-nr/aztec/src/context/private_context.nr index c556c184207..181e37e4aa7 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -4,8 +4,7 @@ use crate::{ oracle::{ arguments, call_private_function::call_private_function_internal, enqueue_public_function_call::enqueue_public_function_call_internal, context::get_portal_address, - header::get_header_at, nullifier_key::{get_nullifier_key_pair, NullifierKeyPair}, - debug_log::debug_log + header::get_header_at, nullifier_key::{get_nullifier_key_pair, NullifierKeyPair} } }; use dep::protocol_types::{ diff --git a/noir-projects/aztec-nr/aztec/src/context/public_context.nr b/noir-projects/aztec-nr/aztec/src/context/public_context.nr index 38b9b17e067..dfca49a3695 100644 --- a/noir-projects/aztec-nr/aztec/src/context/public_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/public_context.nr @@ -233,6 +233,8 @@ impl PublicContextInterface for PublicContext { self.new_l2_to_l1_msgs.push(message); } + // We can consume message with a secret in public context because the message cannot be modified and therefore + // there is no front-running risk (e.g. somebody could front run you to claim your tokens to your address). fn consume_l1_to_l2_message(&mut self, content: Field, secret: Field, sender: EthAddress) { let this = (*self).this_address(); let nullifier = process_l1_to_l2_message( diff --git a/noir-projects/aztec-nr/aztec/src/messaging.nr b/noir-projects/aztec-nr/aztec/src/messaging.nr index 4cfe82baa0c..6e394367709 100644 --- a/noir-projects/aztec-nr/aztec/src/messaging.nr +++ b/noir-projects/aztec-nr/aztec/src/messaging.nr @@ -15,7 +15,7 @@ pub fn process_l1_to_l2_message( content: Field, secret: Field ) -> Field { - let msg = L1ToL2Message::new( + let mut msg = L1ToL2Message::new( portal_contract_address, chain_id, storage_contract_address, @@ -25,7 +25,7 @@ pub fn process_l1_to_l2_message( ); let message_hash = msg.hash(); - let returned_message = get_l1_to_l2_membership_witness(message_hash); + let returned_message = get_l1_to_l2_membership_witness(storage_contract_address, message_hash, secret); let leaf_index = returned_message[0]; let sibling_path = arr_copy_slice(returned_message, [0; L1_TO_L2_MSG_TREE_HEIGHT], 1); @@ -34,5 +34,12 @@ pub fn process_l1_to_l2_message( let root = compute_merkle_root(message_hash, leaf_index, sibling_path); assert(root == l1_to_l2_root, "Message not in state"); + // Note: Had to add this line to make it work (wasted an hour debugging this). L1ToL2Message noir struct is + // an abomination and it would not have ever got to that state if people followed the logical rule of + // "deserialize(serialize(object)) == object" always being true (there are a few params which are not + // serialzied). Will refactor that in a separate PR. + // TODO(#5420) + msg.tree_index = leaf_index; + msg.compute_nullifier() } diff --git a/noir-projects/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr b/noir-projects/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr index 102989a456a..6fd9a65e60c 100644 --- a/noir-projects/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr +++ b/noir-projects/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr @@ -39,8 +39,8 @@ impl L1ToL2Message { pub fn deserialize( fields: [Field; L1_TO_L2_MESSAGE_LENGTH], - secret: Field, - tree_index: Field + secret: Field, // TODO(#5420): refactor this so that this param does not have to be passed separately + tree_index: Field // TODO(#5420): refactor this so that this param does not have to be passed separately ) -> L1ToL2Message { L1ToL2Message { sender: EthAddress::from_field(fields[0]), diff --git a/noir-projects/aztec-nr/aztec/src/oracle/get_l1_to_l2_membership_witness.nr b/noir-projects/aztec-nr/aztec/src/oracle/get_l1_to_l2_membership_witness.nr index 0f9e98eeaa9..b22916ea2c6 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/get_l1_to_l2_membership_witness.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/get_l1_to_l2_membership_witness.nr @@ -1,9 +1,12 @@ -use dep::protocol_types::constants::L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH; +use dep::protocol_types::{ + address::AztecAddress, + constants::L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH, +}; // Obtains membership witness (index and sibling path) for a message in the L1 to L2 message tree. #[oracle(getL1ToL2MembershipWitness)] -fn get_l1_to_l2_membership_witness_oracle(_message_hash: Field) -> [Field; L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH] {} +fn get_l1_to_l2_membership_witness_oracle(_contract_address: AztecAddress, _message_hash: Field, _secret: Field) -> [Field; L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH] {} -unconstrained pub fn get_l1_to_l2_membership_witness(message_hash: Field) -> [Field; L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH] { - get_l1_to_l2_membership_witness_oracle(message_hash) +unconstrained pub fn get_l1_to_l2_membership_witness(contract_address: AztecAddress, message_hash: Field, secret: Field) -> [Field; L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH] { + get_l1_to_l2_membership_witness_oracle(contract_address, message_hash, secret) } diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index b1c5771abf4..ebf8df6d178 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -476,12 +476,13 @@ export class Archiver implements ArchiveSource { } /** - * Gets the L1 to L2 message index in the L1 to L2 message tree. + * Gets the first L1 to L2 message index in the L1 to L2 message tree which is greater than or equal to `startIndex`. * @param l1ToL2Message - The L1 to L2 message. + * @param startIndex - The index to start searching from. * @returns The index of the L1 to L2 message in the L1 to L2 message tree (undefined if not found). */ - getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise { - return this.store.getL1ToL2MessageIndex(l1ToL2Message); + getL1ToL2MessageIndex(l1ToL2Message: Fr, startIndex: bigint): Promise { + return this.store.getL1ToL2MessageIndex(l1ToL2Message, startIndex); } getContractClassIds(): Promise { diff --git a/yarn-project/archiver/src/archiver/archiver_store.ts b/yarn-project/archiver/src/archiver/archiver_store.ts index 8cdd20fe369..6072172b5e4 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.ts @@ -108,11 +108,12 @@ export interface ArchiverDataStore { getL1ToL2Messages(blockNumber: bigint): Promise; /** - * Gets the L1 to L2 message index in the L1 to L2 message tree. + * Gets the first L1 to L2 message index in the L1 to L2 message tree which is greater than or equal to `startIndex`. * @param l1ToL2Message - The L1 to L2 message. + * @param startIndex - The index to start searching from. * @returns The index of the L1 to L2 message in the L1 to L2 message tree (undefined if not found). */ - getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise; + getL1ToL2MessageIndex(l1ToL2Message: Fr, startIndex: bigint): Promise; /** * Gets up to `limit` amount of logs starting from `from`. diff --git a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts index e817bf8bbcd..056e783f2bb 100644 --- a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts +++ b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts @@ -211,6 +211,20 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch await store.addL1ToL2Messages({ lastProcessedL1BlockNumber: 100n, retrievedData: msgs }); }).rejects.toThrow(`Message index ${l1ToL2MessageSubtreeSize} out of subtree range`); }); + + it('correctly handles duplicate messages', async () => { + const messageHash = Fr.random(); + + const msgs = [new InboxLeaf(1n, 0n, messageHash), new InboxLeaf(2n, 0n, messageHash)]; + + await store.addL1ToL2Messages({ lastProcessedL1BlockNumber: 100n, retrievedData: msgs }); + + const index1 = (await store.getL1ToL2MessageIndex(messageHash, 0n))!; + const index2 = await store.getL1ToL2MessageIndex(messageHash, index1 + 1n); + + expect(index2).toBeDefined(); + expect(index2).toBeGreaterThan(index1); + }); }); describe('contractInstances', () => { diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts index 8955e8a5d1d..b084d3940db 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts @@ -167,12 +167,13 @@ export class KVArchiverDataStore implements ArchiverDataStore { } /** - * Gets the L1 to L2 message index in the L1 to L2 message tree. + * Gets the first L1 to L2 message index in the L1 to L2 message tree which is greater than or equal to `startIndex`. * @param l1ToL2Message - The L1 to L2 message. + * @param startIndex - The index to start searching from. * @returns The index of the L1 to L2 message in the L1 to L2 message tree (undefined if not found). */ - public getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise { - return Promise.resolve(this.#messageStore.getL1ToL2MessageIndex(l1ToL2Message)); + getL1ToL2MessageIndex(l1ToL2Message: Fr, startIndex: bigint): Promise { + return Promise.resolve(this.#messageStore.getL1ToL2MessageIndex(l1ToL2Message, startIndex)); } /** diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/message_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/message_store.ts index 3d3b5ca57a4..e85a36e2df6 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/message_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/message_store.ts @@ -15,7 +15,7 @@ import { DataRetrieval } from '../data_retrieval.js'; */ export class MessageStore { #l1ToL2Messages: AztecMap; - #l1ToL2MessageIndices: AztecMap; + #l1ToL2MessageIndices: AztecMap; // We store array of bigints here because there can be duplicate messages #lastL1BlockMessages: AztecSingleton; #log = createDebugLogger('aztec:archiver:message_store'); @@ -60,7 +60,10 @@ export class MessageStore { const indexInTheWholeTree = (message.blockNumber - BigInt(INITIAL_L2_BLOCK_NUM)) * BigInt(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP) + message.index; - void this.#l1ToL2MessageIndices.setIfNotExists(message.leaf.toString(), indexInTheWholeTree); + + const indices = this.#l1ToL2MessageIndices.get(message.leaf.toString()) ?? []; + indices.push(indexInTheWholeTree); + void this.#l1ToL2MessageIndices.set(message.leaf.toString(), indices); } return true; @@ -68,12 +71,14 @@ export class MessageStore { } /** - * Gets the L1 to L2 message index in the L1 to L2 message tree. + * Gets the first L1 to L2 message index in the L1 to L2 message tree which is greater than or equal to `startIndex`. * @param l1ToL2Message - The L1 to L2 message. + * @param startIndex - The index to start searching from. * @returns The index of the L1 to L2 message in the L1 to L2 message tree (undefined if not found). */ - public getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise { - const index = this.#l1ToL2MessageIndices.get(l1ToL2Message.toString()); + getL1ToL2MessageIndex(l1ToL2Message: Fr, startIndex: bigint): Promise { + const indices = this.#l1ToL2MessageIndices.get(l1ToL2Message.toString()) ?? []; + const index = indices.find(i => i >= startIndex); return Promise.resolve(index); } diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/l1_to_l2_message_store.test.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/l1_to_l2_message_store.test.ts index 1625483c0f3..e246dec3bd1 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/l1_to_l2_message_store.test.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/l1_to_l2_message_store.test.ts @@ -25,9 +25,22 @@ describe('l1_to_l2_message_store', () => { expect(retrievedMsgs.length).toEqual(10); const msg = msgs[4]; - const index = store.getMessageIndex(msg.leaf); + const index = store.getMessageIndex(msg.leaf, 0n); expect(index).toEqual( (blockNumber - BigInt(INITIAL_L2_BLOCK_NUM)) * BigInt(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP) + msg.index, ); }); + + it('correctly handles duplicate messages', () => { + const messageHash = Fr.random(); + + store.addMessage(new InboxLeaf(1n, 0n, messageHash)); + store.addMessage(new InboxLeaf(2n, 0n, messageHash)); + + const index1 = store.getMessageIndex(messageHash, 0n)!; + const index2 = store.getMessageIndex(messageHash, index1 + 1n); + + expect(index2).toBeDefined(); + expect(index2).toBeGreaterThan(index1); + }); }); diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/l1_to_l2_message_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/l1_to_l2_message_store.ts index cec550cec18..e311e1b8ed3 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/l1_to_l2_message_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/l1_to_l2_message_store.ts @@ -49,17 +49,21 @@ export class L1ToL2MessageStore { } /** - * Gets the L1 to L2 message index in the L1 to L2 message tree. + * Gets the first L1 to L2 message index in the L1 to L2 message tree which is greater than or equal to `startIndex`. * @param l1ToL2Message - The L1 to L2 message. + * @param startIndex - The index to start searching from. * @returns The index of the L1 to L2 message in the L1 to L2 message tree (undefined if not found). */ - getMessageIndex(l1ToL2Message: Fr): bigint | undefined { + getMessageIndex(l1ToL2Message: Fr, startIndex: bigint): bigint | undefined { for (const [key, message] of this.store.entries()) { if (message.equals(l1ToL2Message)) { - const [blockNumber, messageIndex] = key.split('-'); + const keyParts = key.split('-'); + const [blockNumber, messageIndex] = [BigInt(keyParts[0]), BigInt(keyParts[1])]; const indexInTheWholeTree = - (BigInt(blockNumber) - BigInt(INITIAL_L2_BLOCK_NUM)) * BigInt(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP) + - BigInt(messageIndex); + (blockNumber - BigInt(INITIAL_L2_BLOCK_NUM)) * BigInt(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP) + messageIndex; + if (indexInTheWholeTree < startIndex) { + continue; + } return indexInTheWholeTree; } } diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts index 0e6461bf77c..0efc82081c4 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts @@ -213,12 +213,13 @@ export class MemoryArchiverStore implements ArchiverDataStore { } /** - * Gets the L1 to L2 message index in the L1 to L2 message tree. + * Gets the first L1 to L2 message index in the L1 to L2 message tree which is greater than or equal to `startIndex`. * @param l1ToL2Message - The L1 to L2 message. + * @param startIndex - The index to start searching from. * @returns The index of the L1 to L2 message in the L1 to L2 message tree (undefined if not found). */ - public getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise { - return Promise.resolve(this.l1ToL2Messages.getMessageIndex(l1ToL2Message)); + getL1ToL2MessageIndex(l1ToL2Message: Fr, startIndex: bigint): Promise { + return Promise.resolve(this.l1ToL2Messages.getMessageIndex(l1ToL2Message, startIndex)); } /** diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 657668e07e4..03966222617 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -33,6 +33,7 @@ import { L2_TO_L1_MESSAGE_LENGTH, NOTE_HASH_TREE_HEIGHT, NULLIFIER_TREE_HEIGHT, + NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, NullifierLeafPreimage, PUBLIC_DATA_TREE_HEIGHT, PublicDataTreeLeafPreimage, @@ -388,13 +389,15 @@ export class AztecNodeService implements AztecNode { * Returns the index and a sibling path for a leaf in the committed l1 to l2 data tree. * @param blockNumber - The block number at which to get the data. * @param l1ToL2Message - The l1ToL2Message to get the index / sibling path for. + * @param startIndex - The index to start searching from (used when skipping nullified messages) * @returns A tuple of the index and the sibling path of the L1ToL2Message (undefined if not found). */ public async getL1ToL2MessageMembershipWitness( blockNumber: L2BlockNumber, l1ToL2Message: Fr, + startIndex = 0n, ): Promise<[bigint, SiblingPath] | undefined> { - const index = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message); + const index = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message, startIndex); if (index === undefined) { return undefined; } @@ -409,10 +412,15 @@ export class AztecNodeService implements AztecNode { /** * Returns whether an L1 to L2 message is synced by archiver and if it's ready to be included in a block. * @param l1ToL2Message - The L1 to L2 message to check. + * @param startL2BlockNumber - The block number after which we are interested in checking if the message was + * included. + * @remarks We pass in the minL2BlockNumber because there can be duplicate messages and the block number allow us + * to skip the duplicates (we know after which block a given message is to be included). * @returns Whether the message is synced and ready to be included in a block. */ - public async isL1ToL2MessageSynced(l1ToL2Message: Fr): Promise { - return (await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message)) !== undefined; + public async isL1ToL2MessageSynced(l1ToL2Message: Fr, startL2BlockNumber = INITIAL_L2_BLOCK_NUM): Promise { + const startIndex = BigInt(startL2BlockNumber - INITIAL_L2_BLOCK_NUM) * BigInt(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP); + return (await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message, startIndex)) !== undefined; } /** diff --git a/yarn-project/circuit-types/src/interfaces/aztec-node.ts b/yarn-project/circuit-types/src/interfaces/aztec-node.ts index 738692cf30a..29196841761 100644 --- a/yarn-project/circuit-types/src/interfaces/aztec-node.ts +++ b/yarn-project/circuit-types/src/interfaces/aztec-node.ts @@ -62,19 +62,25 @@ export interface AztecNode { * Returns the index and a sibling path for a leaf in the committed l1 to l2 data tree. * @param blockNumber - The block number at which to get the data. * @param l1ToL2Message - The l1ToL2Message to get the index / sibling path for. - * @returns A tuple of the index and the sibling path of the message (undefined if not found). + * @param startIndex - The index to start searching from. + * @returns A tuple of the index and the sibling path of the L1ToL2Message (undefined if not found). */ getL1ToL2MessageMembershipWitness( blockNumber: L2BlockNumber, l1ToL2Message: Fr, + startIndex: bigint, ): Promise<[bigint, SiblingPath] | undefined>; /** * Returns whether an L1 to L2 message is synced by archiver and if it's ready to be included in a block. * @param l1ToL2Message - The L1 to L2 message to check. + * @param startL2BlockNumber - The block number after which we are interested in checking if the message was + * included. + * @remarks We pass in the minL2BlockNumber because there can be duplicate messages and the block number allow us + * to skip the duplicates (we know after which block a given message is to be included). * @returns Whether the message is synced and ready to be included in a block. */ - isL1ToL2MessageSynced(l1ToL2Message: Fr): Promise; + isL1ToL2MessageSynced(l1ToL2Message: Fr, startL2BlockNumber: number): Promise; /** * Returns a membership witness of an l2ToL1Message in an ephemeral l2 to l1 message tree. diff --git a/yarn-project/circuit-types/src/messaging/l1_to_l2_message_source.ts b/yarn-project/circuit-types/src/messaging/l1_to_l2_message_source.ts index 7ba544e5af5..090726a671d 100644 --- a/yarn-project/circuit-types/src/messaging/l1_to_l2_message_source.ts +++ b/yarn-project/circuit-types/src/messaging/l1_to_l2_message_source.ts @@ -12,11 +12,12 @@ export interface L1ToL2MessageSource { getL1ToL2Messages(blockNumber: bigint): Promise; /** - * Gets the L1 to L2 message index in the L1 to L2 message tree. + * Gets the first L1 to L2 message index in the L1 to L2 message tree which is greater than or equal to `startIndex`. * @param l1ToL2Message - The L1 to L2 message. + * @param startIndex - The index to start searching from. * @returns The index of the L1 to L2 message in the L1 to L2 message tree (undefined if not found). */ - getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise; + getL1ToL2MessageIndex(l1ToL2Message: Fr, startIndex: bigint): Promise; /** * Gets the number of the latest L2 block processed by the implementation. diff --git a/yarn-project/circuits.js/src/hash/hash.ts b/yarn-project/circuits.js/src/hash/hash.ts index 3ead475e6f1..21cad66ffe4 100644 --- a/yarn-project/circuits.js/src/hash/hash.ts +++ b/yarn-project/circuits.js/src/hash/hash.ts @@ -173,3 +173,13 @@ export function computeNullifierHash(input: SideEffectLinkedToNoteHash) { export function computeMessageSecretHash(secretMessage: Fr) { return pedersenHash([secretMessage.toBuffer()], GeneratorIndex.L1_TO_L2_MESSAGE_SECRET); } + +export function computeL1ToL2MessageNullifier( + contract: AztecAddress, + messageHash: Fr, + secret: Fr, + messageIndex: bigint, +) { + const innerMessageNullifier = pedersenHash([messageHash, secret, messageIndex], GeneratorIndex.NULLIFIER); + return siloNullifier(contract, innerMessageNullifier); +} 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 0ceaeffb7d5..fe46f29a2b3 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 @@ -176,7 +176,7 @@ describe('e2e_cross_chain_messaging', () => { .withWallet(user2Wallet) .methods.claim_private(secretHashForL2MessageConsumption, bridgeAmount, secretForL2MessageConsumption) .simulate(), - ).rejects.toThrow(`No L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); + ).rejects.toThrow(`No non-nullified L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); // send the right one - const consumptionReceipt = await l2Bridge @@ -254,6 +254,6 @@ describe('e2e_cross_chain_messaging', () => { .withWallet(user2Wallet) .methods.claim_public(ownerAddress, bridgeAmount, secretForL2MessageConsumption) .simulate(), - ).rejects.toThrow(`Message ${wrongMessage.hash().toString()} not found`); + ).rejects.toThrow(`No non-nullified L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); }, 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 e1d8ee5c24e..713c29eefaa 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 @@ -168,7 +168,7 @@ describe('e2e_public_cross_chain_messaging', () => { // user2 tries to consume this message and minting to itself -> should fail since the message is intended to be consumed only by owner. await expect( l2Bridge.withWallet(user2Wallet).methods.claim_public(user2Wallet.getAddress(), bridgeAmount, secret).simulate(), - ).rejects.toThrow(`Message ${wrongMessage.hash().toString()} not found`); + ).rejects.toThrow(`No non-nullified L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); // 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"); @@ -219,7 +219,7 @@ describe('e2e_public_cross_chain_messaging', () => { await expect( l2Bridge.withWallet(user2Wallet).methods.claim_private(secretHash, bridgeAmount, secret).simulate(), - ).rejects.toThrow(`No L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); + ).rejects.toThrow(`No non-nullified L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); }, 60_000); // Note: We register one portal address when deploying contract but that address is no-longer the only address @@ -314,9 +314,14 @@ describe('e2e_public_cross_chain_messaging', () => { // Note: We register one portal address when deploying contract but that address is no-longer the only address // allowed to send messages to the given contract. In the following test we'll test that it's really the case. it.each([true, false])( - 'can send an L1 -> L2 message from a non-registered portal address consumed from private or public', + 'can send an L1 -> L2 message from a non-registered portal address consumed from private or public and then sends and claims exactly the same message again', async (isPrivate: boolean) => { const testContract = await TestContract.deploy(user1Wallet).send().deployed(); + + const consumeMethod = isPrivate + ? testContract.methods.consume_message_from_arbitrary_sender_private + : testContract.methods.consume_message_from_arbitrary_sender_public; + const secret = Fr.random(); const message = new L1ToL2Message( @@ -326,54 +331,69 @@ describe('e2e_public_cross_chain_messaging', () => { computeMessageSecretHash(secret), // secretHash ); - // We inject the message to Inbox - const txHash = await inbox.write.sendL2Message( - [ - { actor: message.recipient.recipient.toString() as Hex, version: 1n }, - message.content.toString() as Hex, - message.secretHash.toString() as Hex, - ] as const, - {} as any, - ); + await sendL2Message(message); - // We check that the message was correctly injected by checking the emitted event - const msgHash = message.hash(); - { - const txReceipt = await crossChainTestHarness.publicClient.waitForTransactionReceipt({ - hash: txHash, - }); - - // Exactly 1 event should be emitted in the transaction - expect(txReceipt.logs.length).toBe(1); - - // We decode the event and get leaf out of it - const txLog = txReceipt.logs[0]; - const topics = decodeEventLog({ - abi: InboxAbi, - data: txLog.data, - topics: txLog.topics, - }); - const receivedMsgHash = topics.args.hash; - - // We check that the leaf inserted into the subtree matches the expected message hash - expect(receivedMsgHash).toBe(msgHash.toString()); - } + const [message1Index, _1] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message.hash(), 0n))!; - await crossChainTestHarness.makeMessageConsumable(msgHash); + // Finally, we consume the L1 -> L2 message using the test contract either from private or public + await consumeMethod(message.content, secret, message.sender.sender).send().wait(); - // Finally, e consume the L1 -> L2 message using the test contract either from private or public - if (isPrivate) { - await testContract.methods - .consume_message_from_arbitrary_sender_private(message.content, secret, message.sender.sender) - .send() - .wait(); - } else { - await testContract.methods - .consume_message_from_arbitrary_sender_public(message.content, secret, message.sender.sender) - .send() - .wait(); - } + // We send and consume the exact same message the second time to test that oracles correctly return the new + // non-nullified message + await sendL2Message(message); + + // We check that the duplicate message was correctly inserted by checking that its message index is defined and + // larger than the previous message index + const [message2Index, _2] = (await aztecNode.getL1ToL2MessageMembershipWitness( + 'latest', + message.hash(), + message1Index + 1n, + ))!; + + expect(message2Index).toBeDefined(); + expect(message2Index).toBeGreaterThan(message1Index); + + // Now we consume the message again. Everything should pass because oracle should return the duplicate message + // which is not nullified + await consumeMethod(message.content, secret, message.sender.sender).send().wait(); }, - 60_000, + 120_000, ); + + const sendL2Message = async (message: L1ToL2Message) => { + // We inject the message to Inbox + const txHash = await inbox.write.sendL2Message( + [ + { actor: message.recipient.recipient.toString() as Hex, version: 1n }, + message.content.toString() as Hex, + message.secretHash.toString() as Hex, + ] as const, + {} as any, + ); + + // We check that the message was correctly injected by checking the emitted event + const msgHash = message.hash(); + { + const txReceipt = await crossChainTestHarness.publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + + // Exactly 1 event should be emitted in the transaction + expect(txReceipt.logs.length).toBe(1); + + // We decode the event and get leaf out of it + const txLog = txReceipt.logs[0]; + const topics = decodeEventLog({ + abi: InboxAbi, + data: txLog.data, + topics: txLog.topics, + }); + const receivedMsgHash = topics.args.hash; + + // We check that the leaf inserted into the subtree matches the expected message hash + expect(receivedMsgHash).toBe(msgHash.toString()); + } + + await crossChainTestHarness.makeMessageConsumable(msgHash); + }; }); diff --git a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts index feed0f4a2ea..47b49a52d86 100644 --- a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts @@ -459,8 +459,13 @@ export class CrossChainTestHarness { * it's included it becomes available for consumption in the next block because the l1 to l2 message tree. */ async makeMessageConsumable(msgHash: Fr) { + const currentL2BlockNumber = await this.aztecNode.getBlockNumber(); // We poll isL1ToL2MessageSynced endpoint until the message is available - await retryUntil(async () => await this.aztecNode.isL1ToL2MessageSynced(msgHash), 'message sync', 10); + await retryUntil( + async () => await this.aztecNode.isL1ToL2MessageSynced(msgHash, currentL2BlockNumber), + 'message sync', + 10, + ); await this.mintTokensPublicOnL2(0n); await this.mintTokensPublicOnL2(0n); diff --git a/yarn-project/foundation/src/crypto/pedersen/pedersen.wasm.ts b/yarn-project/foundation/src/crypto/pedersen/pedersen.wasm.ts index dbe457a2971..c7a90507001 100644 --- a/yarn-project/foundation/src/crypto/pedersen/pedersen.wasm.ts +++ b/yarn-project/foundation/src/crypto/pedersen/pedersen.wasm.ts @@ -1,6 +1,7 @@ import { BarretenbergSync, Fr as FrBarretenberg } from '@aztec/bb.js'; import { Fr } from '../../fields/fields.js'; +import { Bufferable, serializeToBufferArray } from '../../serialize/serialize.js'; /** * Create a pedersen commitment (point) from an array of input fields. @@ -21,16 +22,17 @@ export function pedersenCommit(input: Buffer[]) { * Create a pedersen hash (field) from an array of input fields. * Left pads any inputs less than 32 bytes. */ -export function pedersenHash(input: Buffer[], index = 0): Fr { - if (!input.every(i => i.length <= 32)) { +export function pedersenHash(input: Bufferable[], index = 0): Fr { + let bufferredInput = serializeToBufferArray(input); + if (!bufferredInput.every(i => i.length <= 32)) { throw new Error('All Pedersen Hash input buffers must be <= 32 bytes.'); } - input = input.map(i => (i.length < 32 ? Buffer.concat([Buffer.alloc(32 - i.length, 0), i]) : i)); + bufferredInput = bufferredInput.map(i => (i.length < 32 ? Buffer.concat([Buffer.alloc(32 - i.length, 0), i]) : i)); return Fr.fromBuffer( Buffer.from( BarretenbergSync.getSingleton() .pedersenHash( - input.map(i => new FrBarretenberg(i)), + bufferredInput.map(i => new FrBarretenberg(i)), index, ) .toBuffer(), diff --git a/yarn-project/merkle-tree/src/interfaces/merkle_tree.ts b/yarn-project/merkle-tree/src/interfaces/merkle_tree.ts index 752c07c3142..912158a70cf 100644 --- a/yarn-project/merkle-tree/src/interfaces/merkle_tree.ts +++ b/yarn-project/merkle-tree/src/interfaces/merkle_tree.ts @@ -57,4 +57,13 @@ export interface MerkleTree extends SiblingPathSource { * @returns The index of the first leaf found with a given value (undefined if not found). */ findLeafIndex(leaf: Buffer, includeUncommitted: boolean): bigint | undefined; + + /** + * Returns the first index containing a leaf value after `startIndex`. + * @param leaf - The leaf value to look for. + * @param startIndex - The index to start searching from (used when skipping nullified messages) + * @param includeUncommitted - Indicates whether to include uncommitted data. + * @returns The index of the first leaf found with a given value (undefined if not found). + */ + findLeafIndexAfter(leaf: Buffer, startIndex: bigint, includeUncommitted: boolean): bigint | undefined; } diff --git a/yarn-project/merkle-tree/src/snapshots/append_only_snapshot.ts b/yarn-project/merkle-tree/src/snapshots/append_only_snapshot.ts index 3096688b806..a3870f9ae23 100644 --- a/yarn-project/merkle-tree/src/snapshots/append_only_snapshot.ts +++ b/yarn-project/merkle-tree/src/snapshots/append_only_snapshot.ts @@ -250,8 +250,12 @@ class AppendOnlySnapshot implements TreeSnapshot { } findLeafIndex(value: Buffer): bigint | undefined { + return this.findLeafIndexAfter(value, 0n); + } + + findLeafIndexAfter(value: Buffer, startIndex: bigint): bigint | undefined { const numLeaves = this.getNumLeaves(); - for (let i = 0n; i < numLeaves; i++) { + for (let i = startIndex; i < numLeaves; i++) { const currentValue = this.getLeafValue(i); if (currentValue && currentValue.equals(value)) { return i; diff --git a/yarn-project/merkle-tree/src/snapshots/base_full_snapshot.ts b/yarn-project/merkle-tree/src/snapshots/base_full_snapshot.ts index 01518cfd3f2..223554a215d 100644 --- a/yarn-project/merkle-tree/src/snapshots/base_full_snapshot.ts +++ b/yarn-project/merkle-tree/src/snapshots/base_full_snapshot.ts @@ -203,8 +203,12 @@ export class BaseFullTreeSnapshot implements TreeSnapshot { } findLeafIndex(value: Buffer): bigint | undefined { + return this.findLeafIndexAfter(value, 0n); + } + + public findLeafIndexAfter(value: Buffer, startIndex: bigint): bigint | undefined { const numLeaves = this.getNumLeaves(); - for (let i = 0n; i < numLeaves; i++) { + for (let i = startIndex; i < numLeaves; i++) { const currentValue = this.getLeafValue(i); if (currentValue && currentValue.equals(value)) { return i; diff --git a/yarn-project/merkle-tree/src/snapshots/snapshot_builder.ts b/yarn-project/merkle-tree/src/snapshots/snapshot_builder.ts index 5feb1a2ef00..cecac01a206 100644 --- a/yarn-project/merkle-tree/src/snapshots/snapshot_builder.ts +++ b/yarn-project/merkle-tree/src/snapshots/snapshot_builder.ts @@ -56,6 +56,14 @@ export interface TreeSnapshot { * @returns The index of the first leaf found with a given value (undefined if not found). */ findLeafIndex(value: Buffer): bigint | undefined; + + /** + * Returns the first index containing a leaf value after `startIndex`. + * @param leaf - The leaf value to look for. + * @param startIndex - The index to start searching from (used when skipping nullified messages) + * @returns The index of the first leaf found with a given value (undefined if not found). + */ + findLeafIndexAfter(leaf: Buffer, startIndex: bigint): bigint | undefined; } /** A snapshot of an indexed tree */ diff --git a/yarn-project/merkle-tree/src/sparse_tree/sparse_tree.ts b/yarn-project/merkle-tree/src/sparse_tree/sparse_tree.ts index 86f3c8506e4..6630e7bb1bb 100644 --- a/yarn-project/merkle-tree/src/sparse_tree/sparse_tree.ts +++ b/yarn-project/merkle-tree/src/sparse_tree/sparse_tree.ts @@ -46,4 +46,8 @@ export class SparseTree extends TreeBase implements UpdateOnlyTree { public findLeafIndex(_value: Buffer, _includeUncommitted: boolean): bigint | undefined { throw new Error('Finding leaf index is not supported for sparse trees'); } + + public findLeafIndexAfter(_value: Buffer, _startIndex: bigint, _includeUncommitted: boolean): bigint | undefined { + throw new Error('Finding leaf index is not supported for sparse trees'); + } } diff --git a/yarn-project/merkle-tree/src/standard_indexed_tree/standard_indexed_tree.ts b/yarn-project/merkle-tree/src/standard_indexed_tree/standard_indexed_tree.ts index 68a3f588ae2..5239e04d6a4 100644 --- a/yarn-project/merkle-tree/src/standard_indexed_tree/standard_indexed_tree.ts +++ b/yarn-project/merkle-tree/src/standard_indexed_tree/standard_indexed_tree.ts @@ -234,6 +234,10 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree { return index; } + public findLeafIndexAfter(_leaf: Buffer, _startIndex: bigint, _includeUncommitted: boolean): bigint | undefined { + throw new Error('Method not implemented for indexed trees'); + } + /** * Initializes the tree. * @param prefilledSize - A number of leaves that are prefilled with values. diff --git a/yarn-project/merkle-tree/src/standard_tree/standard_tree.ts b/yarn-project/merkle-tree/src/standard_tree/standard_tree.ts index c1dc29cda69..4c28b5e74f6 100644 --- a/yarn-project/merkle-tree/src/standard_tree/standard_tree.ts +++ b/yarn-project/merkle-tree/src/standard_tree/standard_tree.ts @@ -43,7 +43,11 @@ export class StandardTree extends TreeBase implements AppendOnlyTree { } public findLeafIndex(value: Buffer, includeUncommitted: boolean): bigint | undefined { - for (let i = 0n; i < this.getNumLeaves(includeUncommitted); i++) { + return this.findLeafIndexAfter(value, 0n, includeUncommitted); + } + + public findLeafIndexAfter(value: Buffer, startIndex: bigint, includeUncommitted: boolean): bigint | undefined { + for (let i = startIndex; i < this.getNumLeaves(includeUncommitted); i++) { const currentValue = this.getLeafValue(i, includeUncommitted); if (currentValue && currentValue.equals(value)) { return i; diff --git a/yarn-project/merkle-tree/src/tree_base.ts b/yarn-project/merkle-tree/src/tree_base.ts index 21baacdc041..7d7c2fc5035 100644 --- a/yarn-project/merkle-tree/src/tree_base.ts +++ b/yarn-project/merkle-tree/src/tree_base.ts @@ -331,4 +331,13 @@ export abstract class TreeBase implements MerkleTree { * @returns The index of the first leaf found with a given value (undefined if not found). */ abstract findLeafIndex(value: Buffer, includeUncommitted: boolean): bigint | undefined; + + /** + * Returns the first index containing a leaf value after `startIndex`. + * @param leaf - The leaf value to look for. + * @param startIndex - The index to start searching from (used when skipping nullified messages) + * @param includeUncommitted - Indicates whether to include uncommitted data. + * @returns The index of the first leaf found with a given value (undefined if not found). + */ + abstract findLeafIndexAfter(leaf: Buffer, startIndex: bigint, includeUncommitted: boolean): bigint | undefined; } diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index d0b5ac16837..1be28074b65 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -6,6 +6,7 @@ import { NoteStatus, NullifierMembershipWitness, PublicDataWitness, + SiblingPath, } from '@aztec/circuit-types'; import { AztecAddress, @@ -16,6 +17,7 @@ import { Header, L1_TO_L2_MSG_TREE_HEIGHT, } from '@aztec/circuits.js'; +import { computeL1ToL2MessageNullifier } from '@aztec/circuits.js/hash'; import { FunctionArtifactWithDebugMetadata, getFunctionArtifactWithDebugMetadata } from '@aztec/foundation/abi'; import { createDebugLogger } from '@aztec/foundation/log'; import { DBOracle, KeyPair, MessageLoadOracleInputs } from '@aztec/simulator'; @@ -121,17 +123,40 @@ export class SimulatorOracle implements DBOracle { } /** - * Fetches the a message from the db, given its key. + * Fetches a message from the db, given its key. + * @param contractAddress - Address of a contract by which the message was emitted. * @param messageHash - Hash of the message. + * @param secret - Secret used to compute a nullifier. + * @dev Contract address and secret are only used to compute the nullifier to get non-nullified messages * @returns The l1 to l2 membership witness (index of message in the tree and sibling path). */ - async getL1ToL2MembershipWitness(messageHash: Fr): Promise> { - const response = await this.aztecNode.getL1ToL2MessageMembershipWitness('latest', messageHash); - if (!response) { - throw new Error(`No L1 to L2 message found for message hash ${messageHash.toString()}`); - } - const [index, siblingPath] = response; - return new MessageLoadOracleInputs(index, siblingPath); + async getL1ToL2MembershipWitness( + contractAddress: AztecAddress, + messageHash: Fr, + secret: Fr, + ): Promise> { + let nullifierIndex: bigint | undefined; + let messageIndex = 0n; + let startIndex = 0n; + let siblingPath: SiblingPath; + + // We iterate over messages until we find one whose nullifier is not in the nullifier tree --> we need to check + // for nullifiers because messages can have duplicates. + do { + const response = await this.aztecNode.getL1ToL2MessageMembershipWitness('latest', messageHash, startIndex); + if (!response) { + throw new Error(`No non-nullified L1 to L2 message found for message hash ${messageHash.toString()}`); + } + [messageIndex, siblingPath] = response; + + const messageNullifier = computeL1ToL2MessageNullifier(contractAddress, messageHash, secret, messageIndex); + nullifierIndex = await this.getNullifierIndex(messageNullifier); + + startIndex = messageIndex + 1n; + } while (nullifierIndex !== undefined); + + // Assuming messageIndex is what you intended to use for the index in MessageLoadOracleInputs + return new MessageLoadOracleInputs(messageIndex, siblingPath); } /** diff --git a/yarn-project/sequencer-client/src/simulator/public_executor.ts b/yarn-project/sequencer-client/src/simulator/public_executor.ts index fc3d4123588..1f1d582431c 100644 --- a/yarn-project/sequencer-client/src/simulator/public_executor.ts +++ b/yarn-project/sequencer-client/src/simulator/public_executor.ts @@ -17,7 +17,7 @@ import { NullifierLeafPreimage, PublicDataTreeLeafPreimage, } from '@aztec/circuits.js'; -import { computePublicDataTreeLeafSlot } from '@aztec/circuits.js/hash'; +import { computeL1ToL2MessageNullifier, computePublicDataTreeLeafSlot } from '@aztec/circuits.js/hash'; import { createDebugLogger } from '@aztec/foundation/log'; import { getCanonicalClassRegistererAddress } from '@aztec/protocol-contracts/class-registerer'; import { CommitmentsDB, MessageLoadOracleInputs, PublicContractsDB, PublicStateDB } from '@aztec/simulator'; @@ -224,17 +224,38 @@ export class WorldStateDB implements CommitmentsDB { } public async getL1ToL2MembershipWitness( + contractAddress: AztecAddress, messageHash: Fr, + secret: Fr, ): Promise> { - const index = (await this.db.findLeafIndex(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, messageHash.toBuffer()))!; - if (index === undefined) { - throw new Error(`Message ${messageHash.toString()} not found`); - } + let nullifierIndex: bigint | undefined; + let messageIndex: bigint | undefined; + let startIndex = 0n; + + // We iterate over messages until we find one whose nullifier is not in the nullifier tree --> we need to check + // for nullifiers because messages can have duplicates. + do { + messageIndex = (await this.db.findLeafIndexAfter( + MerkleTreeId.L1_TO_L2_MESSAGE_TREE, + messageHash.toBuffer(), + startIndex, + ))!; + if (messageIndex === undefined) { + throw new Error(`No non-nullified L1 to L2 message found for message hash ${messageHash.toString()}`); + } + + const messageNullifier = computeL1ToL2MessageNullifier(contractAddress, messageHash, secret, messageIndex); + nullifierIndex = await this.getNullifierIndex(messageNullifier); + + startIndex = messageIndex + 1n; + } while (nullifierIndex !== undefined); + const siblingPath = await this.db.getSiblingPath( MerkleTreeId.L1_TO_L2_MESSAGE_TREE, - index, + messageIndex, ); - return new MessageLoadOracleInputs(index, siblingPath); + + return new MessageLoadOracleInputs(messageIndex, siblingPath); } public async getCommitmentIndex(commitment: Fr): Promise { diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 78f7f2abbc2..812c41bc068 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -244,8 +244,16 @@ export class Oracle { return toACVMField(exists); } - async getL1ToL2MembershipWitness([messageHash]: ACVMField[]): Promise { - const message = await this.typedOracle.getL1ToL2MembershipWitness(fromACVMField(messageHash)); + async getL1ToL2MembershipWitness( + [contractAddress]: ACVMField[], + [messageHash]: ACVMField[], + [secret]: ACVMField[], + ): Promise { + const message = await this.typedOracle.getL1ToL2MembershipWitness( + AztecAddress.fromString(contractAddress), + fromACVMField(messageHash), + fromACVMField(secret), + ); return message.toFields().map(toACVMField); } diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index e2fc9040bd1..b315f61d371 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -171,7 +171,11 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('checkNullifierExists'); } - getL1ToL2MembershipWitness(_messageHash: Fr): Promise> { + getL1ToL2MembershipWitness( + _contractAddress: AztecAddress, + _messageHash: Fr, + _secret: Fr, + ): Promise> { throw new OracleMethodNotAvailableError('getL1ToL2MembershipWitness'); } diff --git a/yarn-project/simulator/src/avm/journal/journal.ts b/yarn-project/simulator/src/avm/journal/journal.ts index be16424c07a..b2709d01d3c 100644 --- a/yarn-project/simulator/src/avm/journal/journal.ts +++ b/yarn-project/simulator/src/avm/journal/journal.ts @@ -156,7 +156,16 @@ export class AvmPersistableStateManager { public async checkL1ToL2MessageExists(msgHash: Fr, msgLeafIndex: Fr): Promise { let exists = false; try { - const gotMessage = await this.hostStorage.commitmentsDb.getL1ToL2MembershipWitness(msgHash); + // The following 2 values are used to compute a message nullifier. Given that here we do not care about getting + // non-nullified messages we can just pass in random values and the nullifier check will effectively be ignored + // (no nullifier will be found). + const ignoredContractAddress = AztecAddress.random(); + const ignoredSecret = Fr.random(); + const gotMessage = await this.hostStorage.commitmentsDb.getL1ToL2MembershipWitness( + ignoredContractAddress, + msgHash, + ignoredSecret, + ); exists = gotMessage !== undefined && gotMessage.index == msgLeafIndex.toBigInt(); } catch { // error getting message - doesn't exist! diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index 64e06296f82..5e6f33e3b6a 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -225,12 +225,15 @@ export class ViewDataOracle extends TypedOracle { } /** - * Fetches the a message from the db, given its key. + * Fetches a message from the db, given its key. + * @param contractAddress - Address of a contract by which the message was emitted. * @param messageHash - Hash of the message. + * @param secret - Secret used to compute a nullifier. + * @dev Contract address and secret are only used to compute the nullifier to get non-nullified messages * @returns The l1 to l2 membership witness (index of message in the tree and sibling path). */ - public async getL1ToL2MembershipWitness(messageHash: Fr) { - return await this.db.getL1ToL2MembershipWitness(messageHash); + public async getL1ToL2MembershipWitness(contractAddress: AztecAddress, messageHash: Fr, secret: Fr) { + return await this.db.getL1ToL2MembershipWitness(contractAddress, messageHash, secret); } /** diff --git a/yarn-project/simulator/src/public/db.ts b/yarn-project/simulator/src/public/db.ts index 8f7524bff1f..3e05d7212f8 100644 --- a/yarn-project/simulator/src/public/db.ts +++ b/yarn-project/simulator/src/public/db.ts @@ -79,12 +79,18 @@ export interface PublicContractsDB { /** Database interface for providing access to commitment tree, l1 to l2 message tree, and nullifier tree. */ export interface CommitmentsDB { /** - * Gets a confirmed L1 to L2 message for the given message hash. - * TODO(Maddiaa): Can be combined with aztec-node method that does the same thing. + * Fetches a message from the db, given its key. + * @param contractAddress - Address of a contract by which the message was emitted. * @param messageHash - Hash of the message. + * @param secret - Secret used to compute a nullifier. + * @dev Contract address and secret are only used to compute the nullifier to get non-nullified messages * @returns The l1 to l2 membership witness (index of message in the tree and sibling path). */ - getL1ToL2MembershipWitness(messageHash: Fr): Promise>; + getL1ToL2MembershipWitness( + contractAddress: AztecAddress, + messageHash: Fr, + secret: Fr, + ): Promise>; /** * Gets the index of a commitment in the note hash tree. diff --git a/yarn-project/simulator/src/public/public_execution_context.ts b/yarn-project/simulator/src/public/public_execution_context.ts index 31e9eb1ee88..69f9caa6eda 100644 --- a/yarn-project/simulator/src/public/public_execution_context.ts +++ b/yarn-project/simulator/src/public/public_execution_context.ts @@ -92,12 +92,15 @@ export class PublicExecutionContext extends TypedOracle { } /** - * Fetches the a message from the db, given its key. - * @param messageHash - Hash of the massage. - * @returns The l1 to l2 message data + * Fetches a message from the db, given its key. + * @param contractAddress - Address of a contract by which the message was emitted. + * @param messageHash - Hash of the message. + * @param secret - Secret used to compute a nullifier. + * @dev Contract address and secret are only used to compute the nullifier to get non-nullified messages + * @returns The l1 to l2 membership witness (index of message in the tree and sibling path). */ - public async getL1ToL2MembershipWitness(messageHash: Fr) { - return await this.commitmentsDb.getL1ToL2MembershipWitness(messageHash); + public async getL1ToL2MembershipWitness(contractAddress: AztecAddress, messageHash: Fr, secret: Fr) { + return await this.commitmentsDb.getL1ToL2MembershipWitness(contractAddress, messageHash, secret); } /** diff --git a/yarn-project/world-state/src/world-state-db/merkle_tree_operations.ts b/yarn-project/world-state/src/world-state-db/merkle_tree_operations.ts index e317aa25e67..c418fa2a1e2 100644 --- a/yarn-project/world-state/src/world-state-db/merkle_tree_operations.ts +++ b/yarn-project/world-state/src/world-state-db/merkle_tree_operations.ts @@ -110,6 +110,14 @@ export interface MerkleTreeOperations { */ findLeafIndex(treeId: MerkleTreeId, value: Buffer): Promise; + /** + * Returns the first index containing a leaf value after `startIndex`. + * @param treeId - The tree for which the index should be returned. + * @param value - The value to search for in the tree. + * @param startIndex - The index to start searching from (used when skipping nullified messages) + */ + findLeafIndexAfter(treeId: MerkleTreeId, value: Buffer, startIndex: bigint): Promise; + /** * Gets the value for a leaf in the tree. * @param treeId - The tree for which the index should be returned. diff --git a/yarn-project/world-state/src/world-state-db/merkle_tree_operations_facade.ts b/yarn-project/world-state/src/world-state-db/merkle_tree_operations_facade.ts index 5e32cc757f0..b6282ee2abd 100644 --- a/yarn-project/world-state/src/world-state-db/merkle_tree_operations_facade.ts +++ b/yarn-project/world-state/src/world-state-db/merkle_tree_operations_facade.ts @@ -120,6 +120,16 @@ export class MerkleTreeOperationsFacade implements MerkleTreeOperations { return this.trees.findLeafIndex(treeId, value, this.includeUncommitted); } + /** + * Returns the first index containing a leaf value after `startIndex`. + * @param treeId - The tree for which the index should be returned. + * @param value - The value to search for in the tree. + * @param startIndex - The index to start searching from (used when skipping nullified messages) + */ + findLeafIndexAfter(treeId: MerkleTreeId, value: Buffer, startIndex: bigint): Promise { + return this.trees.findLeafIndexAfter(treeId, value, startIndex, this.includeUncommitted); + } + /** * Gets the value at the given index. * @param treeId - The ID of the tree to get the leaf value from. diff --git a/yarn-project/world-state/src/world-state-db/merkle_tree_snapshot_operations_facade.ts b/yarn-project/world-state/src/world-state-db/merkle_tree_snapshot_operations_facade.ts index 144ee20e29c..59e958522c6 100644 --- a/yarn-project/world-state/src/world-state-db/merkle_tree_snapshot_operations_facade.ts +++ b/yarn-project/world-state/src/world-state-db/merkle_tree_snapshot_operations_facade.ts @@ -33,6 +33,11 @@ export class MerkleTreeSnapshotOperationsFacade implements MerkleTreeOperations return tree.findLeafIndex(value); } + async findLeafIndexAfter(treeId: MerkleTreeId, value: Buffer, startIndex: bigint): Promise { + const tree = await this.#getTreeSnapshot(treeId); + return tree.findLeafIndexAfter(value, startIndex); + } + async getLeafPreimage( treeId: MerkleTreeId.NULLIFIER_TREE, index: bigint, diff --git a/yarn-project/world-state/src/world-state-db/merkle_trees.ts b/yarn-project/world-state/src/world-state-db/merkle_trees.ts index ae962ca33a7..541874b2d89 100644 --- a/yarn-project/world-state/src/world-state-db/merkle_trees.ts +++ b/yarn-project/world-state/src/world-state-db/merkle_trees.ts @@ -339,6 +339,25 @@ export class MerkleTrees implements MerkleTreeDb { }); } + /** + * Returns the first index containing a leaf value after `startIndex`. + * @param treeId - The tree for which the index should be returned. + * @param value - The value to search for in the tree. + * @param startIndex - The index to start searching from (used when skipping nullified messages) + * @param includeUncommitted - Indicates whether to include uncommitted data. + */ + public async findLeafIndexAfter( + treeId: MerkleTreeId, + value: Buffer, + startIndex: bigint, + includeUncommitted: boolean, + ): Promise { + return await this.synchronize(() => { + const tree = this.trees[treeId]; + return Promise.resolve(tree.findLeafIndexAfter(value, startIndex, includeUncommitted)); + }); + } + /** * Updates a leaf in a tree at a given index. * @param treeId - The ID of the tree.