From eb701dde5d9478e9d537b18003559f01cf0f504a Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 13 May 2025 13:09:16 +0000 Subject: [PATCH 1/4] test: merging e2e_outbox with l2_to_l1 test --- .../cross_chain_messaging_test.ts | 17 +- .../l2_to_l1.test.ts | 410 +++++++++++++++++- 2 files changed, 413 insertions(+), 14 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts index afecfe3a94c1..a509d3244c16 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts @@ -11,7 +11,12 @@ import { createLogger, } from '@aztec/aztec.js'; import { CheatCodes } from '@aztec/aztec.js/testing'; -import { type ExtendedViemWalletClient, createExtendedL1Client, deployL1Contract } from '@aztec/ethereum'; +import { + type DeployL1ContractsReturnType, + type ExtendedViemWalletClient, + createExtendedL1Client, + deployL1Contract, +} from '@aztec/ethereum'; import { InboxAbi, OutboxAbi, TestERC20Abi, TestERC20Bytecode } from '@aztec/l1-artifacts'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { TokenBridgeContract } from '@aztec/noir-contracts.js/TokenBridge'; @@ -51,7 +56,9 @@ export class CrossChainMessagingTest { inbox!: any; // GetContractReturnType | undefined; outbox!: any; // GetContractReturnType | undefined; - cheatcodes!: CheatCodes; + cheatCodes!: CheatCodes; + + deployL1ContractsValues!: DeployL1ContractsReturnType; constructor(testName: string) { this.logger = createLogger(`e2e:e2e_cross_chain_messaging:${testName}`); @@ -59,15 +66,15 @@ export class CrossChainMessagingTest { } async assumeProven() { - await this.cheatcodes.rollup.markAsProven(); + await this.cheatCodes.rollup.markAsProven(); } async setup() { - const { aztecNode, pxe, aztecNodeConfig } = await this.snapshotManager.setup(); + const { aztecNode, pxe, aztecNodeConfig, deployL1ContractsValues } = await this.snapshotManager.setup(); this.aztecNode = aztecNode; this.pxe = pxe; this.aztecNodeConfig = aztecNodeConfig; - this.cheatcodes = await CheatCodes.create(this.aztecNodeConfig.l1RpcUrls, this.pxe); + this.cheatCodes = await CheatCodes.create(this.aztecNodeConfig.l1RpcUrls, this.pxe); } snapshot = ( 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 68343082bade..e47184120d3b 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 @@ -1,6 +1,7 @@ -import { Fr } from '@aztec/aztec.js'; +import { BatchCall, EthAddress, Fr, SiblingPath } from '@aztec/aztec.js'; import { RollupContract } from '@aztec/ethereum'; import { sha256ToField } from '@aztec/foundation/crypto'; +import { truncateAndPad } from '@aztec/foundation/serialize'; import { OutboxAbi } from '@aztec/l1-artifacts'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; @@ -11,10 +12,11 @@ import { CrossChainMessagingTest } from './cross_chain_messaging_test.js'; describe('e2e_cross_chain_messaging l2_to_l1', () => { const t = new CrossChainMessagingTest('l2_to_l1'); - let version: number = 1; - let { crossChainTestHarness, aztecNode, user1Wallet, outbox } = t; + let version: number = 1; + let contract: TestContract; + beforeAll(async () => { await t.applyBaseSnapshots(); await t.setup(); @@ -35,6 +37,8 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { crossChainTestHarness.l1ContractAddresses.rollupAddress.toString(), ).getVersion(), ); + + contract = await TestContract.deploy(user1Wallet).send().deployed(); }, 300_000); afterAll(async () => { @@ -46,8 +50,6 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { it.each([[true], [false]])( `can send an L2 -> L1 message to a non-registered portal address from public or private`, async (isPrivate: boolean) => { - const testContract = await TestContract.deploy(user1Wallet).send().deployed(); - const content = Fr.random(); const recipient = crossChainTestHarness.ethAccount; @@ -55,19 +57,19 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { // We create the L2 -> L1 message using the test contract if (isPrivate) { - l2TxReceipt = await testContract.methods + l2TxReceipt = await contract.methods .create_l2_to_l1_message_arbitrary_recipient_private(content, recipient) .send() .wait(); } else { - l2TxReceipt = await testContract.methods + l2TxReceipt = await contract.methods .create_l2_to_l1_message_arbitrary_recipient_public(content, recipient) .send() .wait(); } const l2ToL1Message = { - sender: { actor: testContract.address.toString() as Hex, version: BigInt(version) }, + sender: { actor: contract.address.toString() as Hex, version: BigInt(version) }, recipient: { actor: recipient.toString() as Hex, chainId: BigInt(crossChainTestHarness.l1Client.chain.id), @@ -76,7 +78,7 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { }; const leaf = sha256ToField([ - testContract.address, + contract.address, new Fr(version), // aztec version recipient.toBuffer32(), new Fr(crossChainTestHarness.l1Client.chain.id), // chain id @@ -129,4 +131,394 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { }, 60_000, ); + + it('Inserts a new transaction with two out messages, and verifies sibling paths of both the new messages', async () => { + // recipient2 = msg.sender, so we can consume it later + const [[recipient1, content1], [recipient2, content2]] = [ + [EthAddress.random(), Fr.random()], + [EthAddress.fromString(t.deployL1ContractsValues.l1Client.account.address), Fr.random()], + ]; + + const call = new BatchCall(user1Wallet, [ + contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content1, recipient1), + contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content2, recipient2), + ]); + + // TODO (#5104): When able to guarantee multiple txs in a single block, make this populate a full tree. Right now we are + // unable to do this because in CI, for some reason, the tx's are handled in different blocks, so it is impossible + // to make a full tree of L2 -> L1 messages as we are only able to set one tx's worth of L1 -> L2 messages in a block (2 messages out of 4) + const txReceipt = await call.send().wait(); + + const block = await aztecNode.getBlock(txReceipt.blockNumber!); + + const l2ToL1Messages = block?.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs); + + expect(l2ToL1Messages?.map(l2ToL1Message => l2ToL1Message.toString())).toStrictEqual( + [makeL2ToL1Message(recipient1, content1), makeL2ToL1Message(recipient2, content2)].map(expectedL2ToL1Message => + expectedL2ToL1Message.toString(), + ), + ); + + // For each individual message, we are using our node API to grab the index and sibling path. We expect + // the index to match the order of the block we obtained earlier. We also then use this sibling path to hash up to the root, + // verifying that the expected root obtained through the message and the sibling path match the actual root + // that was returned by the circuits in the header as out_hash. + const [index, siblingPath] = await aztecNode.getL2ToL1MessageMembershipWitness( + txReceipt.blockNumber!, + l2ToL1Messages![0], + ); + expect(siblingPath.pathSize).toBe(1); + expect(index).toBe(0n); + const expectedRoot = calculateExpectedRoot(l2ToL1Messages![0], siblingPath, index); + expect(expectedRoot.toString('hex')).toEqual(block?.header.contentCommitment.outHash.toString('hex')); + + const [index2, siblingPath2] = await aztecNode.getL2ToL1MessageMembershipWitness( + txReceipt.blockNumber!, + l2ToL1Messages![1], + ); + expect(siblingPath2.pathSize).toBe(1); + expect(index2).toBe(1n); + const expectedRoot2 = calculateExpectedRoot(l2ToL1Messages![1], siblingPath2, index2); + expect(expectedRoot2.toString('hex')).toEqual(block?.header.contentCommitment.outHash.toString('hex')); + + // Outbox L1 tests + + // Since the outbox is only consumable when the block is proven, we need to set the block to be proven + await t.cheatCodes.rollup.markAsProven(txReceipt.blockNumber ?? 0); + + // Check L1 has expected message tree + const [l1Root, l1MinHeight] = await outbox.read.getRootData([txReceipt.blockNumber]); + expect(l1Root).toEqual(`0x${block?.header.contentCommitment.outHash.toString('hex')}`); + // The path for the message should have the shortest possible height, since we only have 2 msgs + expect(l1MinHeight).toEqual(BigInt(siblingPath.pathSize)); + + // Consume msg 2 + // Taken from l2_to_l1.test + const msg2 = { + sender: { actor: contract.address.toString() as `0x${string}`, version: BigInt(version) }, + recipient: { + actor: recipient2.toString() as `0x${string}`, + chainId: BigInt(t.deployL1ContractsValues.l1Client.chain.id), + }, + content: content2.toString() as `0x${string}`, + }; + + const txHash = await outbox.write.consume( + [ + msg2, + BigInt(txReceipt.blockNumber!), + BigInt(index2), + siblingPath2.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + ], + {} as any, + ); + const l1Receipt = await t.deployL1ContractsValues.l1Client.waitForTransactionReceipt({ + hash: txHash, + }); + // Consume call goes through + expect(l1Receipt.status).toEqual('success'); + + const txLog = l1Receipt.logs[0]; + const topics = decodeEventLog({ + abi: OutboxAbi, + data: txLog.data, + topics: txLog.topics, + }) as { + eventName: 'MessageConsumed'; + args: { + l2BlockNumber: bigint; + root: `0x${string}`; + messageHash: `0x${string}`; + leafIndex: bigint; + }; + }; + // Consumed the expected message + expect(topics.args.messageHash).toStrictEqual(l2ToL1Messages?.[1].toString()); + expect(topics.args.leafIndex).toStrictEqual(BigInt(index2)); + + const consumeAgain = outbox.write.consume( + [ + msg2, + BigInt(txReceipt.blockNumber!), + BigInt(index2), + siblingPath2.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + ], + {} as any, + ); + // Ensuring we cannot consume the same message again + await expect(consumeAgain).rejects.toThrow(); + }); + + it('Inserts two transactions with total four out messages, and verifies sibling paths of two new messages', async () => { + // Force txs to be in the same block + await aztecNodeAdmin!.setConfig({ minTxsPerBlock: 2 }); + const [[recipient1, content1], [recipient2, content2], [recipient3, content3], [recipient4, content4]] = [ + [EthAddress.random(), Fr.random()], + [EthAddress.fromString(t.deployL1ContractsValues.l1Client.account.address), Fr.random()], + [EthAddress.random(), Fr.random()], + [EthAddress.random(), Fr.random()], + ]; + + const call0 = new BatchCall(user1Wallet, [ + contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content1, recipient1), + contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content2, recipient2), + contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content3, recipient3), + ]); + + const call1 = contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content4, recipient4); + + const [l2TxReceipt0, l2TxReceipt1] = await Promise.all([call0.send().wait(), call1.send().wait()]); + expect(l2TxReceipt0.blockNumber).toEqual(l2TxReceipt1.blockNumber); + + const block = await aztecNode.getBlock(l2TxReceipt0.blockNumber!); + + const l2ToL1Messages = block?.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs); + // Not checking strict equality as ordering is not guaranteed - this should be covered in that we can recalculate the out hash below + expect(l2ToL1Messages?.length).toEqual(4); + + // For each individual message, we are using our node API to grab the index and sibling path. We expect + // the index to match the order of the block we obtained earlier. We also then use this sibling path to hash up to the root, + // verifying that the expected root obtained through the message and the sibling path match the actual root + // that was returned by the circuits in the header as out_hash. + const singleMessage = makeL2ToL1Message(recipient4, content4); + const [index, siblingPath] = await aztecNode.getL2ToL1MessageMembershipWitness( + l2TxReceipt0.blockNumber!, + singleMessage, + ); + // The solo message is the only one in the tx, so it only requires a subtree of height 1 + // +1 for being rolled up + expect(siblingPath.pathSize).toBe(2); + const expectedRoot = calculateExpectedRoot(singleMessage, siblingPath as SiblingPath<2>, index); + expect(expectedRoot.toString('hex')).toEqual(block?.header.contentCommitment.outHash.toString('hex')); + + const messageToConsume = makeL2ToL1Message(recipient2, content2); + const [index2, siblingPath2] = await aztecNode.getL2ToL1MessageMembershipWitness( + l2TxReceipt0.blockNumber!, + messageToConsume, + ); + // This message is in a group of 3, => it needs a subtree of height 2 + // +1 for being rolled up + expect(siblingPath2.pathSize).toBe(3); + + // Outbox L1 tests + // Since the outbox is only consumable when the block is proven, we need to set the block to be proven + await t.cheatCodes.rollup.markAsProven(l2TxReceipt0.blockNumber ?? 0); + + // Check L1 has expected message tree + const [l1Root, l1MinHeight] = await outbox.read.getRootData([l2TxReceipt0.blockNumber]); + expect(l1Root).toEqual(`0x${block?.header.contentCommitment.outHash.toString('hex')}`); + + // The path for the single message should have the shortest possible height + expect(l1MinHeight).toEqual(BigInt(siblingPath.pathSize)); + + // Consume msg 2 + // Taken from l2_to_l1.test + const msg2 = { + sender: { actor: contract.address.toString() as `0x${string}`, version: BigInt(version) }, + recipient: { + actor: recipient2.toString() as `0x${string}`, + chainId: BigInt(t.deployL1ContractsValues.l1Client.chain.id), + }, + content: content2.toString() as `0x${string}`, + }; + + const txHash = await outbox.write.consume( + [ + msg2, + BigInt(l2TxReceipt0.blockNumber!), + BigInt(index2), + siblingPath2.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + ], + {} as any, + ); + const l1Receipt = await t.deployL1ContractsValues.l1Client.waitForTransactionReceipt({ + hash: txHash, + }); + // Consume call goes through + expect(l1Receipt.status).toEqual('success'); + + const txLog = l1Receipt.logs[0]; + const topics = decodeEventLog({ + abi: OutboxAbi, + data: txLog.data, + topics: txLog.topics, + }) as { + eventName: 'MessageConsumed'; + args: { + l2BlockNumber: bigint; + root: `0x${string}`; + messageHash: `0x${string}`; + leafIndex: bigint; + }; + }; + // Consumed the expected message + expect(topics.args.messageHash).toStrictEqual(messageToConsume.toString()); + expect(topics.args.leafIndex).toStrictEqual(BigInt(index2)); + + const consumeAgain = outbox.write.consume( + [ + msg2, + BigInt(l2TxReceipt0.blockNumber!), + BigInt(index2), + siblingPath2.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + ], + {} as any, + ); + // Ensuring we cannot consume the same message again + await expect(consumeAgain).rejects.toThrow(); + }); + + it('Inserts two out messages in two transactions and verifies sibling paths of both the new messages', async () => { + // Force txs to be in the same block + await aztecNodeAdmin!.setConfig({ minTxsPerBlock: 2 }); + // recipient2 = msg.sender, so we can consume it later + const [[recipient1, content1], [recipient2, content2]] = [ + [EthAddress.random(), Fr.random()], + [EthAddress.fromString(t.deployL1ContractsValues.l1Client.account.address), Fr.random()], + ]; + + const call0 = contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content1, recipient1); + const call1 = contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content2, recipient2); + + // resolve together to force the txs to be in the same block + const [l2TxReceipt0, l2TxReceipt1] = await Promise.all([call0.send().wait(), call1.send().wait()]); + expect(l2TxReceipt0.blockNumber).toEqual(l2TxReceipt1.blockNumber); + + const block = await aztecNode.getBlock(l2TxReceipt0.blockNumber!); + + const l2ToL1Messages = block?.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs); + const messageToConsume = makeL2ToL1Message(recipient2, content2); + + // We cannot guarantee the order of txs in blocks + expect( + l2ToL1Messages?.map(l2ToL1Message => + l2ToL1Message.toString().includes(makeL2ToL1Message(recipient1, content1).toString()), + ), + ); + expect(l2ToL1Messages?.map(l2ToL1Message => l2ToL1Message.toString().includes(messageToConsume.toString()))); + + // For each individual message, we are using our node API to grab the index and sibling path. We expect + // the index to match the order of the block we obtained earlier. We also then use this sibling path to hash up to the root, + // verifying that the expected root obtained through the message and the sibling path match the actual root + // that was returned by the circuits in the header as out_hash. + const [index, siblingPath] = await aztecNode.getL2ToL1MessageMembershipWitness( + l2TxReceipt0.blockNumber!, + l2ToL1Messages![0], + ); + expect(siblingPath.pathSize).toBe(2); + // We can only confirm the below index because we have taken the msg hash as the first of the block.body + // It is not necessarily the msg constructed from [recipient1, content1] above + expect(index).toBe(0n); + + const [index2, siblingPath2] = await aztecNode.getL2ToL1MessageMembershipWitness( + l2TxReceipt0.blockNumber!, + l2ToL1Messages![1], + ); + expect(siblingPath2.pathSize).toBe(2); + // See above comment for confirming index + expect(index2).toBe(2n); + + // Outbox L1 tests + // Since the outbox is only consumable when the block is proven, we need to set the block to be proven + await t.cheatCodes.rollup.markAsProven(l2TxReceipt0.blockNumber ?? 0); + + // Check L1 has expected message tree + const [l1Root, l1MinHeight] = await outbox.read.getRootData([l2TxReceipt0.blockNumber]); + expect(l1Root).toEqual(`0x${block?.header.contentCommitment.outHash.toString('hex')}`); + // The path for the message should have the shortest possible height, since we only have one msg per tx + expect(l1MinHeight).toEqual(BigInt(siblingPath.pathSize)); + + // Consume msg 2 + // Taken from l2_to_l1.test + const msg2 = { + sender: { actor: contract.address.toString() as `0x${string}`, version: BigInt(version) }, + recipient: { + actor: recipient2.toString() as `0x${string}`, + chainId: BigInt(t.deployL1ContractsValues.l1Client.chain.id), + }, + content: content2.toString() as `0x${string}`, + }; + const [inputIndex, inputPath] = messageToConsume.equals(l2ToL1Messages![0]) + ? [index, siblingPath] + : [index2, siblingPath2]; + const txHash = await outbox.write.consume( + [ + msg2, + BigInt(l2TxReceipt0.blockNumber!), + BigInt(inputIndex), + inputPath.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + ], + {} as any, + ); + const l1Receipt = await t.deployL1ContractsValues.l1Client.waitForTransactionReceipt({ + hash: txHash, + }); + // Consume call goes through + expect(l1Receipt.status).toEqual('success'); + + const txLog = l1Receipt.logs[0]; + const topics = decodeEventLog({ + abi: OutboxAbi, + data: txLog.data, + topics: txLog.topics, + }) as { + eventName: 'MessageConsumed'; + args: { + l2BlockNumber: bigint; + root: `0x${string}`; + messageHash: `0x${string}`; + leafIndex: bigint; + }; + }; + // Consumed the expected message + expect(topics.args.messageHash).toStrictEqual(messageToConsume.toString()); + expect(topics.args.leafIndex).toStrictEqual(BigInt(inputIndex)); + + const consumeAgain = outbox.write.consume( + [ + msg2, + BigInt(l2TxReceipt0.blockNumber!), + BigInt(index2), + siblingPath2.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + ], + {} as any, + ); + // Ensuring we cannot consume the same message again + await expect(consumeAgain).rejects.toThrow(); + }); + + function calculateExpectedRoot( + l2ToL1Message: Fr, + siblingPath: SiblingPath, + index: bigint, + ): Buffer { + const firstLayerInput: [Buffer, Buffer] = + index & 0x1n + ? [siblingPath.toBufferArray()[0], l2ToL1Message.toBuffer()] + : [l2ToL1Message.toBuffer(), siblingPath.toBufferArray()[0]]; + const firstLayer = merkleSha256.hash(...firstLayerInput); + if (siblingPath.pathSize === 1) { + return truncateAndPad(firstLayer); + } + index /= 2n; + // In the circuit, the 'firstLayer' is the kernel out hash, which is truncated to 31 bytes + // To match the result, the below preimages and the output are truncated to 31 then padded + const secondLayerInput: [Buffer, Buffer] = + index & 0x1n + ? [siblingPath.toBufferArray()[1], truncateAndPad(firstLayer)] + : [truncateAndPad(firstLayer), siblingPath.toBufferArray()[1]]; + return truncateAndPad(merkleSha256.hash(...secondLayerInput)); + } + + function makeL2ToL1Message(recipient: EthAddress, content: Fr = Fr.ZERO): Fr { + const leaf = sha256ToField([ + contract.address, + new Fr(version), + recipient.toBuffer32(), + new Fr(t.deployL1ContractsValues.l1Client.chain.id), // chain id + content, + ]); + + return leaf; + } }); From 828bfeb6d66d38b9ef0f1115ef4e5edacb74740d Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 13 May 2025 13:31:41 +0000 Subject: [PATCH 2/4] made the test pass --- .../cross_chain_messaging_test.ts | 4 ++++ .../l2_to_l1.test.ts | 20 +++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts index a509d3244c16..d35013cfb2ce 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts @@ -20,6 +20,7 @@ import { import { InboxAbi, OutboxAbi, TestERC20Abi, TestERC20Bytecode } from '@aztec/l1-artifacts'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { TokenBridgeContract } from '@aztec/noir-contracts.js/TokenBridge'; +import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { getContract } from 'viem'; @@ -43,6 +44,7 @@ export class CrossChainMessagingTest { aztecNode!: AztecNode; pxe!: PXE; aztecNodeConfig!: AztecNodeConfig; + aztecNodeAdmin!: AztecNodeAdmin; l1Client!: ExtendedViemWalletClient | undefined; @@ -75,6 +77,8 @@ export class CrossChainMessagingTest { this.pxe = pxe; this.aztecNodeConfig = aztecNodeConfig; this.cheatCodes = await CheatCodes.create(this.aztecNodeConfig.l1RpcUrls, this.pxe); + this.deployL1ContractsValues = deployL1ContractsValues; + this.aztecNodeAdmin = aztecNode; } snapshot = ( 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 e47184120d3b..c131a8955389 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 @@ -1,18 +1,27 @@ -import { BatchCall, EthAddress, Fr, SiblingPath } from '@aztec/aztec.js'; +import { BatchCall, EthAddress, Fr, SiblingPath, type Wallet } from '@aztec/aztec.js'; import { RollupContract } from '@aztec/ethereum'; import { sha256ToField } from '@aztec/foundation/crypto'; import { truncateAndPad } from '@aztec/foundation/serialize'; import { OutboxAbi } from '@aztec/l1-artifacts'; +import { SHA256 } from '@aztec/merkle-tree'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; +import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { type Hex, decodeEventLog, getContract } from 'viem'; +import type { CrossChainTestHarness } from '../shared/cross_chain_test_harness.js'; import { CrossChainMessagingTest } from './cross_chain_messaging_test.js'; describe('e2e_cross_chain_messaging l2_to_l1', () => { const t = new CrossChainMessagingTest('l2_to_l1'); - let { crossChainTestHarness, aztecNode, user1Wallet, outbox } = t; + const merkleSha256 = new SHA256(); + + let crossChainTestHarness: CrossChainTestHarness; + let aztecNode: AztecNode; + let aztecNodeAdmin: AztecNodeAdmin; + let user1Wallet: Wallet; + let outbox: any; let version: number = 1; let contract: TestContract; @@ -20,10 +29,7 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { beforeAll(async () => { await t.applyBaseSnapshots(); await t.setup(); - // Have to destructure again to ensure we have latest refs. - ({ crossChainTestHarness, user1Wallet } = t); - - aztecNode = crossChainTestHarness.aztecNode; + ({ crossChainTestHarness, aztecNode, aztecNodeAdmin, user1Wallet } = t); outbox = getContract({ address: crossChainTestHarness.l1ContractAddresses.outboxAddress.toString(), @@ -250,6 +256,8 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { }); it('Inserts two transactions with total four out messages, and verifies sibling paths of two new messages', async () => { + console.log('Admin', aztecNodeAdmin); + // Force txs to be in the same block await aztecNodeAdmin!.setConfig({ minTxsPerBlock: 2 }); const [[recipient1, content1], [recipient2, content2], [recipient3, content3], [recipient4, content4]] = [ From cf4eaa4526d20018b441503c86b07ba5faaa0f5a Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 13 May 2025 13:32:05 +0000 Subject: [PATCH 3/4] nuking e2e outbox --- .../end-to-end/src/e2e_outbox.test.ts | 444 ------------------ 1 file changed, 444 deletions(-) delete mode 100644 yarn-project/end-to-end/src/e2e_outbox.test.ts diff --git a/yarn-project/end-to-end/src/e2e_outbox.test.ts b/yarn-project/end-to-end/src/e2e_outbox.test.ts deleted file mode 100644 index 72cb57fc05ba..000000000000 --- a/yarn-project/end-to-end/src/e2e_outbox.test.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { - type AccountWalletWithSecretKey, - type AztecNode, - BatchCall, - EthAddress, - Fr, - type SiblingPath, -} from '@aztec/aztec.js'; -import { CheatCodes } from '@aztec/aztec.js/testing'; -import { type DeployL1ContractsReturnType, RollupContract } from '@aztec/ethereum'; -import { sha256ToField } from '@aztec/foundation/crypto'; -import { truncateAndPad } from '@aztec/foundation/serialize'; -import { OutboxAbi } from '@aztec/l1-artifacts'; -import { SHA256 } from '@aztec/merkle-tree'; -import { TestContract } from '@aztec/noir-test-contracts.js/Test'; -import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; - -import { beforeEach, describe, expect, it } from '@jest/globals'; -import { decodeEventLog, getContract } from 'viem'; - -import { setup } from './fixtures/utils.js'; - -describe('E2E Outbox Tests', () => { - let teardown: () => void; - let aztecNode: AztecNode; - let aztecNodeAdmin: AztecNodeAdmin | undefined; - const merkleSha256 = new SHA256(); - let contract: TestContract; - let wallets: AccountWalletWithSecretKey[]; - let deployL1ContractsValues: DeployL1ContractsReturnType; - let outbox: any; - let cheatCodes: CheatCodes; - let version: number = 1; - - beforeEach(async () => { - ({ teardown, aztecNode, wallets, deployL1ContractsValues, cheatCodes, aztecNodeAdmin } = await setup(1)); - outbox = getContract({ - address: deployL1ContractsValues.l1ContractAddresses.outboxAddress.toString(), - abi: OutboxAbi, - client: deployL1ContractsValues.l1Client, - }); - - const rollup = new RollupContract( - deployL1ContractsValues.l1Client, - deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), - ); - version = Number(await rollup.getVersion()); - - const receipt = await TestContract.deploy(wallets[0]).send({ contractAddressSalt: Fr.ZERO }).wait(); - contract = receipt.contract; - }); - - afterEach(() => teardown()); - - it('Inserts a new transaction with two out messages, and verifies sibling paths of both the new messages', async () => { - // recipient2 = msg.sender, so we can consume it later - const [[recipient1, content1], [recipient2, content2]] = [ - [EthAddress.random(), Fr.random()], - [EthAddress.fromString(deployL1ContractsValues.l1Client.account.address), Fr.random()], - ]; - - const call = new BatchCall(wallets[0], [ - contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content1, recipient1), - contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content2, recipient2), - ]); - - // TODO (#5104): When able to guarantee multiple txs in a single block, make this populate a full tree. Right now we are - // unable to do this because in CI, for some reason, the tx's are handled in different blocks, so it is impossible - // to make a full tree of L2 -> L1 messages as we are only able to set one tx's worth of L1 -> L2 messages in a block (2 messages out of 4) - const txReceipt = await call.send().wait(); - - const block = await aztecNode.getBlock(txReceipt.blockNumber!); - - const l2ToL1Messages = block?.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs); - - expect(l2ToL1Messages?.map(l2ToL1Message => l2ToL1Message.toString())).toStrictEqual( - [makeL2ToL1Message(recipient1, content1), makeL2ToL1Message(recipient2, content2)].map(expectedL2ToL1Message => - expectedL2ToL1Message.toString(), - ), - ); - - // For each individual message, we are using our node API to grab the index and sibling path. We expect - // the index to match the order of the block we obtained earlier. We also then use this sibling path to hash up to the root, - // verifying that the expected root obtained through the message and the sibling path match the actual root - // that was returned by the circuits in the header as out_hash. - const [index, siblingPath] = await aztecNode.getL2ToL1MessageMembershipWitness( - txReceipt.blockNumber!, - l2ToL1Messages![0], - ); - expect(siblingPath.pathSize).toBe(1); - expect(index).toBe(0n); - const expectedRoot = calculateExpectedRoot(l2ToL1Messages![0], siblingPath, index); - expect(expectedRoot.toString('hex')).toEqual(block?.header.contentCommitment.outHash.toString('hex')); - - const [index2, siblingPath2] = await aztecNode.getL2ToL1MessageMembershipWitness( - txReceipt.blockNumber!, - l2ToL1Messages![1], - ); - expect(siblingPath2.pathSize).toBe(1); - expect(index2).toBe(1n); - const expectedRoot2 = calculateExpectedRoot(l2ToL1Messages![1], siblingPath2, index2); - expect(expectedRoot2.toString('hex')).toEqual(block?.header.contentCommitment.outHash.toString('hex')); - - // Outbox L1 tests - - // Since the outbox is only consumable when the block is proven, we need to set the block to be proven - await cheatCodes.rollup.markAsProven(txReceipt.blockNumber ?? 0); - - // Check L1 has expected message tree - const [l1Root, l1MinHeight] = await outbox.read.getRootData([txReceipt.blockNumber]); - expect(l1Root).toEqual(`0x${block?.header.contentCommitment.outHash.toString('hex')}`); - // The path for the message should have the shortest possible height, since we only have 2 msgs - expect(l1MinHeight).toEqual(BigInt(siblingPath.pathSize)); - - // Consume msg 2 - // Taken from l2_to_l1.test - const msg2 = { - sender: { actor: contract.address.toString() as `0x${string}`, version: BigInt(version) }, - recipient: { - actor: recipient2.toString() as `0x${string}`, - chainId: BigInt(deployL1ContractsValues.l1Client.chain.id), - }, - content: content2.toString() as `0x${string}`, - }; - - const txHash = await outbox.write.consume( - [ - msg2, - BigInt(txReceipt.blockNumber!), - BigInt(index2), - siblingPath2.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], - ], - {} as any, - ); - const l1Receipt = await deployL1ContractsValues.l1Client.waitForTransactionReceipt({ - hash: txHash, - }); - // Consume call goes through - expect(l1Receipt.status).toEqual('success'); - - const txLog = l1Receipt.logs[0]; - const topics = decodeEventLog({ - abi: OutboxAbi, - data: txLog.data, - topics: txLog.topics, - }) as { - eventName: 'MessageConsumed'; - args: { - l2BlockNumber: bigint; - root: `0x${string}`; - messageHash: `0x${string}`; - leafIndex: bigint; - }; - }; - // Consumed the expected message - expect(topics.args.messageHash).toStrictEqual(l2ToL1Messages?.[1].toString()); - expect(topics.args.leafIndex).toStrictEqual(BigInt(index2)); - - const consumeAgain = outbox.write.consume( - [ - msg2, - BigInt(txReceipt.blockNumber!), - BigInt(index2), - siblingPath2.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], - ], - {} as any, - ); - // Ensuring we cannot consume the same message again - await expect(consumeAgain).rejects.toThrow(); - }); - - it('Inserts two transactions with total four out messages, and verifies sibling paths of two new messages', async () => { - // Force txs to be in the same block - await aztecNodeAdmin!.setConfig({ minTxsPerBlock: 2 }); - const [[recipient1, content1], [recipient2, content2], [recipient3, content3], [recipient4, content4]] = [ - [EthAddress.random(), Fr.random()], - [EthAddress.fromString(deployL1ContractsValues.l1Client.account.address), Fr.random()], - [EthAddress.random(), Fr.random()], - [EthAddress.random(), Fr.random()], - ]; - - const call0 = new BatchCall(wallets[0], [ - contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content1, recipient1), - contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content2, recipient2), - contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content3, recipient3), - ]); - - const call1 = contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content4, recipient4); - - const [l2TxReceipt0, l2TxReceipt1] = await Promise.all([call0.send().wait(), call1.send().wait()]); - expect(l2TxReceipt0.blockNumber).toEqual(l2TxReceipt1.blockNumber); - - const block = await aztecNode.getBlock(l2TxReceipt0.blockNumber!); - - const l2ToL1Messages = block?.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs); - // Not checking strict equality as ordering is not guaranteed - this should be covered in that we can recalculate the out hash below - expect(l2ToL1Messages?.length).toEqual(4); - - // For each individual message, we are using our node API to grab the index and sibling path. We expect - // the index to match the order of the block we obtained earlier. We also then use this sibling path to hash up to the root, - // verifying that the expected root obtained through the message and the sibling path match the actual root - // that was returned by the circuits in the header as out_hash. - const singleMessage = makeL2ToL1Message(recipient4, content4); - const [index, siblingPath] = await aztecNode.getL2ToL1MessageMembershipWitness( - l2TxReceipt0.blockNumber!, - singleMessage, - ); - // The solo message is the only one in the tx, so it only requires a subtree of height 1 - // +1 for being rolled up - expect(siblingPath.pathSize).toBe(2); - const expectedRoot = calculateExpectedRoot(singleMessage, siblingPath as SiblingPath<2>, index); - expect(expectedRoot.toString('hex')).toEqual(block?.header.contentCommitment.outHash.toString('hex')); - - const messageToConsume = makeL2ToL1Message(recipient2, content2); - const [index2, siblingPath2] = await aztecNode.getL2ToL1MessageMembershipWitness( - l2TxReceipt0.blockNumber!, - messageToConsume, - ); - // This message is in a group of 3, => it needs a subtree of height 2 - // +1 for being rolled up - expect(siblingPath2.pathSize).toBe(3); - - // Outbox L1 tests - // Since the outbox is only consumable when the block is proven, we need to set the block to be proven - await cheatCodes.rollup.markAsProven(l2TxReceipt0.blockNumber ?? 0); - - // Check L1 has expected message tree - const [l1Root, l1MinHeight] = await outbox.read.getRootData([l2TxReceipt0.blockNumber]); - expect(l1Root).toEqual(`0x${block?.header.contentCommitment.outHash.toString('hex')}`); - - // The path for the single message should have the shortest possible height - expect(l1MinHeight).toEqual(BigInt(siblingPath.pathSize)); - - // Consume msg 2 - // Taken from l2_to_l1.test - const msg2 = { - sender: { actor: contract.address.toString() as `0x${string}`, version: BigInt(version) }, - recipient: { - actor: recipient2.toString() as `0x${string}`, - chainId: BigInt(deployL1ContractsValues.l1Client.chain.id), - }, - content: content2.toString() as `0x${string}`, - }; - - const txHash = await outbox.write.consume( - [ - msg2, - BigInt(l2TxReceipt0.blockNumber!), - BigInt(index2), - siblingPath2.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], - ], - {} as any, - ); - const l1Receipt = await deployL1ContractsValues.l1Client.waitForTransactionReceipt({ - hash: txHash, - }); - // Consume call goes through - expect(l1Receipt.status).toEqual('success'); - - const txLog = l1Receipt.logs[0]; - const topics = decodeEventLog({ - abi: OutboxAbi, - data: txLog.data, - topics: txLog.topics, - }) as { - eventName: 'MessageConsumed'; - args: { - l2BlockNumber: bigint; - root: `0x${string}`; - messageHash: `0x${string}`; - leafIndex: bigint; - }; - }; - // Consumed the expected message - expect(topics.args.messageHash).toStrictEqual(messageToConsume.toString()); - expect(topics.args.leafIndex).toStrictEqual(BigInt(index2)); - - const consumeAgain = outbox.write.consume( - [ - msg2, - BigInt(l2TxReceipt0.blockNumber!), - BigInt(index2), - siblingPath2.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], - ], - {} as any, - ); - // Ensuring we cannot consume the same message again - await expect(consumeAgain).rejects.toThrow(); - }); - - it('Inserts two out messages in two transactions and verifies sibling paths of both the new messages', async () => { - // Force txs to be in the same block - await aztecNodeAdmin!.setConfig({ minTxsPerBlock: 2 }); - // recipient2 = msg.sender, so we can consume it later - const [[recipient1, content1], [recipient2, content2]] = [ - [EthAddress.random(), Fr.random()], - [EthAddress.fromString(deployL1ContractsValues.l1Client.account.address), Fr.random()], - ]; - - const call0 = contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content1, recipient1); - const call1 = contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content2, recipient2); - - // resolve together to force the txs to be in the same block - const [l2TxReceipt0, l2TxReceipt1] = await Promise.all([call0.send().wait(), call1.send().wait()]); - expect(l2TxReceipt0.blockNumber).toEqual(l2TxReceipt1.blockNumber); - - const block = await aztecNode.getBlock(l2TxReceipt0.blockNumber!); - - const l2ToL1Messages = block?.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs); - const messageToConsume = makeL2ToL1Message(recipient2, content2); - - // We cannot guarantee the order of txs in blocks - expect( - l2ToL1Messages?.map(l2ToL1Message => - l2ToL1Message.toString().includes(makeL2ToL1Message(recipient1, content1).toString()), - ), - ); - expect(l2ToL1Messages?.map(l2ToL1Message => l2ToL1Message.toString().includes(messageToConsume.toString()))); - - // For each individual message, we are using our node API to grab the index and sibling path. We expect - // the index to match the order of the block we obtained earlier. We also then use this sibling path to hash up to the root, - // verifying that the expected root obtained through the message and the sibling path match the actual root - // that was returned by the circuits in the header as out_hash. - const [index, siblingPath] = await aztecNode.getL2ToL1MessageMembershipWitness( - l2TxReceipt0.blockNumber!, - l2ToL1Messages![0], - ); - expect(siblingPath.pathSize).toBe(2); - // We can only confirm the below index because we have taken the msg hash as the first of the block.body - // It is not necesssarily the msg constructed from [recipient1, content1] above - expect(index).toBe(0n); - - const [index2, siblingPath2] = await aztecNode.getL2ToL1MessageMembershipWitness( - l2TxReceipt0.blockNumber!, - l2ToL1Messages![1], - ); - expect(siblingPath2.pathSize).toBe(2); - // See above comment for confirming index - expect(index2).toBe(2n); - - // Outbox L1 tests - // Since the outbox is only consumable when the block is proven, we need to set the block to be proven - await cheatCodes.rollup.markAsProven(l2TxReceipt0.blockNumber ?? 0); - - // Check L1 has expected message tree - const [l1Root, l1MinHeight] = await outbox.read.getRootData([l2TxReceipt0.blockNumber]); - expect(l1Root).toEqual(`0x${block?.header.contentCommitment.outHash.toString('hex')}`); - // The path for the message should have the shortest possible height, since we only have one msg per tx - expect(l1MinHeight).toEqual(BigInt(siblingPath.pathSize)); - - // Consume msg 2 - // Taken from l2_to_l1.test - const msg2 = { - sender: { actor: contract.address.toString() as `0x${string}`, version: BigInt(version) }, - recipient: { - actor: recipient2.toString() as `0x${string}`, - chainId: BigInt(deployL1ContractsValues.l1Client.chain.id), - }, - content: content2.toString() as `0x${string}`, - }; - const [inputIndex, inputPath] = messageToConsume.equals(l2ToL1Messages![0]) - ? [index, siblingPath] - : [index2, siblingPath2]; - const txHash = await outbox.write.consume( - [ - msg2, - BigInt(l2TxReceipt0.blockNumber!), - BigInt(inputIndex), - inputPath.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], - ], - {} as any, - ); - const l1Receipt = await deployL1ContractsValues.l1Client.waitForTransactionReceipt({ - hash: txHash, - }); - // Consume call goes through - expect(l1Receipt.status).toEqual('success'); - - const txLog = l1Receipt.logs[0]; - const topics = decodeEventLog({ - abi: OutboxAbi, - data: txLog.data, - topics: txLog.topics, - }) as { - eventName: 'MessageConsumed'; - args: { - l2BlockNumber: bigint; - root: `0x${string}`; - messageHash: `0x${string}`; - leafIndex: bigint; - }; - }; - // Consumed the expected message - expect(topics.args.messageHash).toStrictEqual(messageToConsume.toString()); - expect(topics.args.leafIndex).toStrictEqual(BigInt(inputIndex)); - - const consumeAgain = outbox.write.consume( - [ - msg2, - BigInt(l2TxReceipt0.blockNumber!), - BigInt(index2), - siblingPath2.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], - ], - {} as any, - ); - // Ensuring we cannot consume the same message again - await expect(consumeAgain).rejects.toThrow(); - }); - - function calculateExpectedRoot( - l2ToL1Message: Fr, - siblingPath: SiblingPath, - index: bigint, - ): Buffer { - const firstLayerInput: [Buffer, Buffer] = - index & 0x1n - ? [siblingPath.toBufferArray()[0], l2ToL1Message.toBuffer()] - : [l2ToL1Message.toBuffer(), siblingPath.toBufferArray()[0]]; - const firstLayer = merkleSha256.hash(...firstLayerInput); - if (siblingPath.pathSize === 1) { - return truncateAndPad(firstLayer); - } - index /= 2n; - // In the circuit, the 'firstLayer' is the kernel out hash, which is truncated to 31 bytes - // To match the result, the below preimages and the output are truncated to 31 then padded - const secondLayerInput: [Buffer, Buffer] = - index & 0x1n - ? [siblingPath.toBufferArray()[1], truncateAndPad(firstLayer)] - : [truncateAndPad(firstLayer), siblingPath.toBufferArray()[1]]; - return truncateAndPad(merkleSha256.hash(...secondLayerInput)); - } - - function makeL2ToL1Message(recipient: EthAddress, content: Fr = Fr.ZERO): Fr { - const leaf = sha256ToField([ - contract.address, - new Fr(version), - recipient.toBuffer32(), - new Fr(deployL1ContractsValues.l1Client.chain.id), // chain id - content, - ]); - - return leaf; - } -}); From 42c7e571e701d5850129eff3e5667312f1183068 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 13 May 2025 13:35:39 +0000 Subject: [PATCH 4/4] linter fix --- .../end-to-end/src/e2e_cross_chain_messaging/l2_to_l1.test.ts | 2 -- 1 file changed, 2 deletions(-) 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 c131a8955389..fb71db26e70d 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 @@ -256,8 +256,6 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { }); it('Inserts two transactions with total four out messages, and verifies sibling paths of two new messages', async () => { - console.log('Admin', aztecNodeAdmin); - // Force txs to be in the same block await aztecNodeAdmin!.setConfig({ minTxsPerBlock: 2 }); const [[recipient1, content1], [recipient2, content2], [recipient3, content3], [recipient4, content4]] = [