diff --git a/docs/docs/developers/tutorials/token_portal/typescript_glue_code.md b/docs/docs/developers/tutorials/token_portal/typescript_glue_code.md index a81c58e1a59..592ca263f2f 100644 --- a/docs/docs/developers/tutorials/token_portal/typescript_glue_code.md +++ b/docs/docs/developers/tutorials/token_portal/typescript_glue_code.md @@ -109,7 +109,7 @@ This fetches the wallets from the sandbox and deploys our cross chain harness on ## Public flow test -#include_code e2e_public_cross_chain /yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts typescript +#include_code e2e_public_cross_chain /yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts typescript ## Running the test diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts index c0872969c69..afbb66309f8 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts @@ -89,10 +89,8 @@ export class BlacklistTokenContractTest { this.admin = this.wallets[0]; this.other = this.wallets[1]; this.blacklisted = this.wallets[2]; - // this.accounts = this.wallets.map(a => a.getCompleteAddress()); this.accounts = await pxe.getRegisteredAccounts(); this.wallets.forEach((w, i) => this.logger.verbose(`Wallet ${i} address: ${w.getAddress()}`)); - this.accounts.forEach((w, i) => this.logger.verbose(`Account ${i} address: ${w.address}`)); }); await this.snapshotManager.snapshot( diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts deleted file mode 100644 index 3edfd40cc7b..00000000000 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { - type AccountWallet, - type AztecAddress, - type AztecNode, - type DebugLogger, - type DeployL1Contracts, - EthAddress, - type EthAddressLike, - type FieldLike, - Fr, - L1Actor, - L1ToL2Message, - L2Actor, - type PXE, - computeAuthWitMessageHash, - computeSecretHash, -} from '@aztec/aztec.js'; -import { sha256ToField } from '@aztec/foundation/crypto'; -import { InboxAbi, OutboxAbi } from '@aztec/l1-artifacts'; -import { TestContract } from '@aztec/noir-contracts.js'; -import { type TokenContract } from '@aztec/noir-contracts.js/Token'; -import { type TokenBridgeContract } from '@aztec/noir-contracts.js/TokenBridge'; - -import { type Chain, type GetContractReturnType, type Hex, type HttpTransport, type PublicClient } from 'viem'; -import { decodeEventLog, toFunctionSelector } from 'viem/utils'; - -import { publicDeployAccounts, setup } from './fixtures/utils.js'; -import { CrossChainTestHarness } from './shared/cross_chain_test_harness.js'; - -describe('e2e_public_cross_chain_messaging', () => { - let aztecNode: AztecNode; - let pxe: PXE; - let deployL1ContractsValues: DeployL1Contracts; - let logger: DebugLogger; - let teardown: () => Promise; - let wallets: AccountWallet[]; - - let user1Wallet: AccountWallet; - let user2Wallet: AccountWallet; - let ethAccount: EthAddress; - let ownerAddress: AztecAddress; - - let crossChainTestHarness: CrossChainTestHarness; - let l2Token: TokenContract; - let l2Bridge: TokenBridgeContract; - let inbox: GetContractReturnType>; - let outbox: GetContractReturnType>; - - beforeAll(async () => { - ({ aztecNode, pxe, deployL1ContractsValues, wallets, logger, teardown } = await setup(2)); - user1Wallet = wallets[0]; - user2Wallet = wallets[1]; - await publicDeployAccounts(wallets[0], wallets.slice(0, 2)); - }, 45_000); - - beforeEach(async () => { - crossChainTestHarness = await CrossChainTestHarness.new( - aztecNode, - pxe, - deployL1ContractsValues.publicClient, - deployL1ContractsValues.walletClient, - wallets[0], - logger, - ); - l2Token = crossChainTestHarness.l2Token; - l2Bridge = crossChainTestHarness.l2Bridge; - ethAccount = crossChainTestHarness.ethAccount; - ownerAddress = crossChainTestHarness.ownerAddress; - inbox = crossChainTestHarness.inbox; - outbox = crossChainTestHarness.outbox; - - logger.info('Successfully deployed contracts and initialized portal'); - }, 100_000); - - afterAll(async () => { - await teardown(); - }); - - // docs:start:e2e_public_cross_chain - it('Publicly deposit funds from L1 -> L2 and withdraw back to L1', async () => { - // Generate a claim secret using pedersen - const l1TokenBalance = 1000000n; - const bridgeAmount = 100n; - - const [secret, secretHash] = crossChainTestHarness.generateClaimSecret(); - - // 1. Mint tokens on L1 - await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); - - // 2. Deposit tokens to the TokenPortal - const msgHash = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); - expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); - - // Wait for the message to be available for consumption - await crossChainTestHarness.makeMessageConsumable(msgHash); - - // Get message leaf index, needed for claiming in public - const maybeIndexAndPath = await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash, 0n); - expect(maybeIndexAndPath).toBeDefined(); - const messageLeafIndex = maybeIndexAndPath![0]; - - // 3. Consume L1 -> L2 message and mint public tokens on L2 - await crossChainTestHarness.consumeMessageOnAztecAndMintPublicly(bridgeAmount, secret, messageLeafIndex); - await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount); - const afterBalance = bridgeAmount; - - // time to withdraw the funds again! - logger.info('Withdrawing funds from L2'); - - // 4. Give approval to bridge to burn owner's funds: - const withdrawAmount = 9n; - const nonce = Fr.random(); - const burnMessageHash = computeAuthWitMessageHash( - l2Bridge.address, - wallets[0].getChainId(), - wallets[0].getVersion(), - l2Token.methods.burn_public(ownerAddress, withdrawAmount, nonce).request(), - ); - await user1Wallet.setPublicAuthWit(burnMessageHash, true).send().wait(); - - // 5. Withdraw owner's funds from L2 to L1 - const l2ToL1Message = crossChainTestHarness.getL2ToL1MessageLeaf(withdrawAmount); - const l2TxReceipt = await crossChainTestHarness.withdrawPublicFromAztecToL1(withdrawAmount, nonce); - await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, afterBalance - withdrawAmount); - - // Check balance before and after exit. - expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); - - const [l2ToL1MessageIndex, siblingPath] = await aztecNode.getL2ToL1MessageMembershipWitness( - l2TxReceipt.blockNumber!, - l2ToL1Message, - ); - - await crossChainTestHarness.withdrawFundsFromBridgeOnL1( - withdrawAmount, - l2TxReceipt.blockNumber!, - l2ToL1MessageIndex, - siblingPath, - ); - expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); - }, 120_000); - // docs:end:e2e_public_cross_chain - - // Unit tests for TokenBridge's public methods. - - it('Someone else can mint funds to me on my behalf (publicly)', async () => { - // Generate a claim secret using pedersen - const l1TokenBalance = 1000000n; - const bridgeAmount = 100n; - - const [secret, secretHash] = crossChainTestHarness.generateClaimSecret(); - - await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); - const msgHash = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); - expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); - - await crossChainTestHarness.makeMessageConsumable(msgHash); - - const content = sha256ToField([ - Buffer.from(toFunctionSelector('mint_public(bytes32,uint256)').substring(2), 'hex'), - user2Wallet.getAddress(), - new Fr(bridgeAmount), - ]); - const wrongMessage = new L1ToL2Message( - new L1Actor(crossChainTestHarness.tokenPortalAddress, crossChainTestHarness.publicClient.chain.id), - new L2Actor(l2Bridge.address, 1), - content, - secretHash, - ); - - // get message leaf index, needed for claiming in public - const maybeIndexAndPath = await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash, 0n); - expect(maybeIndexAndPath).toBeDefined(); - const messageLeafIndex = maybeIndexAndPath![0]; - - // user2 tries to consume this message and minting to itself -> should fail since the message is intended to be consumed only by owner. - await expect( - l2Bridge - .withWallet(user2Wallet) - .methods.claim_public(user2Wallet.getAddress(), bridgeAmount, secret, messageLeafIndex) - .prove(), - ).rejects.toThrow(`No non-nullified L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); - - // user2 consumes owner's L1-> L2 message on bridge contract and mints public tokens on L2 - logger.info("user2 consumes owner's message on L2 Publicly"); - await l2Bridge - .withWallet(user2Wallet) - .methods.claim_public(ownerAddress, bridgeAmount, secret, messageLeafIndex) - .send() - .wait(); - // ensure funds are gone to owner and not user2. - await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount); - await crossChainTestHarness.expectPublicBalanceOnL2(user2Wallet.getAddress(), 0n); - }, 90_000); - - it("Bridge can't withdraw my funds if I don't give approval", async () => { - const mintAmountToOwner = 100n; - await crossChainTestHarness.mintTokensPublicOnL2(mintAmountToOwner); - - const withdrawAmount = 9n; - const nonce = Fr.random(); - // Should fail as owner has not given approval to bridge burn their funds. - await expect( - l2Bridge - .withWallet(user1Wallet) - .methods.exit_to_l1_public(ethAccount, withdrawAmount, EthAddress.ZERO, nonce) - .prove(), - ).rejects.toThrow('Assertion failed: Message not authorized by account'); - }, 60_000); - - it("can't claim funds privately which were intended for public deposit from the token portal", async () => { - const bridgeAmount = 100n; - const [secret, secretHash] = crossChainTestHarness.generateClaimSecret(); - - await crossChainTestHarness.mintTokensOnL1(bridgeAmount); - const msgHash = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); - expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(0n); - - await crossChainTestHarness.makeMessageConsumable(msgHash); - - // Wrong message hash - const content = sha256ToField([ - Buffer.from(toFunctionSelector('mint_private(bytes32,uint256)').substring(2), 'hex'), - secretHash, - new Fr(bridgeAmount), - ]); - const wrongMessage = new L1ToL2Message( - new L1Actor(crossChainTestHarness.tokenPortalAddress, crossChainTestHarness.publicClient.chain.id), - new L2Actor(l2Bridge.address, 1), - content, - secretHash, - ); - - await expect( - l2Bridge.withWallet(user2Wallet).methods.claim_private(secretHash, bridgeAmount, secret).prove(), - ).rejects.toThrow(`No non-nullified L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); - }, 60_000); - - // Note: We register one portal address when deploying contract but that address is no-longer the only address - // allowed to receive messages from the given contract. In the following test we'll test that it's really the case. - it.each([true, false])( - 'can send an L2 -> L1 message to a non-registered portal address from private or public', - async (isPrivate: boolean) => { - const testContract = await TestContract.deploy(user1Wallet).send().deployed(); - - const content = Fr.random(); - const recipient = crossChainTestHarness.ethAccount; - - let l2TxReceipt; - - // We create the L2 -> L1 message using the test contract - if (isPrivate) { - l2TxReceipt = await testContract.methods - .create_l2_to_l1_message_arbitrary_recipient_private(content, recipient) - .send() - .wait(); - } else { - l2TxReceipt = await testContract.methods - .create_l2_to_l1_message_arbitrary_recipient_public(content, recipient) - .send() - .wait(); - } - - const l2ToL1Message = { - sender: { actor: testContract.address.toString() as Hex, version: 1n }, - recipient: { - actor: recipient.toString() as Hex, - chainId: BigInt(crossChainTestHarness.publicClient.chain.id), - }, - content: content.toString() as Hex, - }; - - const leaf = sha256ToField([ - testContract.address, - new Fr(1), // aztec version - recipient.toBuffer32(), - new Fr(crossChainTestHarness.publicClient.chain.id), // chain id - content, - ]); - - const [l2MessageIndex, siblingPath] = await aztecNode.getL2ToL1MessageMembershipWitness( - l2TxReceipt.blockNumber!, - leaf, - ); - - const txHash = await outbox.write.consume( - [ - l2ToL1Message, - BigInt(l2TxReceipt.blockNumber!), - BigInt(l2MessageIndex), - siblingPath.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], - ], - {} as any, - ); - - const txReceipt = await crossChainTestHarness.publicClient.waitForTransactionReceipt({ - hash: txHash, - }); - - // Exactly 1 event should be emitted in the transaction - expect(txReceipt.logs.length).toBe(1); - - // We decode the event log before checking it - const txLog = txReceipt.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; - }; - }; - - // We check that MessageConsumed event was emitted with the expected message hash and leaf index - expect(topics.args.messageHash).toStrictEqual(leaf.toString()); - expect(topics.args.leafIndex).toStrictEqual(BigInt(0)); - }, - 60_000, - ); - - // Note: We register one portal address when deploying contract but that address is no-longer the only address - // allowed to send messages to the given contract. In the following test we'll test that it's really the case. - it.each([true, false])( - 'can send an L1 -> L2 message from a non-registered portal address consumed from private or public and then sends and claims exactly the same message again', - async (isPrivate: boolean) => { - const testContract = await TestContract.deploy(user1Wallet).send().deployed(); - - const consumeMethod = isPrivate - ? (content: FieldLike, secret: FieldLike, sender: EthAddressLike, _leafIndex: FieldLike) => - testContract.methods.consume_message_from_arbitrary_sender_private(content, secret, sender) - : testContract.methods.consume_message_from_arbitrary_sender_public; - - const secret = Fr.random(); - - const message = new L1ToL2Message( - new L1Actor(crossChainTestHarness.ethAccount, crossChainTestHarness.publicClient.chain.id), - new L2Actor(testContract.address, 1), - Fr.random(), // content - computeSecretHash(secret), // secretHash - ); - - await sendL2Message(message); - - const [message1Index, _1] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message.hash(), 0n))!; - - // Finally, we consume the L1 -> L2 message using the test contract either from private or public - await consumeMethod(message.content, secret, message.sender.sender, message1Index).send().wait(); - - // We send and consume the exact same message the second time to test that oracles correctly return the new - // non-nullified message - await sendL2Message(message); - - // We check that the duplicate message was correctly inserted by checking that its message index is defined and - // larger than the previous message index - const [message2Index, _2] = (await aztecNode.getL1ToL2MessageMembershipWitness( - 'latest', - message.hash(), - message1Index + 1n, - ))!; - - expect(message2Index).toBeDefined(); - expect(message2Index).toBeGreaterThan(message1Index); - - // Now we consume the message again. Everything should pass because oracle should return the duplicate message - // which is not nullified - await consumeMethod(message.content, secret, message.sender.sender, message2Index).send().wait(); - }, - 120_000, - ); - - const sendL2Message = async (message: L1ToL2Message) => { - // We inject the message to Inbox - const txHash = await inbox.write.sendL2Message( - [ - { actor: message.recipient.recipient.toString() as Hex, version: 1n }, - message.content.toString() as Hex, - message.secretHash.toString() as Hex, - ] as const, - {} as any, - ); - - // We check that the message was correctly injected by checking the emitted event - const msgHash = message.hash(); - { - const txReceipt = await crossChainTestHarness.publicClient.waitForTransactionReceipt({ - hash: txHash, - }); - - // Exactly 1 event should be emitted in the transaction - expect(txReceipt.logs.length).toBe(1); - - // We decode the event and get leaf out of it - const txLog = txReceipt.logs[0]; - const topics = decodeEventLog({ - abi: InboxAbi, - data: txLog.data, - topics: txLog.topics, - }); - const receivedMsgHash = topics.args.hash; - - // We check that the leaf inserted into the subtree matches the expected message hash - expect(receivedMsgHash).toBe(msgHash.toString()); - } - - await crossChainTestHarness.makeMessageConsumable(msgHash); - }; -}); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts new file mode 100644 index 00000000000..566118a99cc --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts @@ -0,0 +1,161 @@ +import { Fr, L1Actor, L1ToL2Message, L2Actor, computeAuthWitMessageHash } from '@aztec/aztec.js'; +import { sha256ToField } from '@aztec/foundation/crypto'; + +import { toFunctionSelector } from 'viem'; + +import { PublicCrossChainMessagingContractTest } from './public_cross_chain_messaging_contract_test.js'; + +describe('e2e_public_cross_chain_messaging deposits', () => { + const t = new PublicCrossChainMessagingContractTest('deposits'); + + let { + wallets, + crossChainTestHarness, + ethAccount, + aztecNode, + logger, + ownerAddress, + l2Bridge, + l2Token, + user1Wallet, + user2Wallet, + } = t; + + beforeEach(async () => { + await t.applyBaseSnapshots(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ wallets, crossChainTestHarness, user1Wallet, user2Wallet } = t); + + ethAccount = crossChainTestHarness.ethAccount; + aztecNode = crossChainTestHarness.aztecNode; + logger = crossChainTestHarness.logger; + ownerAddress = crossChainTestHarness.ownerAddress; + l2Bridge = crossChainTestHarness.l2Bridge; + l2Token = crossChainTestHarness.l2Token; + }, 200_000); + + afterEach(async () => { + await t.teardown(); + }); + + // docs:start:e2e_public_cross_chain + it('Publicly deposit funds from L1 -> L2 and withdraw back to L1', async () => { + // Generate a claim secret using pedersen + const l1TokenBalance = 1000000n; + const bridgeAmount = 100n; + + const [secret, secretHash] = crossChainTestHarness.generateClaimSecret(); + + // 1. Mint tokens on L1 + logger.verbose(`1. Mint tokens on L1`); + await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); + + // 2. Deposit tokens to the TokenPortal + logger.verbose(`2. Deposit tokens to the TokenPortal`); + const msgHash = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); + + // Wait for the message to be available for consumption + logger.verbose(`Wait for the message to be available for consumption`); + await crossChainTestHarness.makeMessageConsumable(msgHash); + + // Get message leaf index, needed for claiming in public + const maybeIndexAndPath = await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash, 0n); + expect(maybeIndexAndPath).toBeDefined(); + const messageLeafIndex = maybeIndexAndPath![0]; + + // 3. Consume L1 -> L2 message and mint public tokens on L2 + logger.verbose('3. Consume L1 -> L2 message and mint public tokens on L2'); + await crossChainTestHarness.consumeMessageOnAztecAndMintPublicly(bridgeAmount, secret, messageLeafIndex); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount); + const afterBalance = bridgeAmount; + + // time to withdraw the funds again! + logger.info('Withdrawing funds from L2'); + + // 4. Give approval to bridge to burn owner's funds: + const withdrawAmount = 9n; + const nonce = Fr.random(); + const burnMessageHash = computeAuthWitMessageHash( + l2Bridge.address, + wallets[0].getChainId(), + wallets[0].getVersion(), + l2Token.methods.burn_public(ownerAddress, withdrawAmount, nonce).request(), + ); + await user1Wallet.setPublicAuthWit(burnMessageHash, true).send().wait(); + + // 5. Withdraw owner's funds from L2 to L1 + logger.verbose('5. Withdraw owner funds from L2 to L1'); + const l2ToL1Message = crossChainTestHarness.getL2ToL1MessageLeaf(withdrawAmount); + const l2TxReceipt = await crossChainTestHarness.withdrawPublicFromAztecToL1(withdrawAmount, nonce); + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, afterBalance - withdrawAmount); + + // Check balance before and after exit. + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); + + const [l2ToL1MessageIndex, siblingPath] = await aztecNode.getL2ToL1MessageMembershipWitness( + l2TxReceipt.blockNumber!, + l2ToL1Message, + ); + + await crossChainTestHarness.withdrawFundsFromBridgeOnL1( + withdrawAmount, + l2TxReceipt.blockNumber!, + l2ToL1MessageIndex, + siblingPath, + ); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); + }, 120_000); + // docs:end:e2e_public_cross_chain + + it('Someone else can mint funds to me on my behalf (publicly)', async () => { + // Generate a claim secret using pedersen + const l1TokenBalance = 1000000n; + const bridgeAmount = 100n; + + const [secret, secretHash] = crossChainTestHarness.generateClaimSecret(); + + await crossChainTestHarness.mintTokensOnL1(l1TokenBalance); + const msgHash = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); + + await crossChainTestHarness.makeMessageConsumable(msgHash); + + const content = sha256ToField([ + Buffer.from(toFunctionSelector('mint_public(bytes32,uint256)').substring(2), 'hex'), + user2Wallet.getAddress(), + new Fr(bridgeAmount), + ]); + const wrongMessage = new L1ToL2Message( + new L1Actor(crossChainTestHarness.tokenPortalAddress, crossChainTestHarness.publicClient.chain.id), + new L2Actor(l2Bridge.address, 1), + content, + secretHash, + ); + + // get message leaf index, needed for claiming in public + const maybeIndexAndPath = await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash, 0n); + expect(maybeIndexAndPath).toBeDefined(); + const messageLeafIndex = maybeIndexAndPath![0]; + + // user2 tries to consume this message and minting to itself -> should fail since the message is intended to be consumed only by owner. + await expect( + l2Bridge + .withWallet(user2Wallet) + .methods.claim_public(user2Wallet.getAddress(), bridgeAmount, secret, messageLeafIndex) + .prove(), + ).rejects.toThrow(`No non-nullified L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); + + // user2 consumes owner's L1-> L2 message on bridge contract and mints public tokens on L2 + logger.info("user2 consumes owner's message on L2 Publicly"); + await l2Bridge + .withWallet(user2Wallet) + .methods.claim_public(ownerAddress, bridgeAmount, secret, messageLeafIndex) + .send() + .wait(); + // ensure funds are gone to owner and not user2. + await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, bridgeAmount); + await crossChainTestHarness.expectPublicBalanceOnL2(user2Wallet.getAddress(), 0n); + }, 90_000); +}); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/failure_cases.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/failure_cases.test.ts new file mode 100644 index 00000000000..8e8bb4f1bb9 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/failure_cases.test.ts @@ -0,0 +1,68 @@ +import { EthAddress, Fr, L1Actor, L1ToL2Message, L2Actor } from '@aztec/aztec.js'; +import { sha256ToField } from '@aztec/foundation/crypto'; + +import { toFunctionSelector } from 'viem'; + +import { PublicCrossChainMessagingContractTest } from './public_cross_chain_messaging_contract_test.js'; + +describe('e2e_public_cross_chain_messaging failures', () => { + const t = new PublicCrossChainMessagingContractTest('failures'); + + let { crossChainTestHarness, ethAccount, l2Bridge, user1Wallet, user2Wallet } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ crossChainTestHarness, user1Wallet, user2Wallet } = t); + ethAccount = crossChainTestHarness.ethAccount; + l2Bridge = crossChainTestHarness.l2Bridge; + }, 200_000); + + afterAll(async () => { + await t.teardown(); + }); + + it("Bridge can't withdraw my funds if I don't give approval", async () => { + const mintAmountToOwner = 100n; + await crossChainTestHarness.mintTokensPublicOnL2(mintAmountToOwner); + + const withdrawAmount = 9n; + const nonce = Fr.random(); + // Should fail as owner has not given approval to bridge burn their funds. + await expect( + l2Bridge + .withWallet(user1Wallet) + .methods.exit_to_l1_public(ethAccount, withdrawAmount, EthAddress.ZERO, nonce) + .prove(), + ).rejects.toThrow('Assertion failed: Message not authorized by account'); + }, 60_000); + + it("can't claim funds privately which were intended for public deposit from the token portal", async () => { + const bridgeAmount = 100n; + const [secret, secretHash] = crossChainTestHarness.generateClaimSecret(); + + await crossChainTestHarness.mintTokensOnL1(bridgeAmount); + const msgHash = await crossChainTestHarness.sendTokensToPortalPublic(bridgeAmount, secretHash); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(0n); + + await crossChainTestHarness.makeMessageConsumable(msgHash); + + // Wrong message hash + const content = sha256ToField([ + Buffer.from(toFunctionSelector('mint_private(bytes32,uint256)').substring(2), 'hex'), + secretHash, + new Fr(bridgeAmount), + ]); + const wrongMessage = new L1ToL2Message( + new L1Actor(crossChainTestHarness.tokenPortalAddress, crossChainTestHarness.publicClient.chain.id), + new L2Actor(l2Bridge.address, 1), + content, + secretHash, + ); + + await expect( + l2Bridge.withWallet(user2Wallet).methods.claim_private(secretHash, bridgeAmount, secret).prove(), + ).rejects.toThrow(`No non-nullified L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); + }, 60_000); +}); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l1_to_l2.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l1_to_l2.test.ts new file mode 100644 index 00000000000..9285a56a36d --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l1_to_l2.test.ts @@ -0,0 +1,122 @@ +import { + type EthAddressLike, + type FieldLike, + Fr, + L1Actor, + L1ToL2Message, + L2Actor, + computeSecretHash, +} from '@aztec/aztec.js'; +import { InboxAbi } from '@aztec/l1-artifacts'; +import { TestContract } from '@aztec/noir-contracts.js'; + +import { type Hex, decodeEventLog } from 'viem'; + +import { PublicCrossChainMessagingContractTest } from './public_cross_chain_messaging_contract_test.js'; + +describe('e2e_public_cross_chain_messaging l1_to_l2', () => { + const t = new PublicCrossChainMessagingContractTest('l1_to_l2'); + + let { crossChainTestHarness, aztecNode, user1Wallet, inbox } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ crossChainTestHarness, user1Wallet } = t); + + aztecNode = crossChainTestHarness.aztecNode; + inbox = crossChainTestHarness.inbox; + }, 200_000); + + afterAll(async () => { + await t.teardown(); + }); + + // Note: We register one portal address when deploying contract but that address is no-longer the only address + // allowed to send messages to the given contract. In the following test we'll test that it's really the case. + it.each([true, false])( + 'can send an L1 -> L2 message from a non-registered portal address consumed from private or public and then sends and claims exactly the same message again', + async (isPrivate: boolean) => { + const testContract = await TestContract.deploy(user1Wallet).send().deployed(); + + const consumeMethod = isPrivate + ? (content: FieldLike, secret: FieldLike, sender: EthAddressLike, _leafIndex: FieldLike) => + testContract.methods.consume_message_from_arbitrary_sender_private(content, secret, sender) + : testContract.methods.consume_message_from_arbitrary_sender_public; + + const secret = Fr.random(); + + const message = new L1ToL2Message( + new L1Actor(crossChainTestHarness.ethAccount, crossChainTestHarness.publicClient.chain.id), + new L2Actor(testContract.address, 1), + Fr.random(), // content + computeSecretHash(secret), // secretHash + ); + + await sendL2Message(message); + + const [message1Index, _1] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message.hash(), 0n))!; + + // Finally, we consume the L1 -> L2 message using the test contract either from private or public + await consumeMethod(message.content, secret, message.sender.sender, message1Index).send().wait(); + + // We send and consume the exact same message the second time to test that oracles correctly return the new + // non-nullified message + await sendL2Message(message); + + // We check that the duplicate message was correctly inserted by checking that its message index is defined and + // larger than the previous message index + const [message2Index, _2] = (await aztecNode.getL1ToL2MessageMembershipWitness( + 'latest', + message.hash(), + message1Index + 1n, + ))!; + + expect(message2Index).toBeDefined(); + expect(message2Index).toBeGreaterThan(message1Index); + + // Now we consume the message again. Everything should pass because oracle should return the duplicate message + // which is not nullified + await consumeMethod(message.content, secret, message.sender.sender, message2Index).send().wait(); + }, + 120_000, + ); + + const sendL2Message = async (message: L1ToL2Message) => { + // We inject the message to Inbox + const txHash = await inbox.write.sendL2Message( + [ + { actor: message.recipient.recipient.toString() as Hex, version: 1n }, + message.content.toString() as Hex, + message.secretHash.toString() as Hex, + ] as const, + {} as any, + ); + + // We check that the message was correctly injected by checking the emitted event + const msgHash = message.hash(); + { + const txReceipt = await crossChainTestHarness.publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + + // Exactly 1 event should be emitted in the transaction + expect(txReceipt.logs.length).toBe(1); + + // We decode the event and get leaf out of it + const txLog = txReceipt.logs[0]; + const topics = decodeEventLog({ + abi: InboxAbi, + data: txLog.data, + topics: txLog.topics, + }); + const receivedMsgHash = topics.args.hash; + + // We check that the leaf inserted into the subtree matches the expected message hash + expect(receivedMsgHash).toBe(msgHash.toString()); + } + + await crossChainTestHarness.makeMessageConsumable(msgHash); + }; +}); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts new file mode 100644 index 00000000000..b43333c5edf --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts @@ -0,0 +1,116 @@ +import { Fr } from '@aztec/aztec.js'; +import { sha256ToField } from '@aztec/foundation/crypto'; +import { OutboxAbi } from '@aztec/l1-artifacts'; +import { TestContract } from '@aztec/noir-contracts.js'; + +import { type Hex, decodeEventLog } from 'viem'; + +import { PublicCrossChainMessagingContractTest } from './public_cross_chain_messaging_contract_test.js'; + +describe('e2e_public_cross_chain_messaging l2_to_l1', () => { + const t = new PublicCrossChainMessagingContractTest('l2_to_l1'); + + let { crossChainTestHarness, aztecNode, user1Wallet, outbox } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ crossChainTestHarness, user1Wallet } = t); + + aztecNode = crossChainTestHarness.aztecNode; + + outbox = crossChainTestHarness.outbox; + }, 200_000); + + afterAll(async () => { + await t.teardown(); + }); + + // Note: We register one portal address when deploying contract but that address is no-longer the only address + // allowed to receive messages from the given contract. In the following test we'll test that it's really the case. + 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; + + let l2TxReceipt; + + // We create the L2 -> L1 message using the test contract + if (isPrivate) { + l2TxReceipt = await testContract.methods + .create_l2_to_l1_message_arbitrary_recipient_private(content, recipient) + .send() + .wait(); + } else { + l2TxReceipt = await testContract.methods + .create_l2_to_l1_message_arbitrary_recipient_public(content, recipient) + .send() + .wait(); + } + + const l2ToL1Message = { + sender: { actor: testContract.address.toString() as Hex, version: 1n }, + recipient: { + actor: recipient.toString() as Hex, + chainId: BigInt(crossChainTestHarness.publicClient.chain.id), + }, + content: content.toString() as Hex, + }; + + const leaf = sha256ToField([ + testContract.address, + new Fr(1), // aztec version + recipient.toBuffer32(), + new Fr(crossChainTestHarness.publicClient.chain.id), // chain id + content, + ]); + + const [l2MessageIndex, siblingPath] = await aztecNode.getL2ToL1MessageMembershipWitness( + l2TxReceipt.blockNumber!, + leaf, + ); + + const txHash = await outbox.write.consume( + [ + l2ToL1Message, + BigInt(l2TxReceipt.blockNumber!), + BigInt(l2MessageIndex), + siblingPath.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + ], + {} as any, + ); + + const txReceipt = await crossChainTestHarness.publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + + // Exactly 1 event should be emitted in the transaction + expect(txReceipt.logs.length).toBe(1); + + // We decode the event log before checking it + const txLog = txReceipt.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; + }; + }; + + // We check that MessageConsumed event was emitted with the expected message hash and leaf index + expect(topics.args.messageHash).toStrictEqual(leaf.toString()); + expect(topics.args.leafIndex).toStrictEqual(BigInt(0)); + }, + 60_000, + ); +}); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts new file mode 100644 index 00000000000..38add3ebc8a --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts @@ -0,0 +1,225 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { type AztecNodeConfig } from '@aztec/aztec-node'; +import { + type AccountWallet, + AztecAddress, + type AztecNode, + type CompleteAddress, + type DebugLogger, + EthAddress, + type PXE, + createDebugLogger, +} from '@aztec/aztec.js'; +import { InboxAbi, OutboxAbi, PortalERC20Abi, TokenPortalAbi } from '@aztec/l1-artifacts'; +import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts.js'; + +import { + type Chain, + type HttpTransport, + type PublicClient, + createPublicClient, + createWalletClient, + getContract, + http, +} from 'viem'; +import { mnemonicToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; + +import { MNEMONIC } from '../fixtures/fixtures.js'; +import { + SnapshotManager, + type SubsystemsContext, + addAccounts, + publicDeployAccounts, +} from '../fixtures/snapshot_manager.js'; +import { CrossChainTestHarness } from '../shared/cross_chain_test_harness.js'; + +const { E2E_DATA_PATH: dataPath } = process.env; + +export class PublicCrossChainMessagingContractTest { + private snapshotManager: SnapshotManager; + logger: DebugLogger; + wallets: AccountWallet[] = []; + accounts: CompleteAddress[] = []; + aztecNode!: AztecNode; + pxe!: PXE; + aztecNodeConfig!: AztecNodeConfig; + + publicClient!: PublicClient | undefined; + + user1Wallet!: AccountWallet; + user2Wallet!: AccountWallet; + crossChainTestHarness!: CrossChainTestHarness; + ethAccount!: EthAddress; + ownerAddress!: AztecAddress; + l2Token!: TokenContract; + l2Bridge!: TokenBridgeContract; + + inbox!: any; // GetContractReturnType | undefined; + outbox!: any; // GetContractReturnType | undefined; + + constructor(testName: string) { + this.logger = createDebugLogger(`aztec:e2e_public_cross_chain_messaging:${testName}`); + this.snapshotManager = new SnapshotManager(`e2e_public_cross_chain_messaging/${testName}`, dataPath); + } + + async setup() { + const { aztecNode, pxe, aztecNodeConfig } = await this.snapshotManager.setup(); + this.aztecNode = aztecNode; + this.pxe = pxe; + this.aztecNodeConfig = aztecNodeConfig; + } + + snapshot = ( + name: string, + apply: (context: SubsystemsContext) => Promise, + restore: (snapshotData: T, context: SubsystemsContext) => Promise = () => Promise.resolve(), + ): Promise => this.snapshotManager.snapshot(name, apply, restore); + + async teardown() { + await this.snapshotManager.teardown(); + } + + viemStuff(rpcUrl: string) { + const hdAccount = mnemonicToAccount(MNEMONIC); + + const walletClient = createWalletClient({ + account: hdAccount, + chain: foundry, + transport: http(rpcUrl), + }); + const publicClient = createPublicClient({ + chain: foundry, + transport: http(rpcUrl), + }); + + return { walletClient, publicClient }; + } + + async applyBaseSnapshots() { + // Note that we are using the same `pxe`, `aztecNodeConfig` and `aztecNode` across all snapshots. + // This is to not have issues with different networks. + + await this.snapshotManager.snapshot( + '3_accounts', + addAccounts(3, this.logger), + async ({ accountKeys }, { pxe, aztecNodeConfig, aztecNode }) => { + const accountManagers = accountKeys.map(ak => getSchnorrAccount(pxe, ak[0], ak[1], 1)); + this.wallets = await Promise.all(accountManagers.map(a => a.getWallet())); + this.wallets.forEach((w, i) => this.logger.verbose(`Wallet ${i} address: ${w.getAddress()}`)); + this.accounts = await pxe.getRegisteredAccounts(); + + this.user1Wallet = this.wallets[0]; + this.user2Wallet = this.wallets[1]; + + this.pxe = pxe; + this.aztecNode = aztecNode; + this.aztecNodeConfig = aztecNodeConfig; + }, + ); + + await this.snapshotManager.snapshot( + 'e2e_public_cross_chain_messaging', + async () => { + // Create the token contract state. + // Move this account thing to addAccounts above? + this.logger.verbose(`Public deploy accounts...`); + await publicDeployAccounts(this.wallets[0], this.accounts.slice(0, 3)); + + const { publicClient, walletClient } = this.viemStuff(this.aztecNodeConfig.rpcUrl); + + this.logger.verbose(`Setting up cross chain harness...`); + this.crossChainTestHarness = await CrossChainTestHarness.new( + this.aztecNode, + this.pxe, + publicClient, + walletClient, + this.wallets[0], + this.logger, + ); + + this.logger.verbose(`L2 token deployed to: ${this.crossChainTestHarness.l2Token.address}`); + + return this.toCrossChainContext(); + }, + async crossChainContext => { + this.l2Token = await TokenContract.at(crossChainContext.l2Token, this.user1Wallet); + this.l2Bridge = await TokenBridgeContract.at(crossChainContext.l2Bridge, this.user1Wallet); + + // There is an issue with the reviver so we are getting strings sometimes. Working around it here. + this.ethAccount = EthAddress.fromString(crossChainContext.ethAccount.toString()); + this.ownerAddress = AztecAddress.fromString(crossChainContext.ownerAddress.toString()); + const tokenPortalAddress = EthAddress.fromString(crossChainContext.tokenPortal.toString()); + + const { publicClient, walletClient } = this.viemStuff(this.aztecNodeConfig.rpcUrl); + + const inbox = getContract({ + address: this.aztecNodeConfig.l1Contracts.inboxAddress.toString(), + abi: InboxAbi, + client: walletClient, + }); + const outbox = getContract({ + address: this.aztecNodeConfig.l1Contracts.outboxAddress.toString(), + abi: OutboxAbi, + client: walletClient, + }); + + const tokenPortal = getContract({ + address: tokenPortalAddress.toString(), + abi: TokenPortalAbi, + client: walletClient, + }); + const underlyingERC20 = getContract({ + address: crossChainContext.underlying.toString(), + abi: PortalERC20Abi, + client: walletClient, + }); + + this.crossChainTestHarness = new CrossChainTestHarness( + this.aztecNode, + this.pxe, + this.logger, + this.l2Token, + this.l2Bridge, + this.ethAccount, + tokenPortalAddress, + tokenPortal, + underlyingERC20, + inbox, + outbox, + publicClient, + walletClient, + this.ownerAddress, + ); + + this.publicClient = publicClient; + this.inbox = inbox; + this.outbox = outbox; + }, + ); + } + + toCrossChainContext(): CrossChainContext { + return { + l2Token: this.crossChainTestHarness.l2Token.address, + l2Bridge: this.crossChainTestHarness.l2Bridge.address, + tokenPortal: this.crossChainTestHarness.tokenPortal.address, + underlying: EthAddress.fromString(this.crossChainTestHarness.underlyingERC20.address), + ethAccount: this.crossChainTestHarness.ethAccount, + ownerAddress: this.crossChainTestHarness.ownerAddress, + inbox: EthAddress.fromString(this.crossChainTestHarness.inbox.address), + outbox: EthAddress.fromString(this.crossChainTestHarness.outbox.address), + }; + } +} + +type CrossChainContext = { + l2Token: AztecAddress; + l2Bridge: AztecAddress; + tokenPortal: EthAddress; + underlying: EthAddress; + ethAccount: EthAddress; + ownerAddress: AztecAddress; + inbox: EthAddress; + outbox: EthAddress; +};