diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 07086475a197..ea18381410d3 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,29 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD + +### [Aztec.js] `TxReceipt` now includes `epochNumber` + +`TxReceipt` now includes an `epochNumber` field that indicates which epoch the transaction was included in. + +### [Aztec.js] `computeL2ToL1MembershipWitness` signature changed + +The function signature has changed to resolve the epoch internally from a transaction hash, rather than requiring the caller to pass the epoch number. + +**Migration:** + +```diff +- const witness = await computeL2ToL1MembershipWitness(aztecNode, epochNumber, messageHash); +- // epoch was passed in by the caller ++ const witness = await computeL2ToL1MembershipWitness(aztecNode, messageHash, txHash); ++ // epoch is now available on the returned witness ++ const epoch = witness.epochNumber; +``` + +The return type `L2ToL1MembershipWitness` now includes `epochNumber`. An optional `messageIndexInTx` parameter can be passed as the fourth argument to disambiguate when a transaction emits multiple identical L2-to-L1 messages. + +**Impact**: All call sites that compute L2-to-L1 membership witnesses must update to the new argument order and extract `epochNumber` from the result instead of passing it in. + ### Two separate init nullifiers for private and public Contract initialization now emits two separate nullifiers instead of one: a **private init nullifier** and a **public init nullifier**. Each nullifier gates its respective execution domain: diff --git a/docs/examples/ts/token_bridge/index.ts b/docs/examples/ts/token_bridge/index.ts index 81c410bae820..436a4be41dde 100644 --- a/docs/examples/ts/token_bridge/index.ts +++ b/docs/examples/ts/token_bridge/index.ts @@ -4,9 +4,7 @@ import { AztecAddress, EthAddress } from "@aztec/aztec.js/addresses"; import { Fr } from "@aztec/aztec.js/fields"; import { createAztecNodeClient } from "@aztec/aztec.js/node"; import { createExtendedL1Client } from "@aztec/ethereum/client"; -import { RollupContract } from "@aztec/ethereum/contracts"; import { deployL1Contract } from "@aztec/ethereum/deploy-l1-contract"; -import { CheckpointNumber } from "@aztec/foundation/branded-types"; import { sha256ToField } from "@aztec/foundation/crypto/sha256"; import { computeL2ToL1MessageHash, @@ -41,10 +39,6 @@ console.log(`Account: ${account.address.toString()}\n`); const nodeInfo = await node.getNodeInfo(); const registryAddress = nodeInfo.l1ContractAddresses.registryAddress.toString(); const inboxAddress = nodeInfo.l1ContractAddresses.inboxAddress.toString(); -const rollupAddress = nodeInfo.l1ContractAddresses.rollupAddress.toString(); - -// Create rollup contract instance for querying epoch information -const rollup = new RollupContract(l1Client, rollupAddress); // docs:end:setup // docs:start:deploy_l1_contracts @@ -308,15 +302,11 @@ while (provenBlockNumber < exitReceipt.blockNumber!) { console.log("Block proven!\n"); -// Get the epoch for the exit block's checkpoint -// In Aztec, checkpoint number equals block number (1:1 mapping) -const epoch = await rollup.getEpochNumberForCheckpoint( - CheckpointNumber.fromBlockNumber(exitReceipt.blockNumber!), -); +// Compute the membership witness using the message hash and the L2 tx hash +const witness = await computeL2ToL1MembershipWitness(node, msgLeaf, exitReceipt.txHash); +const epoch = witness!.epochNumber; console.log(` Epoch for block ${exitReceipt.blockNumber}: ${epoch}`); -// Compute the membership witness using the message hash and epoch -const witness = await computeL2ToL1MembershipWitness(node, epoch, msgLeaf); const siblingPathHex = witness!.siblingPath .toBufferArray() .map((buf: Buffer) => `0x${buf.toString("hex")}` as `0x${string}`); diff --git a/yarn-project/archiver/src/modules/data_source_base.ts b/yarn-project/archiver/src/modules/data_source_base.ts index 7bdb3e1faf99..b4da6586d2d0 100644 --- a/yarn-project/archiver/src/modules/data_source_base.ts +++ b/yarn-project/archiver/src/modules/data_source_base.ts @@ -154,7 +154,7 @@ export abstract class ArchiverDataSourceBase } public getSettledTxReceipt(txHash: TxHash): Promise { - return this.store.getSettledTxReceipt(txHash); + return this.store.getSettledTxReceipt(txHash, this.l1Constants); } public isPendingChainInvalid(): Promise { diff --git a/yarn-project/archiver/src/store/block_store.ts b/yarn-project/archiver/src/store/block_store.ts index 7ea51ec45a15..fc534436c3f5 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -20,6 +20,7 @@ import { serializeValidateCheckpointResult, } from '@aztec/stdlib/block'; import { type CheckpointData, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; +import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; import { @@ -851,7 +852,10 @@ export class BlockStore { * @param txHash - The hash of a tx we try to get the receipt for. * @returns The requested tx receipt (or undefined if not found). */ - async getSettledTxReceipt(txHash: TxHash): Promise { + async getSettledTxReceipt( + txHash: TxHash, + l1Constants?: Pick, + ): Promise { const txEffect = await this.getTxEffect(txHash); if (!txEffect) { return undefined; @@ -860,10 +864,11 @@ export class BlockStore { const blockNumber = BlockNumber(txEffect.l2BlockNumber); // Use existing archiver methods to determine finalization level - const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber] = await Promise.all([ + const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([ this.getProvenBlockNumber(), this.getCheckpointedL2BlockNumber(), this.getFinalizedL2BlockNumber(), + this.getBlockData(blockNumber), ]); let status: TxStatus; @@ -877,6 +882,9 @@ export class BlockStore { status = TxStatus.PROPOSED; } + const epochNumber = + blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined; + return new TxReceipt( txHash, status, @@ -885,6 +893,7 @@ export class BlockStore { txEffect.data.transactionFee.toBigInt(), txEffect.l2BlockHash, blockNumber, + epochNumber, ); } diff --git a/yarn-project/archiver/src/store/kv_archiver_store.ts b/yarn-project/archiver/src/store/kv_archiver_store.ts index 054e795a49c9..bf0783a503e2 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.ts @@ -22,6 +22,7 @@ import type { ExecutablePrivateFunctionWithMembershipProof, UtilityFunctionWithMembershipProof, } from '@aztec/stdlib/contract'; +import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client'; import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; import type { BlockHeader, TxHash, TxReceipt } from '@aztec/stdlib/tx'; @@ -408,8 +409,11 @@ export class KVArchiverDataStore implements ContractDataSource { * @param txHash - The hash of a tx we try to get the receipt for. * @returns The requested tx receipt (or undefined if not found). */ - getSettledTxReceipt(txHash: TxHash): Promise { - return this.#blockStore.getSettledTxReceipt(txHash); + getSettledTxReceipt( + txHash: TxHash, + l1Constants?: Pick, + ): Promise { + return this.#blockStore.getSettledTxReceipt(txHash, l1Constants); } /** diff --git a/yarn-project/archiver/src/test/mock_l2_block_source.ts b/yarn-project/archiver/src/test/mock_l2_block_source.ts index 4491991066cd..d3f46943b65a 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -18,7 +18,12 @@ import { } from '@aztec/stdlib/block'; import { Checkpoint, type CheckpointData, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; import type { ContractClassPublic, ContractDataSource, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { EmptyL1RollupConstants, type L1RollupConstants, getSlotRangeForEpoch } from '@aztec/stdlib/epoch-helpers'; +import { + EmptyL1RollupConstants, + type L1RollupConstants, + getEpochAtSlot, + getSlotRangeForEpoch, +} from '@aztec/stdlib/epoch-helpers'; import { computeCheckpointOutHash } from '@aztec/stdlib/messaging'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; import { type BlockHeader, TxExecutionResult, TxHash, TxReceipt, TxStatus } from '@aztec/stdlib/tx'; @@ -394,6 +399,7 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { txEffect.transactionFee.toBigInt(), await block.hash(), block.number, + getEpochAtSlot(block.slot, EmptyL1RollupConstants), ); } } diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 242c8204f744..203d2b4f866a 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -720,6 +720,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return (await this.blockSource.getCheckpointedBlocks(from, limit)) ?? []; } + public getCheckpointsDataForEpoch(epochNumber: EpochNumber) { + return this.blockSource.getCheckpointsDataForEpoch(epochNumber); + } + /** * Method to fetch the current min L2 fees. * @returns The current min L2 fees. diff --git a/yarn-project/aztec/src/testing/epoch_test_settler.ts b/yarn-project/aztec/src/testing/epoch_test_settler.ts index ee9cbdbafb9b..c2fa06dcf024 100644 --- a/yarn-project/aztec/src/testing/epoch_test_settler.ts +++ b/yarn-project/aztec/src/testing/epoch_test_settler.ts @@ -4,7 +4,7 @@ import { type EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; import type { Logger } from '@aztec/foundation/log'; import { EpochMonitor } from '@aztec/prover-node'; import type { EthAddress, L2BlockSource } from '@aztec/stdlib/block'; -import { computeL2ToL1MembershipWitnessFromMessagesInEpoch } from '@aztec/stdlib/messaging'; +import { computeEpochOutHash } from '@aztec/stdlib/messaging'; export class EpochTestSettler { private rollupCheatCodes: RollupCheatCodes; @@ -51,9 +51,8 @@ export class EpochTestSettler { messagesInEpoch[checkpointIndex].push(block.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs)); } - const [firstMessage] = messagesInEpoch.flat(3); - if (firstMessage) { - const { root: outHash } = computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, firstMessage); + const outHash = computeEpochOutHash(messagesInEpoch); + if (!outHash.isZero()) { await this.rollupCheatCodes.insertOutbox(epoch, outHash.toBigInt()); } else { this.log.info(`No L2 to L1 messages in epoch ${epoch}`); diff --git a/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts b/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts index 3a4b9932e270..5c5243df8c66 100644 --- a/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts @@ -7,7 +7,6 @@ import { Fr } from '@aztec/aztec.js/fields'; import { createLogger } from '@aztec/aztec.js/log'; import { createAztecNodeClient, waitForNode } from '@aztec/aztec.js/node'; import { createExtendedL1Client } from '@aztec/ethereum/client'; -import { RollupContract } from '@aztec/ethereum/contracts'; import { deployL1Contract } from '@aztec/ethereum/deploy-l1-contract'; import { FeeAssetHandlerAbi, @@ -224,11 +223,7 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { // docs:end:l2-withdraw // docs:start:l1-withdraw - const rollup = new RollupContract(l1Client, l1ContractAddresses.rollupAddress.toString()); - const block = await node.getBlock(l2TxReceipt.blockNumber!); - const epoch = await rollup.getEpochNumberForCheckpoint(block!.checkpointNumber); - - const result = await computeL2ToL1MembershipWitness(node, epoch, l2ToL1Message); + const result = await computeL2ToL1MembershipWitness(node, l2ToL1Message, l2TxReceipt.txHash); if (!result) { throw new Error('L2 to L1 message not found'); } @@ -236,7 +231,7 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { await l1PortalManager.withdrawFunds( withdrawAmount, EthAddress.fromString(ownerEthAddress), - epoch, + result.epochNumber, result.leafIndex, result.siblingPath, ); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l2_to_l1.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l2_to_l1.test.ts index d747d3ef6666..1874f64e75d0 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l2_to_l1.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l2_to_l1.test.ts @@ -3,7 +3,6 @@ import { BatchCall } from '@aztec/aztec.js/contracts'; import { Fr } from '@aztec/aztec.js/fields'; import type { Wallet } from '@aztec/aztec.js/wallet'; import { OutboxContract, RollupContract, type ViemL2ToL1Msg } from '@aztec/ethereum/contracts'; -import { EpochNumber } from '@aztec/foundation/branded-types'; import { OutboxAbi } from '@aztec/l1-artifacts'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import { computeL2ToL1MessageHash } from '@aztec/stdlib/hash'; @@ -13,6 +12,7 @@ import { computeL2ToL1MembershipWitness, getL2ToL1MessageLeafId, } from '@aztec/stdlib/messaging'; +import type { TxHash } from '@aztec/stdlib/tx'; import { type Hex, decodeEventLog } from 'viem'; @@ -68,7 +68,7 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { const blockNumber = txReceipt.blockNumber!; // Advance the epoch until the tx is proven since the messages are inserted to the outbox when the epoch is proven. - const epoch = await t.advanceToEpochProven(txReceipt); + await t.advanceToEpochProven(txReceipt); // Check that the block contains the 2 messages. const block = (await aztecNode.getBlock(blockNumber))!; @@ -76,9 +76,9 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { expect(l2ToL1Messages).toStrictEqual([computeMessageLeaf(messages[0]), computeMessageLeaf(messages[1])]); // Consume messages[0]. - await expectConsumeMessageToSucceed(epoch, messages[0]); + await expectConsumeMessageToSucceed(messages[0], txReceipt.txHash); // Consume messages[1]. - await expectConsumeMessageToSucceed(epoch, messages[1]); + await expectConsumeMessageToSucceed(messages[1], txReceipt.txHash); }); // When the block contains a tx with no messages, the zero txOutHash is skipped and won't be included in the top tree. @@ -103,10 +103,10 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { expect(noMessageReceipt.blockNumber).toEqual(withMessageReceipt.blockNumber); // Advance the epoch until the tx is proven since the messages are inserted to the outbox when the epoch is proven. - const epoch = await t.advanceToEpochProven(withMessageReceipt); + await t.advanceToEpochProven(withMessageReceipt); // Consume the message. - await expectConsumeMessageToSucceed(epoch, message); + await expectConsumeMessageToSucceed(message, withMessageReceipt.txHash); }); it('2 txs (balanced), one with 3 messages (unbalanced), one with 4 messages (balanced)', async () => { @@ -139,25 +139,25 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { } // Advance the epoch until the tx is proven since the messages are inserted to the outbox when the epoch is proven. - const epoch = await t.advanceToEpochProven(l2TxReceipt1); + await t.advanceToEpochProven(l2TxReceipt1); // Consume messages in tx0. { // Consume messages[0], which is in the subtree of height 2. const msg = tx0.messages[0]; - await expectConsumeMessageToSucceed(epoch, msg); + await expectConsumeMessageToSucceed(msg, l2TxReceipt0.txHash); } { // Consume messages[2], which is in the subtree of height 1. const msg = tx0.messages[2]; - await expectConsumeMessageToSucceed(epoch, msg); + await expectConsumeMessageToSucceed(msg, l2TxReceipt0.txHash); } // Consume messages in tx1. { // Consume messages[2], which is in the subtree of height 2. const msg = tx1.messages[0]; - await expectConsumeMessageToSucceed(epoch, msg); + await expectConsumeMessageToSucceed(msg, l2TxReceipt1.txHash); } }); @@ -185,32 +185,32 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { expect(l2TxReceipt2.blockNumber).toEqual(blockNumber); // Advance the epoch until the tx is proven since the messages are inserted to the outbox when the epoch is proven. - const epoch = await t.advanceToEpochProven(l2TxReceipt2); + await t.advanceToEpochProven(l2TxReceipt2); // Consume messages in tx0. { // Consume messages[0], which is in the subtree of height 2. const msg = tx0.messages[0]; - await expectConsumeMessageToSucceed(epoch, msg); + await expectConsumeMessageToSucceed(msg, l2TxReceipt0.txHash); } { // Consume messages[2], which is in the subtree of height 1. const msg = tx0.messages[2]; - await expectConsumeMessageToSucceed(epoch, msg); + await expectConsumeMessageToSucceed(msg, l2TxReceipt0.txHash); } // Consume messages in tx1. { // Consume messages[0], which is the tx subtree root. const msg = tx1.messages[0]; - await expectConsumeMessageToSucceed(epoch, msg); + await expectConsumeMessageToSucceed(msg, l2TxReceipt1.txHash); } // Consume messages in tx2. { // Consume messages[1], which is in the subtree of height 1. const msg = tx2.messages[1]; - await expectConsumeMessageToSucceed(epoch, msg); + await expectConsumeMessageToSucceed(msg, l2TxReceipt2.txHash); } }); @@ -252,9 +252,10 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { return { recipients, contents, messages }; } - async function expectConsumeMessageToSucceed(epoch: EpochNumber, msg: ReturnType) { + async function expectConsumeMessageToSucceed(msg: ReturnType, l2TxHash: TxHash) { const msgLeaf = computeMessageLeaf(msg); - const witness = (await computeL2ToL1MembershipWitness(aztecNode, epoch, msgLeaf))!; + const result = (await computeL2ToL1MembershipWitness(aztecNode, msgLeaf, l2TxHash))!; + const { epochNumber: epoch, ...witness } = result; const leafId = getL2ToL1MessageLeafId(witness); const txHash = await outbox.consume( @@ -295,22 +296,17 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { expect(topics.args.leafId).toBe(leafId); // Ensure we cannot consume the same message again. - await expectConsumeMessageToFail(epoch, msg, witness); + await expectConsumeMessageToFail(msg, result); } async function expectConsumeMessageToFail( - epoch: EpochNumber, msg: ReturnType, - witness?: L2ToL1MembershipWitness, + witness: L2ToL1MembershipWitness, ) { - if (!witness) { - const msgLeaf = computeMessageLeaf(msg); - witness = (await computeL2ToL1MembershipWitness(aztecNode, epoch, msgLeaf))!; - } await expect( outbox.consume( msg, - epoch, + witness.epochNumber, witness.leafIndex, witness.siblingPath.toFields().map(f => f.toString()), ), diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_private.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_private.test.ts index 15172407c2e1..2060c5263a68 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_private.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_private.test.ts @@ -73,17 +73,17 @@ describe('e2e_cross_chain_messaging token_bridge_private', () => { await crossChainTestHarness.expectPrivateBalanceOnL2(ownerAddress, bridgeAmount - withdrawAmount); // Advance the epoch until the tx is proven since the messages are inserted to the outbox when the epoch is proven. - const epoch = await t.advanceToEpochProven(l2TxReceipt); + await t.advanceToEpochProven(l2TxReceipt); - const l2ToL1MessageResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, l2ToL1Message); + const l2ToL1MessageResult = (await computeL2ToL1MembershipWitness(aztecNode, l2ToL1Message, l2TxReceipt.txHash))!; // Check balance before and after exit. expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); await crossChainTestHarness.withdrawFundsFromBridgeOnL1( withdrawAmount, - epoch, - l2ToL1MessageResult!.leafIndex, - l2ToL1MessageResult!.siblingPath, + l2ToL1MessageResult.epochNumber, + l2ToL1MessageResult.leafIndex, + l2ToL1MessageResult.siblingPath, ); expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); }); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_public.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_public.test.ts index 0218cdc49857..4f4e4d34cc12 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_public.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_public.test.ts @@ -80,17 +80,17 @@ describe('e2e_cross_chain_messaging token_bridge_public', () => { await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, afterBalance - withdrawAmount); // Advance the epoch until the tx is proven since the messages are inserted to the outbox when the epoch is proven. - const epoch = await t.advanceToEpochProven(l2TxReceipt); + await t.advanceToEpochProven(l2TxReceipt); - const l2ToL1MessageResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, l2ToL1Message); + const l2ToL1MessageResult = (await computeL2ToL1MembershipWitness(aztecNode, l2ToL1Message, l2TxReceipt.txHash))!; // Check balance before and after exit. expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); await crossChainTestHarness.withdrawFundsFromBridgeOnL1( withdrawAmount, - epoch, - l2ToL1MessageResult!.leafIndex, - l2ToL1MessageResult!.siblingPath, + l2ToL1MessageResult.epochNumber, + l2ToL1MessageResult.leafIndex, + l2ToL1MessageResult.siblingPath, ); expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); }, 120_000); diff --git a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts index 786b2c9aa7a5..2b3d00198fa4 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts @@ -347,12 +347,9 @@ describe('e2e_p2p_add_rollup', () => { chainId: new Fr(l1Client.chain.id), }); - const rollup = new RollupContract(l1Client, l1ContractAddresses.rollupAddress); - const block = await node.getBlock(l2OutgoingReceipt.blockNumber!); - const epoch = await rollup.getEpochNumberForCheckpoint(block!.checkpointNumber); - - const l2ToL1MessageResult = (await computeL2ToL1MembershipWitness(node, epoch, leaf))!; - const leafId = getL2ToL1MessageLeafId(l2ToL1MessageResult); + const l2ToL1MessageResult = (await computeL2ToL1MembershipWitness(node, leaf, l2OutgoingReceipt.txHash))!; + const { epochNumber: epoch, ...l2ToL1MessageWitness } = l2ToL1MessageResult; + const leafId = getL2ToL1MessageLeafId(l2ToL1MessageWitness); // We need to advance to the next epoch so that the out hash will be set to outbox when the epoch is proven. const cheatcodes = RollupCheatCodes.create(l1RpcUrls, l1ContractAddresses, t.ctx.dateProvider); @@ -374,8 +371,8 @@ describe('e2e_p2p_add_rollup', () => { args: [ l2ToL1Message, BigInt(epoch), - BigInt(l2ToL1MessageResult!.leafIndex), - l2ToL1MessageResult!.siblingPath + BigInt(l2ToL1MessageWitness.leafIndex), + l2ToL1MessageWitness.siblingPath .toBufferArray() .map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], ], diff --git a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts index 6795cc1bf180..b75f1e8b76d3 100644 --- a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts +++ b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts @@ -252,8 +252,12 @@ export const uniswapL1L2TestSuite = ( await wethCrossChainHarness.expectPublicBalanceOnL2(uniswapL2Contract.address, 0n); // Since the outbox is only consumable when the epoch is proven, we need to advance to the next epoch. - const block = await aztecNode.getBlock(l2UniswapInteractionReceipt.blockNumber!); - const epoch = await rollup.getEpochNumberForCheckpoint(block!.checkpointNumber); + const swapResult = (await computeL2ToL1MembershipWitness( + aztecNode, + swapPrivateLeaf, + l2UniswapInteractionReceipt.txHash, + ))!; + const { epochNumber: epoch } = swapResult; await cheatCodes.rollup.advanceToEpoch(EpochNumber(epoch + 1)); await waitForProven(aztecNode, l2UniswapInteractionReceipt, { provenTimeout: 300 }); @@ -262,14 +266,17 @@ export const uniswapL1L2TestSuite = ( const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( daiCrossChainHarness.tokenPortalAddress, ); - const swapResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, swapPrivateLeaf); - const withdrawResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, withdrawLeaf); + const withdrawResult = (await computeL2ToL1MembershipWitness( + aztecNode, + withdrawLeaf, + l2UniswapInteractionReceipt.txHash, + ))!; - const swapPrivateL2MessageIndex = swapResult!.leafIndex; - const swapPrivateSiblingPath = swapResult!.siblingPath; + const swapPrivateL2MessageIndex = swapResult.leafIndex; + const swapPrivateSiblingPath = swapResult.siblingPath; - const withdrawL2MessageIndex = withdrawResult!.leafIndex; - const withdrawSiblingPath = withdrawResult!.siblingPath; + const withdrawL2MessageIndex = withdrawResult.leafIndex; + const withdrawSiblingPath = withdrawResult.siblingPath; const withdrawMessageMetadata = { _epoch: BigInt(epoch), @@ -840,16 +847,15 @@ export const uniswapL1L2TestSuite = ( chainId: new Fr(l1Client.chain.id), }); - const block = await aztecNode.getBlock(withdrawReceipt.blockNumber!); - const epoch = await rollup.getEpochNumberForCheckpoint(block!.checkpointNumber); - const swapResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, swapPrivateLeaf); - const withdrawResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, withdrawLeaf); + const swapResult = (await computeL2ToL1MembershipWitness(aztecNode, swapPrivateLeaf, withdrawReceipt.txHash))!; + const { epochNumber: epoch } = swapResult; + const withdrawResult = (await computeL2ToL1MembershipWitness(aztecNode, withdrawLeaf, withdrawReceipt.txHash))!; - const swapPrivateL2MessageIndex = swapResult!.leafIndex; - const swapPrivateSiblingPath = swapResult!.siblingPath; + const swapPrivateL2MessageIndex = swapResult.leafIndex; + const swapPrivateSiblingPath = swapResult.siblingPath; - const withdrawL2MessageIndex = withdrawResult!.leafIndex; - const withdrawSiblingPath = withdrawResult!.siblingPath; + const withdrawL2MessageIndex = withdrawResult.leafIndex; + const withdrawSiblingPath = withdrawResult.siblingPath; const withdrawMessageMetadata = { _epoch: BigInt(epoch), @@ -973,16 +979,15 @@ export const uniswapL1L2TestSuite = ( chainId: new Fr(l1Client.chain.id), }); - const block = await aztecNode.getBlock(withdrawReceipt.blockNumber!); - const epoch = await rollup.getEpochNumberForCheckpoint(block!.checkpointNumber); - const swapResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, swapPublicLeaf); - const withdrawResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, withdrawLeaf); + const swapResult = (await computeL2ToL1MembershipWitness(aztecNode, swapPublicLeaf, withdrawReceipt.txHash))!; + const { epochNumber: epoch } = swapResult; + const withdrawResult = (await computeL2ToL1MembershipWitness(aztecNode, withdrawLeaf, withdrawReceipt.txHash))!; - const swapPublicL2MessageIndex = swapResult!.leafIndex; - const swapPublicSiblingPath = swapResult!.siblingPath; + const swapPublicL2MessageIndex = swapResult.leafIndex; + const swapPublicSiblingPath = swapResult.siblingPath; - const withdrawL2MessageIndex = withdrawResult!.leafIndex; - const withdrawSiblingPath = withdrawResult!.siblingPath; + const withdrawL2MessageIndex = withdrawResult.leafIndex; + const withdrawSiblingPath = withdrawResult.siblingPath; const withdrawMessageMetadata = { _epoch: BigInt(epoch), diff --git a/yarn-project/prover-client/src/orchestrator/orchestrator_rollup_structure.test.ts b/yarn-project/prover-client/src/orchestrator/orchestrator_rollup_structure.test.ts index 5ca5d0bcbe66..3ceb5715adc2 100644 --- a/yarn-project/prover-client/src/orchestrator/orchestrator_rollup_structure.test.ts +++ b/yarn-project/prover-client/src/orchestrator/orchestrator_rollup_structure.test.ts @@ -7,7 +7,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; import { createLogger } from '@aztec/foundation/log'; import { Gas, GasFees } from '@aztec/stdlib/gas'; -import { ScopedL2ToL1Message, computeL2ToL1MembershipWitnessFromMessagesInEpoch } from '@aztec/stdlib/messaging'; +import { ScopedL2ToL1Message, computeEpochOutHash } from '@aztec/stdlib/messaging'; import { FeeRecipient } from '@aztec/stdlib/rollup'; import type { ServerCircuitName } from '@aztec/stdlib/stats'; import { makeScopedL2ToL1Message } from '@aztec/stdlib/testing'; @@ -150,11 +150,7 @@ describe('prover/orchestrator/rollup-structure', () => { const epochEndArchive = await getTreeSnapshot(MerkleTreeId.ARCHIVE, await context.worldState.fork()); expect(result.publicInputs.endArchiveRoot).toEqual(epochEndArchive.root); - const firstMessage = l1ToL2MessagesInEpoch.flat(4)[0]; - const { root: epochOutHash } = computeL2ToL1MembershipWitnessFromMessagesInEpoch( - l1ToL2MessagesInEpoch, - firstMessage, - ); + const epochOutHash = computeEpochOutHash(l1ToL2MessagesInEpoch); expect(result.publicInputs.outHash).toEqual(epochOutHash); const expectedCheckpointHeaderHashes = checkpoints.map(c => c.header.hash()); diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index 9afac73c16d5..71eef7dca702 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -254,6 +254,11 @@ describe('AztecNodeApiSchema', () => { expect(response).toEqual([]); }); + it('getCheckpointsDataForEpoch', async () => { + const response = await context.client.getCheckpointsDataForEpoch(EpochNumber(1)); + expect(response).toEqual([]); + }); + it('getNodeVersion', async () => { const response = await context.client.getNodeVersion(); expect(response).toBe('1.0.0'); @@ -537,6 +542,10 @@ class MockAztecNode implements AztecNode { return Promise.resolve([]); } + getCheckpointsDataForEpoch(_epochNumber: EpochNumber) { + return Promise.resolve([]); + } + findLeavesIndexes( referenceBlock: BlockParameter, treeId: MerkleTreeId, diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index 7f613d9e6891..94ec70a55654 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -25,6 +25,7 @@ import { CheckpointedL2Block } from '../block/checkpointed_l2_block.js'; import { type DataInBlock, dataInBlockSchemaFor } from '../block/in_block.js'; import { L2Block } from '../block/l2_block.js'; import { type L2BlockSource, type L2Tips, L2TipsSchema } from '../block/l2_block_source.js'; +import { CheckpointDataSchema } from '../checkpoint/checkpoint_data.js'; import { PublishedCheckpoint } from '../checkpoint/published_checkpoint.js'; import { type ContractClassPublic, @@ -74,7 +75,12 @@ import { type WorldStateSyncStatus, WorldStateSyncStatusSchema } from './world_s export interface AztecNode extends Pick< L2BlockSource, - 'getBlocks' | 'getCheckpoints' | 'getBlockHeader' | 'getL2Tips' | 'getCheckpointedBlocks' + | 'getBlocks' + | 'getCheckpoints' + | 'getBlockHeader' + | 'getL2Tips' + | 'getCheckpointedBlocks' + | 'getCheckpointsDataForEpoch' > { /** * Returns the tips of the L2 chain. @@ -567,6 +573,8 @@ export const AztecNodeApiSchema: ApiSchemaFor = { .args(BlockNumberPositiveSchema, z.number().gt(0).lte(MAX_RPC_BLOCKS_LEN)) .returns(z.array(CheckpointedL2Block.schema)), + getCheckpointsDataForEpoch: z.function().args(EpochNumberSchema).returns(z.array(CheckpointDataSchema)), + getCurrentMinFees: z.function().returns(GasFees.schema), getMaxPriorityFees: z.function().returns(GasFees.schema), diff --git a/yarn-project/stdlib/src/messaging/l2_to_l1_membership.test.ts b/yarn-project/stdlib/src/messaging/l2_to_l1_membership.test.ts index b39cd2e2c7d6..2a6775a64160 100644 --- a/yarn-project/stdlib/src/messaging/l2_to_l1_membership.test.ts +++ b/yarn-project/stdlib/src/messaging/l2_to_l1_membership.test.ts @@ -23,7 +23,7 @@ describe('L2 to L1 membership', () => { const hasher = (left: Buffer, right: Buffer) => sha256Trunc(Buffer.concat([left, right])); // This should match the implementation in Outbox.sol -> verifyMembership - const verifyMembership = (leaf: Fr, witness: L2ToL1MembershipWitness) => { + const verifyMembership = (leaf: Fr, witness: Pick) => { let subtreeRoot = leaf.toBuffer(); let indexAtHeight = witness.leafIndex; const path = witness.siblingPath.toBufferArray(); @@ -35,24 +35,33 @@ describe('L2 to L1 membership', () => { expect(subtreeRoot).toEqual(witness.root.toBuffer()); }; - const verifyMembershipForMessagesInEpoch = (messagesInEpoch: Fr[][][][]): L2ToL1MembershipWitness[] => { + const verifyMembershipForMessagesInEpoch = (messagesInEpoch: Fr[][][][]) => { let root = Fr.ZERO; - const messages = messagesInEpoch.flat(3); - const witnesses = messages.map((msg, i) => { - const witness = computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, msg); - const leafId = getL2ToL1MessageLeafId(witness); - expect(foundLeafIds.has(leafId)).toBe(false); - foundLeafIds.add(leafId); - verifyMembership(msg, witness); - - if (i === 0) { - root = witness.root; - } else { - expect(witness.root).toEqual(root); + const witnesses: ReturnType[] = []; + let isFirst = true; + for (let ci = 0; ci < messagesInEpoch.length; ci++) { + for (let bi = 0; bi < messagesInEpoch[ci].length; bi++) { + for (let ti = 0; ti < messagesInEpoch[ci][bi].length; ti++) { + for (let mi = 0; mi < messagesInEpoch[ci][bi][ti].length; mi++) { + const msg = messagesInEpoch[ci][bi][ti][mi]; + const witness = computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, msg, ci, bi, ti, mi); + const leafId = getL2ToL1MessageLeafId(witness); + expect(foundLeafIds.has(leafId)).toBe(false); + foundLeafIds.add(leafId); + verifyMembership(msg, witness); + + if (isFirst) { + root = witness.root; + isFirst = false; + } else { + expect(witness.root).toEqual(root); + } + + witnesses.push(witness); + } + } } - - return witness; - }); + } const computedRoot = computeEpochOutHash(messagesInEpoch); expect(root).toEqual(computedRoot); @@ -68,11 +77,38 @@ describe('L2 to L1 membership', () => { it('throws if the message is not found', () => { const messagesInEpoch = [[[msgHashes(3), msgHashes(1)]]]; const targetMsg = Fr.random(); - expect(() => computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, targetMsg)).toThrow( + expect(() => computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, targetMsg, 0, 0, 0)).toThrow( 'The L2ToL1Message you are trying to prove inclusion of does not exist', ); }); + it('throws if duplicate messages exist in tx without explicit index', () => { + const msg = Fr.random(); + const messagesInEpoch = [[[[msg, msg, Fr.random()]]]]; + expect(() => computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, msg, 0, 0, 0)).toThrow( + 'Multiple messages with the same value', + ); + }); + + it('succeeds with explicit index for duplicate messages', () => { + const msg = Fr.random(); + const messagesInEpoch = [[[[msg, msg, Fr.random()]]]]; + const witness0 = computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, msg, 0, 0, 0, 0); + const witness1 = computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, msg, 0, 0, 0, 1); + expect(witness0.leafIndex).not.toEqual(witness1.leafIndex); + verifyMembership(msg, witness0); + verifyMembership(msg, witness1); + }); + + it('throws if explicit index does not match the message', () => { + const msg = Fr.random(); + const otherMsg = Fr.random(); + const messagesInEpoch = [[[[otherMsg, msg]]]]; + expect(() => computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, msg, 0, 0, 0, 0)).toThrow( + 'Message at index 0 in tx does not match the expected message', + ); + }); + it('a single tx with 1 message', () => { const txMessages = msgHashes(1); const messagesInEpoch = [[[txMessages]]]; @@ -802,7 +838,7 @@ describe('L2 to L1 membership', () => { [[[], []]], [[[], []], [[]], [[], [], []], [[], []]], ]; - const witness = computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, msg); + const witness = computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, msg, 2, 2, 0, 0); expect(witness.leafIndex).toBe(2n); // The message is the root of the second checkpoint. expect(witness.siblingPath.pathSize).toBe(epochTopTreeDepth); }); diff --git a/yarn-project/stdlib/src/messaging/l2_to_l1_membership.ts b/yarn-project/stdlib/src/messaging/l2_to_l1_membership.ts index e4964abdbaeb..7dbc9eb5c439 100644 --- a/yarn-project/stdlib/src/messaging/l2_to_l1_membership.ts +++ b/yarn-project/stdlib/src/messaging/l2_to_l1_membership.ts @@ -3,6 +3,9 @@ import type { EpochNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { SiblingPath, UnbalancedMerkleTreeCalculator, computeUnbalancedShaRoot } from '@aztec/foundation/trees'; +import type { AztecNode } from '../interfaces/aztec-node.js'; +import { TxHash } from '../tx/tx_hash.js'; + /** * # L2-to-L1 Message Tree Structure and Leaf IDs * @@ -92,59 +95,94 @@ export function getL2ToL1MessageLeafId( return 2n ** BigInt(membershipWitness.siblingPath.pathSize) + membershipWitness.leafIndex; } -export interface MessageRetrieval { - getL2ToL1Messages(epoch: EpochNumber): Promise; -} - export type L2ToL1MembershipWitness = { root: Fr; leafIndex: bigint; siblingPath: SiblingPath; + epochNumber: EpochNumber; }; +/** + * Computes the L2 to L1 membership witness for a given message in a transaction. + * + * @param node - The Aztec node to query for block/tx/epoch data. + * @param message - The L2 to L1 message hash to prove membership of. + * @param txHash - The hash of the transaction that emitted the message. + * @param messageIndexInTx - Optional index of the message within the transaction's L2-to-L1 messages. + * If not provided, the message is found by scanning the tx's messages (throws if duplicates exist). + * @returns The membership witness and epoch number, or undefined if the tx is not yet in a block/epoch. + */ export async function computeL2ToL1MembershipWitness( - messageRetriever: MessageRetrieval, - epoch: EpochNumber, + node: Pick< + AztecNode, + 'getL2ToL1Messages' | 'getTxReceipt' | 'getTxEffect' | 'getBlock' | 'getCheckpointsDataForEpoch' + >, message: Fr, + txHash: TxHash, + messageIndexInTx?: number, ): Promise { - const messagesInEpoch = await messageRetriever.getL2ToL1Messages(epoch); - if (messagesInEpoch.length === 0) { + const { epochNumber, blockNumber } = await node.getTxReceipt(txHash); + if (epochNumber === undefined || blockNumber === undefined) { + return undefined; + } + + const [messagesInEpoch, block, txEffect, checkpointsData] = await Promise.all([ + node.getL2ToL1Messages(epochNumber), + node.getBlock(blockNumber), + node.getTxEffect(txHash), + node.getCheckpointsDataForEpoch(epochNumber), + ]); + + if (messagesInEpoch.length === 0 || !block || !txEffect) { + return undefined; + } + + const checkpointIndex = checkpointsData.findIndex(c => c.checkpointNumber === block.checkpointNumber); + if (checkpointIndex === -1) { return undefined; } - return computeL2ToL1MembershipWitnessFromMessagesInEpoch(messagesInEpoch, message); + const blockIndex = block.indexWithinCheckpoint; + const txIndex = txEffect.txIndexInBlock; + + const { root, leafIndex, siblingPath } = computeL2ToL1MembershipWitnessFromMessagesInEpoch( + messagesInEpoch, + message, + checkpointIndex, + blockIndex, + txIndex, + messageIndexInTx, + ); + return { epochNumber, root, leafIndex, siblingPath }; } -// TODO: Allow to specify the message to consume by its index or by an offset, in case there are multiple messages with -// the same value. +/** + * Computes a membership witness for a message in the epoch's L2-to-L1 message tree, given explicit position indices. + * + * @param messagesInEpoch - All L2-to-L1 messages in the epoch, organized as checkpoints → blocks → txs → messages. + * @param message - The message hash to prove membership of. + * @param checkpointIndex - Index of the checkpoint within the epoch's message array. + * @param blockIndex - Index of the block within the checkpoint. + * @param txIndex - Index of the transaction within the block. + * @param messageIndexInTx - Optional index of the message within the transaction's messages. + * If not provided, the message is found by scanning (throws if duplicates exist within the tx). + */ +/** @internal Exported for testing only. */ export function computeL2ToL1MembershipWitnessFromMessagesInEpoch( messagesInEpoch: Fr[][][][], message: Fr, -): L2ToL1MembershipWitness { - // Find the index of the message in the tx, index of the tx in the block, and index of the block in the epoch. - let messageIndexInTx = -1; - let txIndex = -1; - let blockIndex = -1; - const checkpointIndex = messagesInEpoch.findIndex(messagesInCheckpoint => { - blockIndex = messagesInCheckpoint.findIndex(messagesInBlock => { - txIndex = messagesInBlock.findIndex(messagesInTx => { - messageIndexInTx = messagesInTx.findIndex(msg => msg.equals(message)); - return messageIndexInTx !== -1; - }); - return txIndex !== -1; - }); - return blockIndex !== -1; - }); - - if (checkpointIndex === -1) { - throw new Error('The L2ToL1Message you are trying to prove inclusion of does not exist'); - } + checkpointIndex: number, + blockIndex: number, + txIndex: number, + messageIndexInTx?: number, +): { root: Fr; leafIndex: bigint; siblingPath: SiblingPath } { + const messagesInTx = messagesInEpoch[checkpointIndex][blockIndex][txIndex]; + const resolvedMessageIndex = resolveMessageIndex(messagesInTx, message, messageIndexInTx); // Build the tx tree. - const messagesInTx = messagesInEpoch[checkpointIndex][blockIndex][txIndex]; const txTree = UnbalancedMerkleTreeCalculator.create(messagesInTx.map(msg => msg.toBuffer())); // Get the sibling path of the target message in the tx tree. - const pathToMessageInTxSubtree = txTree.getSiblingPathByLeafIndex(messageIndexInTx); + const pathToMessageInTxSubtree = txTree.getSiblingPathByLeafIndex(resolvedMessageIndex); // Build the tree of the block containing the target message. const blockTree = buildBlockTree(messagesInEpoch[checkpointIndex][blockIndex]); @@ -189,7 +227,7 @@ export function computeL2ToL1MembershipWitnessFromMessagesInEpoch( // Compute the combined index. // It is the index of the message in the balanced tree (by filling up the wonky tree with empty nodes) at its current // height. It's used to validate the membership proof. - const messageLeafPosition = txTree.getLeafLocation(messageIndexInTx); + const messageLeafPosition = txTree.getLeafLocation(resolvedMessageIndex); const txLeafPosition = blockTree.getLeafLocation(txIndex); const blockLeafPosition = checkpointTree.getLeafLocation(blockIndex); const checkpointLeafPosition = epochTree.getLeafLocation(checkpointIndex); @@ -207,6 +245,33 @@ export function computeL2ToL1MembershipWitnessFromMessagesInEpoch( }; } +function resolveMessageIndex(messagesInTx: Fr[], message: Fr, messageIndexInTx?: number): number { + if (messageIndexInTx !== undefined) { + if (!messagesInTx[messageIndexInTx]?.equals(message)) { + throw new Error(`Message at index ${messageIndexInTx} in tx does not match the expected message ${message}`); + } + return messageIndexInTx; + } + + const indices = messagesInTx.reduce((acc, msg, i) => { + if (msg.equals(message)) { + acc.push(i); + } + return acc; + }, []); + + if (indices.length === 0) { + throw new Error('The L2ToL1Message you are trying to prove inclusion of does not exist'); + } + if (indices.length > 1) { + throw new Error( + `Multiple messages with the same value ${message} found in tx (indices: ${indices.join(', ')}). ` + + `Provide messageIndexInTx to disambiguate.`, + ); + } + return indices[0]; +} + function buildCheckpointTree(messagesInCheckpoint: Fr[][][]) { const blockOutHashes = messagesInCheckpoint.map(messagesInBlock => buildBlockTree(messagesInBlock).getRoot()); return buildCompressedTree(blockOutHashes); diff --git a/yarn-project/stdlib/src/tx/tx_receipt.test.ts b/yarn-project/stdlib/src/tx/tx_receipt.test.ts index 9fe953452242..8be605399c4f 100644 --- a/yarn-project/stdlib/src/tx/tx_receipt.test.ts +++ b/yarn-project/stdlib/src/tx/tx_receipt.test.ts @@ -1,4 +1,4 @@ -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types'; import { jsonStringify } from '@aztec/foundation/json-rpc'; import { BlockHash } from '../block/block_hash.js'; @@ -15,6 +15,7 @@ describe('TxReceipt', () => { 1n, BlockHash.random(), BlockNumber(1), + EpochNumber(3), ); expect(TxReceipt.schema.parse(JSON.parse(jsonStringify(receipt)))).toEqual(receipt); diff --git a/yarn-project/stdlib/src/tx/tx_receipt.ts b/yarn-project/stdlib/src/tx/tx_receipt.ts index 4cdb8f0eb6ed..c222de9c906e 100644 --- a/yarn-project/stdlib/src/tx/tx_receipt.ts +++ b/yarn-project/stdlib/src/tx/tx_receipt.ts @@ -1,4 +1,4 @@ -import { BlockNumber, BlockNumberSchema } from '@aztec/foundation/branded-types'; +import { BlockNumber, BlockNumberSchema, EpochNumber, EpochNumberSchema } from '@aztec/foundation/branded-types'; import { z } from 'zod'; @@ -58,6 +58,8 @@ export class TxReceipt { public blockHash?: BlockHash, /** The block number in which the transaction was included. */ public blockNumber?: BlockNumber, + /** The epoch number in which the transaction was included. */ + public epochNumber?: EpochNumber, /** * Debug logs collected during public function execution. Served only when the node is in test mode and placed on * the receipt only because it's a convenient place for it (the logs are printed out by the wallet when a mined @@ -109,6 +111,7 @@ export class TxReceipt { error: z.string().optional(), blockHash: BlockHash.schema.optional(), blockNumber: BlockNumberSchema.optional(), + epochNumber: EpochNumberSchema.optional(), transactionFee: schemas.BigInt.optional(), debugLogs: z.array(DebugLog.schema).optional(), }) @@ -123,6 +126,7 @@ export class TxReceipt { transactionFee?: bigint; blockHash?: BlockHash; blockNumber?: BlockNumber; + epochNumber?: EpochNumber; debugLogs?: DebugLog[]; }) { return new TxReceipt( @@ -133,6 +137,7 @@ export class TxReceipt { fields.transactionFee, fields.blockHash, fields.blockNumber, + fields.epochNumber, fields.debugLogs, ); }