|
| 1 | +import { |
| 2 | + AccountWalletWithPrivateKey, |
| 3 | + AztecNode, |
| 4 | + BatchCall, |
| 5 | + DeployL1Contracts, |
| 6 | + EthAddress, |
| 7 | + Fr, |
| 8 | + SiblingPath, |
| 9 | + sha256, |
| 10 | +} from '@aztec/aztec.js'; |
| 11 | +import { SHA256 } from '@aztec/merkle-tree'; |
| 12 | +import { TestContract } from '@aztec/noir-contracts.js'; |
| 13 | + |
| 14 | +import { beforeEach, describe, expect, it } from '@jest/globals'; |
| 15 | + |
| 16 | +import { setup } from './fixtures/utils.js'; |
| 17 | + |
| 18 | +// @remark - This does not test the Outbox Contract yet. All this test does is create L2 to L1 messages in a block, |
| 19 | +// verify their existence, and produce a sibling path that is also checked for validity against the circuit produced |
| 20 | +// out_hash in the header. |
| 21 | +describe('E2E Outbox Tests', () => { |
| 22 | + let teardown: () => void; |
| 23 | + let aztecNode: AztecNode; |
| 24 | + const merkleSha256 = new SHA256(); |
| 25 | + let contract: TestContract; |
| 26 | + let wallets: AccountWalletWithPrivateKey[]; |
| 27 | + let deployL1ContractsValues: DeployL1Contracts; |
| 28 | + |
| 29 | + beforeEach(async () => { |
| 30 | + ({ teardown, aztecNode, wallets, deployL1ContractsValues } = await setup(1)); |
| 31 | + |
| 32 | + const receipt = await TestContract.deploy(wallets[0]).send({ contractAddressSalt: Fr.ZERO }).wait(); |
| 33 | + contract = receipt.contract; |
| 34 | + }, 100_000); |
| 35 | + |
| 36 | + afterAll(() => teardown()); |
| 37 | + |
| 38 | + it('Inserts a new transaction with two out messages, and verifies sibling paths of both the new messages', async () => { |
| 39 | + const [[recipient1, content1], [recipient2, content2]] = [ |
| 40 | + [EthAddress.random(), Fr.random()], |
| 41 | + [EthAddress.random(), Fr.random()], |
| 42 | + ]; |
| 43 | + |
| 44 | + // We can't put any more l2 to L1 messages here There are a max of 2 L2 to L1 messages per transaction |
| 45 | + const call = new BatchCall(wallets[0], [ |
| 46 | + contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content1, recipient1).request(), |
| 47 | + contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content2, recipient2).request(), |
| 48 | + ]); |
| 49 | + |
| 50 | + // TODO (#5104): When able to guarantee multiple txs in a single block, make this populate a full tree. Right now we are |
| 51 | + // unable to do this because in CI, for some reason, the tx's are handled in different blocks, so it is impossible |
| 52 | + // 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) |
| 53 | + const txReceipt = await call.send().wait(); |
| 54 | + |
| 55 | + const block = await aztecNode.getBlock(txReceipt.blockNumber!); |
| 56 | + |
| 57 | + const l2ToL1Messages = block?.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs); |
| 58 | + |
| 59 | + expect(l2ToL1Messages?.map(l2ToL1Message => l2ToL1Message.toString())).toStrictEqual( |
| 60 | + [makeL2ToL1Message(recipient2, content2), makeL2ToL1Message(recipient1, content1), Fr.ZERO, Fr.ZERO].map( |
| 61 | + expectedL2ToL1Message => expectedL2ToL1Message.toString(), |
| 62 | + ), |
| 63 | + ); |
| 64 | + |
| 65 | + // For each individual message, we are using our node API to grab the index and sibling path. We expect |
| 66 | + // 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, |
| 67 | + // verifying that the expected root obtained through the message and the sibling path match the actual root |
| 68 | + // that was returned by the circuits in the header as out_hash. |
| 69 | + const [index, siblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( |
| 70 | + txReceipt.blockNumber!, |
| 71 | + l2ToL1Messages![0], |
| 72 | + ); |
| 73 | + expect(siblingPath.pathSize).toBe(2); |
| 74 | + expect(index).toBe(0); |
| 75 | + const expectedRoot = calculateExpectedRoot(l2ToL1Messages![0], siblingPath as SiblingPath<2>, index); |
| 76 | + expect(expectedRoot.toString('hex')).toEqual(block?.header.contentCommitment.outHash.toString('hex')); |
| 77 | + |
| 78 | + const [index2, siblingPath2] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( |
| 79 | + txReceipt.blockNumber!, |
| 80 | + l2ToL1Messages![1], |
| 81 | + ); |
| 82 | + expect(siblingPath2.pathSize).toBe(2); |
| 83 | + expect(index2).toBe(1); |
| 84 | + const expectedRoot2 = calculateExpectedRoot(l2ToL1Messages![1], siblingPath2 as SiblingPath<2>, index2); |
| 85 | + expect(expectedRoot2.toString('hex')).toEqual(block?.header.contentCommitment.outHash.toString('hex')); |
| 86 | + }, 360_000); |
| 87 | + |
| 88 | + function calculateExpectedRoot(l2ToL1Message: Fr, siblingPath: SiblingPath<2>, index: number): Buffer { |
| 89 | + const firstLayerInput: [Buffer, Buffer] = |
| 90 | + index & 0x1 |
| 91 | + ? [siblingPath.toBufferArray()[0], l2ToL1Message.toBuffer()] |
| 92 | + : [l2ToL1Message.toBuffer(), siblingPath.toBufferArray()[0]]; |
| 93 | + const firstLayer = merkleSha256.hash(...firstLayerInput); |
| 94 | + index /= 2; |
| 95 | + const secondLayerInput: [Buffer, Buffer] = |
| 96 | + index & 0x1 ? [siblingPath.toBufferArray()[1], firstLayer] : [firstLayer, siblingPath.toBufferArray()[1]]; |
| 97 | + return merkleSha256.hash(...secondLayerInput); |
| 98 | + } |
| 99 | + |
| 100 | + function makeL2ToL1Message(recipient: EthAddress, content: Fr = Fr.ZERO): Fr { |
| 101 | + const leaf = Fr.fromBufferReduce( |
| 102 | + sha256( |
| 103 | + Buffer.concat([ |
| 104 | + contract.address.toBuffer(), |
| 105 | + new Fr(1).toBuffer(), // aztec version |
| 106 | + recipient.toBuffer32(), |
| 107 | + new Fr(deployL1ContractsValues.publicClient.chain.id).toBuffer(), // chain id |
| 108 | + content.toBuffer(), |
| 109 | + ]), |
| 110 | + ), |
| 111 | + ); |
| 112 | + |
| 113 | + return leaf; |
| 114 | + } |
| 115 | +}); |
0 commit comments