diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index e38996d04e99..9b157e0be4e7 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,79 @@ 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: + +- Private external functions check the private init nullifier. +- Public external functions check the public init nullifier. + +**How initializers work:** + +- **Private initializers** emit the private init nullifier. If the contract has any external public functions, the protocol auto-enqueues a public call to emit the public init nullifier. +- **Public initializers** emit both nullifiers directly. +- Contracts with no public functions only emit the private init nullifier. + +**`only_self` functions no longer have init checks.** They behave as if marked `noinitcheck`. + +**External functions called during private initialization must be `#[only_self]`.** Init nullifiers are emitted at the end of the initializer, so any external functions called on the initializing contract (e.g. via `enqueue_self` or `call_self`) during initialization will fail the init check unless they skip it. + +**Breaking change for deployment:** If your contract has external public functions and a private initializer, the class must be registered onchain before initialization. You can no longer pass `skipClassPublication: true`, because the auto-enqueued public call requires the class to be available. + +```diff + const deployed = await MyContract.deploy(wallet, ...args).send({ +- skipClassPublication: true, + }).deployed(); +``` + +### [Aztec.nr] Made `compute_note_hash_for_nullification` unconstrained + +This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous (constrained computation of nullifiers can be performed only on the `ConfirmedNote` type). If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe. + +### [Aztec.nr] Changes to standard note hash computation + +Note hashes used to be computed with the storage slot being the last value of the preimage, it is now the first. This is to make it easier to ensure all note hashes have proper domain separation. + +This change requires no input from your side unless you were testing or relying on hardcoded note hashes. + +### [Aztec.js] `getPublicEvents` now returns an object instead of an array + +`getPublicEvents` now returns a `GetPublicEventsResult` object with `events` and `maxLogsHit` fields instead of a plain array. This enables pagination through large result sets using the new `afterLog` filter option. + +```diff +- const events = await getPublicEvents(node, MyContract.events.MyEvent, filter); ++ const { events } = await getPublicEvents(node, MyContract.events.MyEvent, filter); +``` + +The `maxLogsHit` flag indicates whether the log limit was reached, meaning more results may be available. You can use `afterLog` in the filter to fetch the next page. + +### [Aztec.nr] Removed `get_random_bytes` + +The `get_random_bytes` unconstrained function has been removed from `aztec::utils::random`. If you were using it, you can replace it with direct calls to the `random` oracle from `aztec::oracle::random` and convert to bytes yourself. + ### [Aztec.js] `simulate()`, `send()`, and deploy return types changed to always return objects All SDK interaction methods now return structured objects that include offchain output alongside the primary result. This affects `.simulate()`, `.send()`, deploy `.send()`, and `Wallet.sendTx()`. 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 7a8cfc85f238..5da56c656be8 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 a9ec9a501c85..9b2ee842f095 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -20,7 +20,7 @@ import { serializeValidateCheckpointResult, } from '@aztec/stdlib/block'; import { type CheckpointData, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; -import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; +import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; import { @@ -871,7 +871,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; @@ -880,10 +883,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; @@ -897,6 +901,9 @@ export class BlockStore { status = TxStatus.PROPOSED; } + const epochNumber = + blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined; + return new TxReceipt( txHash, status, @@ -905,6 +912,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 d46075e2a588..b4c25793c4da 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.ts @@ -410,8 +410,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 da295c09cb96..6c804faa9931 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 4ea7c746cb74..da33a7d9d1dd 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 3b67b0057ba5..8a4796e04048 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, ); }