From d30cb94745e5f04ccf9fd8f9e6ac48cb0d2bd137 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 25 May 2021 00:28:16 -0400 Subject: [PATCH 01/13] feat[message-relayer]: relay tx generator --- packages/message-relayer/hardhat.config.ts | 15 + packages/message-relayer/package.json | 16 +- packages/message-relayer/src/relay-tx.ts | 334 ++++++++++++++++++ packages/message-relayer/test/setup.ts | 12 + .../test/unit-tests/relay-tx.spec.ts | 22 ++ yarn.lock | 58 +++ 6 files changed, 455 insertions(+), 2 deletions(-) create mode 100644 packages/message-relayer/hardhat.config.ts create mode 100644 packages/message-relayer/src/relay-tx.ts create mode 100644 packages/message-relayer/test/setup.ts create mode 100644 packages/message-relayer/test/unit-tests/relay-tx.spec.ts diff --git a/packages/message-relayer/hardhat.config.ts b/packages/message-relayer/hardhat.config.ts new file mode 100644 index 000000000000..fac7c21376f0 --- /dev/null +++ b/packages/message-relayer/hardhat.config.ts @@ -0,0 +1,15 @@ +import { HardhatUserConfig } from 'hardhat/config' + +import '@nomiclabs/hardhat-ethers' +import '@nomiclabs/hardhat-waffle' + +const config: HardhatUserConfig = { + paths: { + sources: './test/test-contracts', + }, + solidity: { + version: '0.7.6', + }, +} + +export default config diff --git a/packages/message-relayer/package.json b/packages/message-relayer/package.json index 1b48abee2dae..e357e670e88d 100644 --- a/packages/message-relayer/package.json +++ b/packages/message-relayer/package.json @@ -14,7 +14,8 @@ "clean": "rimraf dist/ ./tsconfig.build.tsbuildinfo", "lint": "yarn lint:fix && yarn lint:check", "lint:fix": "prettier --config .prettierrc.json --write \"{src,exec,test}/**/*.ts\"", - "lint:check": "tslint --format stylish --project ." + "lint:check": "tslint --format stylish --project .", + "test": "hardhat test --show-stack-traces" }, "keywords": [ "optimism", @@ -30,9 +31,9 @@ }, "dependencies": { "@eth-optimism/common-ts": "^0.1.2", - "bcfg": "^0.1.6", "@eth-optimism/contracts": "^0.3.1", "@eth-optimism/core-utils": "^0.4.3", + "bcfg": "^0.1.6", "dotenv": "^8.2.0", "ethers": "^5.1.0", "google-spreadsheet": "^3.1.15", @@ -40,6 +41,17 @@ "rlp": "^2.2.6" }, "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-waffle": "^2.0.1", + "@types/chai": "^4.2.18", + "@types/chai-as-promised": "^7.1.4", + "@types/mocha": "^8.2.2", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", + "ethereum-waffle": "^3.3.0", + "ethers": "^5.2.0", + "hardhat": "^2.3.0", + "mocha": "^8.4.0", "prettier": "^2.2.1", "tslint": "^6.1.3", "tslint-config-prettier": "^1.18.0", diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts new file mode 100644 index 000000000000..177b5eb2a5b7 --- /dev/null +++ b/packages/message-relayer/src/relay-tx.ts @@ -0,0 +1,334 @@ +/* Imports: External */ +import { ethers } from 'ethers' +import { + fromHexString, + remove0x, + toHexString, + toRpcHexString, +} from '@eth-optimism/core-utils' +import { getContractInterface } from '@eth-optimism/contracts' +import * as rlp from 'rlp' +import { MerkleTree } from 'merkletreejs' + +// Number of blocks added to the L2 chain before the first L2 transaction. Genesis are added to the +// chain to initialize the system. However, they create a discrepancy between the L2 block number +// the index of the transaction that corresponds to that block number. For example, if there's 1 +// genesis block, then the transaction with an index of 0 corresponds to the block with index 1. +const NUM_L2_GENESIS_BLOCKS = 1 + +interface StateRootBatchHeader { + batchIndex: ethers.BigNumber + batchRoot: string + batchSize: ethers.BigNumber + prevTotalElements: ethers.BigNumber + extraData: string +} + +interface StateRootBatch { + header: StateRootBatchHeader + stateRoots: string[] +} + +interface CrossDomainMessage { + target: string + sender: string + message: string + messageNonce: number +} + +interface StateTrieProof { + accountProof: string + storageProof: string +} + +const getMessageByTransactionHash = async ( + l2RpcProvider: ethers.providers.JsonRpcProvider, + l2CrossDomainMessengerAddress: string, + l2TransactionHash: string +): Promise => { + const transaction = await l2RpcProvider.getTransaction(l2TransactionHash) + if (transaction === null) { + throw new Error(`unable to find tx with hash: ${l2TransactionHash}`) + } + + const l2CrossDomainMessenger = new ethers.Contract( + l2CrossDomainMessengerAddress, + getContractInterface('OVM_L2CrossDomainMessenger'), + l2RpcProvider + ) + + const sentMessageEvents = await l2CrossDomainMessenger.queryFilter( + l2CrossDomainMessenger.filters.SentMessage(), + transaction.blockNumber, + transaction.blockNumber + ) + + if (sentMessageEvents.length === 0) { + return null + } + + if (sentMessageEvents.length > 1) { + throw new Error( + `can currently only support one message per transaction, found ${sentMessageEvents}` + ) + } + + const encodedMessage = sentMessageEvents[0].args.message + const decodedMessage = l2CrossDomainMessenger.interface.decodeFunctionData( + 'relayMessage', + encodedMessage + ) + + return { + target: decodedMessage._target, + sender: decodedMessage._sender, + message: decodedMessage._message, + messageNonce: decodedMessage._messageNonce.toNumber(), + } +} + +const encodeCrossDomainMessage = (message: CrossDomainMessage): string => { + return getContractInterface( + 'OVM_L2CrossDomainMessenger' + ).encodeFunctionData('relayMessage', [ + message.target, + message.sender, + message.message, + message.messageNonce, + ]) +} + +export const getStateBatchAppendedEventByTransactionIndex = async ( + l1RpcProvider: ethers.providers.JsonRpcProvider, + l1StateCommitmentChainAddress: string, + l2TransactionIndex: number +): Promise => { + const l1StateCommitmentChain = new ethers.Contract( + l1StateCommitmentChainAddress, + getContractInterface('OVM_StateCommitmentChain'), + l1RpcProvider + ) + + const getStateBatchAppendedEventByBatchIndex = async ( + index: number + ): Promise => { + const eventQueryResult = await l1StateCommitmentChain.queryFilter( + l1StateCommitmentChain.filters.StateBatchAppended(index) + ) + if (eventQueryResult.length === 0) { + return null + } else { + return eventQueryResult[0] + } + } + + const totalBatches = await l1StateCommitmentChain.getTotalBatches() + if (totalBatches === 0) { + return null + } + + const isEventHi = (event: ethers.Event, index: number) => { + const prevTotalElements = event.args._prevTotalElements.toNumber() + return index < prevTotalElements + } + + const isEventLo = (event: ethers.Event, index: number) => { + const prevTotalElements = event.args._prevTotalElements.toNumber() + const batchSize = event.args._batchSize.toNumber() + return index >= prevTotalElements + batchSize + } + + const lastBatchEvent = await getStateBatchAppendedEventByBatchIndex( + totalBatches - 1 + ) + if (isEventLo(lastBatchEvent, l2TransactionIndex)) { + return null + } else if (!isEventHi(lastBatchEvent, l2TransactionIndex)) { + return lastBatchEvent + } + + let lowerBound = 0 + let upperBound = totalBatches - 1 + let batchEvent: ethers.Event | null = lastBatchEvent + while (lowerBound < upperBound) { + const middleOfBounds = Math.floor((lowerBound + upperBound) / 2) + batchEvent = await getStateBatchAppendedEventByBatchIndex(middleOfBounds) + + if (isEventHi(batchEvent, l2TransactionIndex)) { + upperBound = middleOfBounds + } else if (isEventLo(batchEvent, l2TransactionIndex)) { + lowerBound = middleOfBounds + } else { + break + } + } + + return batchEvent +} + +export const getStateRootBatchByIndex = async ( + l1RpcProvider: ethers.providers.JsonRpcProvider, + l1StateCommitmentChainAddress: string, + l2TransactionIndex: number +): Promise => { + const l1StateCommitmentChain = new ethers.Contract( + l1StateCommitmentChainAddress, + getContractInterface('OVM_StateCommitmentChain'), + l1RpcProvider + ) + + const stateBatchAppendedEvent = await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + l1StateCommitmentChainAddress, + l2TransactionIndex + ) + if (stateBatchAppendedEvent === null) { + return null + } + + const stateBatchTransaction = await stateBatchAppendedEvent.getTransaction() + const [stateRoots] = l1StateCommitmentChain.interface.decodeFunctionData( + 'appendStateBatch', + stateBatchTransaction.data + ) + + return { + header: { + batchIndex: stateBatchAppendedEvent.args._batchIndex, + batchRoot: stateBatchAppendedEvent.args._batchRoot, + batchSize: stateBatchAppendedEvent.args._batchSize, + prevTotalElements: stateBatchAppendedEvent.args._prevTotalElements, + extraData: stateBatchAppendedEvent.args._extraData, + }, + stateRoots, + } +} + +const getMerkleTreeProof = (leaves: string[], index: number): string[] => { + const parsedLeaves = [] + for (let i = 0; i < Math.pow(2, Math.ceil(Math.log2(leaves.length))); i++) { + if (i < leaves.length) { + parsedLeaves.push(leaves[i]) + } else { + parsedLeaves.push(ethers.utils.keccak256('0x' + '00'.repeat(32))) + } + } + + const bufLeaves = parsedLeaves.map(fromHexString) + + const tree = new MerkleTree( + bufLeaves, + (el: Buffer | string): Buffer => { + return fromHexString(ethers.utils.keccak256(el)) + } + ) + + const proof = tree.getProof(bufLeaves[index], index).map((element: any) => { + return toHexString(element.data) + }) + + return proof +} + +const getStateTrieProof = async ( + l2RpcProvider: ethers.providers.JsonRpcProvider, + blockNumber: number, + address: string, + slot: string +): Promise => { + const proof = await l2RpcProvider.send('eth_getProof', [ + address, + [slot], + toRpcHexString(blockNumber), + ]) + + return { + accountProof: toHexString(rlp.encode(proof.accountProof)), + storageProof: toHexString(rlp.encode(proof.storageProof[0].proof)), + } +} + +/** + * Generates the transaction data to send to the L1CrossDomainMessenger in order to execute an + * L2 => L1 message. + * @param l2RpcProviderUrl + * @param l2CrossDomainMessengerAddress + * @param l2TransactionHash + * @param l2BlockOffset + * @returns 0x-prefixed transaction data as a hex string. + */ +export const makeRelayTransactionData = async ( + l1RpcProviderUrl: string, + l2RpcProviderUrl: string, + l1StateCommitmentChainAddress: string, + l2CrossDomainMessengerAddress: string, + l2TransactionHash: string +): Promise => { + const l1RpcProvider = new ethers.providers.JsonRpcProvider(l1RpcProviderUrl) + const l2RpcProvider = new ethers.providers.JsonRpcProvider(l2RpcProviderUrl) + + const l2Transaction = await l2RpcProvider.getTransaction(l2TransactionHash) + if (l2Transaction === null) { + throw new Error(`unable to find tx with hash: ${l2TransactionHash}`) + } + + const message = await getMessageByTransactionHash( + l2RpcProvider, + l2CrossDomainMessengerAddress, + l2TransactionHash + ) + if (message === null) { + throw new Error( + `unable to find a message to relay in tx with hash: ${l2TransactionHash}` + ) + } + + const messageSlot = ethers.utils.keccak256( + ethers.utils.keccak256( + encodeCrossDomainMessage(message) + + remove0x(l2CrossDomainMessengerAddress) + ) + '00'.repeat(32) + ) + + const stateTrieProof = await getStateTrieProof( + l2RpcProvider, + l2Transaction.blockNumber + NUM_L2_GENESIS_BLOCKS, + l2CrossDomainMessengerAddress, + messageSlot + ) + + const batch = await getStateRootBatchByIndex( + l1RpcProvider, + l1StateCommitmentChainAddress, + l2Transaction.blockNumber - NUM_L2_GENESIS_BLOCKS + ) + + const txIndexInBatch = + l2Transaction.blockNumber - batch.header.prevTotalElements.toNumber() + + const stateRootMerkleProof = getMerkleTreeProof( + batch.stateRoots, + txIndexInBatch + ) + + const relayTransactionData = getContractInterface( + 'OVM_L1CrossDomainMessenger' + ).encodeFunctionData('relayMessage', [ + message.target, + message.sender, + message.message, + message.messageNonce, + { + stateRoot: batch.stateRoots[txIndexInBatch], + stateRootBatchHeader: batch.header, + stateRootProof: { + index: txIndexInBatch, + siblings: stateRootMerkleProof, + }, + stateTrieWitness: stateTrieProof.accountProof, + storageTrieWitness: stateTrieProof.storageProof, + }, + ]) + + return relayTransactionData +} diff --git a/packages/message-relayer/test/setup.ts b/packages/message-relayer/test/setup.ts new file mode 100644 index 000000000000..f5952afdbe6b --- /dev/null +++ b/packages/message-relayer/test/setup.ts @@ -0,0 +1,12 @@ +/* External Imports */ +import chai = require('chai') +import Mocha from 'mocha' +import { solidity } from 'ethereum-waffle' +import chaiAsPromised from 'chai-as-promised' + +chai.use(solidity) +chai.use(chaiAsPromised) +const should = chai.should() +const expect = chai.expect + +export { should, expect, Mocha } diff --git a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts new file mode 100644 index 000000000000..db14cc660107 --- /dev/null +++ b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts @@ -0,0 +1,22 @@ +import { expect } from '../setup' + +/* Imports: External */ +import { ethers } from 'ethers' + +/* Imports: Internal */ +import { makeRelayTransactionData } from '../../src/relay-tx' + +describe('relay transaction generation functions', () => { + describe('makeRelayTransactionData', () => { + it('should do the thing', async () => { + const result = await makeRelayTransactionData( + 'https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27', + 'https://mainnet.optimism.io', + '0x6786EB419547a4902d285F70c6acDbC9AefAdB6F', + '0x4200000000000000000000000000000000000007', + '0x031b49156168b8d587a1bd37b598e907303304ae33bcdb015996e2f427a3aef0' + ) + console.log(result) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index eaf60064a1bc..9c0b57790d9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2248,6 +2248,13 @@ dependencies: "@types/chai" "*" +"@types/chai-as-promised@^7.1.4": + version "7.1.4" + resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz#caf64e76fb056b8c8ced4b761ed499272b737601" + integrity sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA== + dependencies: + "@types/chai" "*" + "@types/chai@*", "@types/chai@^4.1.7": version "4.2.16" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.16.tgz#f09cc36e18d28274f942e7201147cce34d97e8c8" @@ -7178,6 +7185,57 @@ hardhat@^2.2.1: uuid "^3.3.2" ws "^7.2.1" +hardhat@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.3.0.tgz#5c29f8b4d08155c3dc8c908af9713fd5079522d5" + integrity sha512-nc4ro2bM4wPaA6/0Y22o5F5QrifQk2KCyPUUKLPUeFFZoGNGYB8vmeW/k9gV9DdMukdWTzfYlKc2Jn4bfb6tDQ== + dependencies: + "@ethereumjs/block" "^3.2.1" + "@ethereumjs/blockchain" "^5.2.1" + "@ethereumjs/common" "^2.2.0" + "@ethereumjs/tx" "^3.1.3" + "@ethereumjs/vm" "^5.3.2" + "@sentry/node" "^5.18.1" + "@solidity-parser/parser" "^0.11.0" + "@types/bn.js" "^5.1.0" + "@types/lru-cache" "^5.1.0" + abort-controller "^3.0.0" + adm-zip "^0.4.16" + ansi-escapes "^4.3.0" + chalk "^2.4.2" + chokidar "^3.4.0" + ci-info "^2.0.0" + debug "^4.1.1" + enquirer "^2.3.0" + env-paths "^2.2.0" + eth-sig-util "^2.5.2" + ethereum-cryptography "^0.1.2" + ethereumjs-abi "^0.6.8" + ethereumjs-util "^7.0.10" + find-up "^2.1.0" + fp-ts "1.19.3" + fs-extra "^7.0.1" + glob "^7.1.3" + immutable "^4.0.0-rc.12" + io-ts "1.10.4" + lodash "^4.17.11" + merkle-patricia-tree "^4.1.0" + mnemonist "^0.38.0" + mocha "^7.1.2" + node-fetch "^2.6.0" + qs "^6.7.0" + raw-body "^2.4.1" + resolve "1.17.0" + semver "^6.3.0" + slash "^3.0.0" + solc "0.7.3" + source-map-support "^0.5.13" + stacktrace-parser "^0.1.10" + "true-case-path" "^2.2.1" + tsort "0.0.1" + uuid "^3.3.2" + ws "^7.2.1" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" From f0519ad6d1aad0667550768b84f56a778e0e16ea Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 25 May 2021 00:31:19 -0400 Subject: [PATCH 02/13] whoops, I burned our infura key --- .../message-relayer/test/unit-tests/relay-tx.spec.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts index db14cc660107..e9103aafe01d 100644 --- a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts +++ b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts @@ -8,15 +8,5 @@ import { makeRelayTransactionData } from '../../src/relay-tx' describe('relay transaction generation functions', () => { describe('makeRelayTransactionData', () => { - it('should do the thing', async () => { - const result = await makeRelayTransactionData( - 'https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27', - 'https://mainnet.optimism.io', - '0x6786EB419547a4902d285F70c6acDbC9AefAdB6F', - '0x4200000000000000000000000000000000000007', - '0x031b49156168b8d587a1bd37b598e907303304ae33bcdb015996e2f427a3aef0' - ) - console.log(result) - }) }) }) From e891f8aab66e1e99bf0bfaac1f04207f874e899f Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 25 May 2021 12:28:04 -0400 Subject: [PATCH 03/13] fix minor bug --- packages/message-relayer/package.json | 3 +-- packages/message-relayer/src/relay-tx.ts | 4 ++-- packages/message-relayer/test/unit-tests/relay-tx.spec.ts | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/message-relayer/package.json b/packages/message-relayer/package.json index e357e670e88d..3765d424a183 100644 --- a/packages/message-relayer/package.json +++ b/packages/message-relayer/package.json @@ -35,7 +35,7 @@ "@eth-optimism/core-utils": "^0.4.3", "bcfg": "^0.1.6", "dotenv": "^8.2.0", - "ethers": "^5.1.0", + "ethers": "^5.2.0", "google-spreadsheet": "^3.1.15", "merkletreejs": "^0.2.18", "rlp": "^2.2.6" @@ -49,7 +49,6 @@ "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "ethereum-waffle": "^3.3.0", - "ethers": "^5.2.0", "hardhat": "^2.3.0", "mocha": "^8.4.0", "prettier": "^2.2.1", diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts index 177b5eb2a5b7..ef058a9b3c8c 100644 --- a/packages/message-relayer/src/relay-tx.ts +++ b/packages/message-relayer/src/relay-tx.ts @@ -6,7 +6,7 @@ import { toHexString, toRpcHexString, } from '@eth-optimism/core-utils' -import { getContractInterface } from '@eth-optimism/contracts' +import { getContractInterface, predeploys } from '@eth-optimism/contracts' import * as rlp from 'rlp' import { MerkleTree } from 'merkletreejs' @@ -293,7 +293,7 @@ export const makeRelayTransactionData = async ( const stateTrieProof = await getStateTrieProof( l2RpcProvider, l2Transaction.blockNumber + NUM_L2_GENESIS_BLOCKS, - l2CrossDomainMessengerAddress, + predeploys.OVM_L2ToL1MessagePasser, messageSlot ) diff --git a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts index e9103aafe01d..aba090f2fbc6 100644 --- a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts +++ b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts @@ -7,6 +7,5 @@ import { ethers } from 'ethers' import { makeRelayTransactionData } from '../../src/relay-tx' describe('relay transaction generation functions', () => { - describe('makeRelayTransactionData', () => { - }) + describe('makeRelayTransactionData', () => {}) }) From 897ba676e5d298abda12d221ce1002699319e85d Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 25 May 2021 12:58:25 -0400 Subject: [PATCH 04/13] add comments --- packages/message-relayer/src/relay-tx.ts | 68 +++++++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts index ef058a9b3c8c..c40ecb4a071e 100644 --- a/packages/message-relayer/src/relay-tx.ts +++ b/packages/message-relayer/src/relay-tx.ts @@ -41,6 +41,13 @@ interface StateTrieProof { storageProof: string } +/** + * Finds the L2 => L1 message triggered by a given L2 transaction, if the message exists. + * @param l2RpcProvider L2 RPC provider. + * @param l2CrossDomainMessengerAddress Address of the L2CrossDomainMessenger. + * @param l2TransactionHash Hash of the L2 transaction to find a message for. + * @returns Message assocaited with the transaction or null if no such message exists. + */ const getMessageByTransactionHash = async ( l2RpcProvider: ethers.providers.JsonRpcProvider, l2CrossDomainMessengerAddress: string, @@ -87,6 +94,11 @@ const getMessageByTransactionHash = async ( } } +/** + * Encodes a cross domain message. + * @param message Message to encode. + * @returns Encoded message. + */ const encodeCrossDomainMessage = (message: CrossDomainMessage): string => { return getContractInterface( 'OVM_L2CrossDomainMessenger' @@ -98,6 +110,13 @@ const encodeCrossDomainMessage = (message: CrossDomainMessage): string => { ]) } +/** + * Finds the StateBatchAppended event associated with a given L2 transaction. + * @param l1RpcProvider L1 RPC provider. + * @param l1StateCommitmentChainAddress Address of the L1StateCommitmentChain. + * @param l2TransactionIndex Index of the L2 transaction to find a StateBatchAppended event for. + * @returns StateBatchAppended event for the given transaction or null if no such event exists. + */ export const getStateBatchAppendedEventByTransactionIndex = async ( l1RpcProvider: ethers.providers.JsonRpcProvider, l1StateCommitmentChainAddress: string, @@ -166,7 +185,15 @@ export const getStateBatchAppendedEventByTransactionIndex = async ( return batchEvent } -export const getStateRootBatchByIndex = async ( +/** + * Finds the full state root batch associated with a given transaction index. + * @param l1RpcProvider L1 RPC provider. + * @param l1StateCommitmentChainAddress Address of the L1StateCommitmentChain. + * @param l2TransactionIndex Index of the L2 transaction to find a state root batch for. + * @returns State root batch associated with the given transaction index or null if no state root + * batch exists. + */ +export const getStateRootBatchByTransactionIndex = async ( l1RpcProvider: ethers.providers.JsonRpcProvider, l1StateCommitmentChainAddress: string, l2TransactionIndex: number @@ -204,6 +231,12 @@ export const getStateRootBatchByIndex = async ( } } +/** + * Generates a Merkle proof (using the particular scheme we use within Lib_MerkleTree). + * @param leaves Leaves of the merkle tree. + * @param index Index to generate a proof for. + * @returns Merkle proof sibling leaves, as hex strings. + */ const getMerkleTreeProof = (leaves: string[], index: number): string[] => { const parsedLeaves = [] for (let i = 0; i < Math.pow(2, Math.ceil(Math.log2(leaves.length))); i++) { @@ -230,6 +263,14 @@ const getMerkleTreeProof = (leaves: string[], index: number): string[] => { return proof } +/** + * Generates a Merkle-Patricia trie proof for a given account and storage slot. + * @param l2RpcProvider L2 RPC provider. + * @param blockNumber Block number to generate the proof at. + * @param address Address to generate the proof for. + * @param slot Storage slot to generate the proof for. + * @returns Account proof and storage proof. + */ const getStateTrieProof = async ( l2RpcProvider: ethers.providers.JsonRpcProvider, blockNumber: number, @@ -251,10 +292,11 @@ const getStateTrieProof = async ( /** * Generates the transaction data to send to the L1CrossDomainMessenger in order to execute an * L2 => L1 message. - * @param l2RpcProviderUrl - * @param l2CrossDomainMessengerAddress - * @param l2TransactionHash - * @param l2BlockOffset + * @param l1RpcProviderUrl L1 RPC provider url. + * @param l2RpcProviderUrl L2 RPC provider url. + * @param l1StateCommitmentChainAddress Address of the StateCommitmentChain. + * @param l2CrossDomainMessengerAddress Address of the L2CrossDomainMessenger. + * @param l2TransactionHash L2 transaction hash to generate a relay transaction for. * @returns 0x-prefixed transaction data as a hex string. */ export const makeRelayTransactionData = async ( @@ -267,11 +309,13 @@ export const makeRelayTransactionData = async ( const l1RpcProvider = new ethers.providers.JsonRpcProvider(l1RpcProviderUrl) const l2RpcProvider = new ethers.providers.JsonRpcProvider(l2RpcProviderUrl) + // Step 1: Find the transaction. const l2Transaction = await l2RpcProvider.getTransaction(l2TransactionHash) if (l2Transaction === null) { throw new Error(`unable to find tx with hash: ${l2TransactionHash}`) } + // Step 2: Find the message associated with the transaction. const message = await getMessageByTransactionHash( l2RpcProvider, l2CrossDomainMessengerAddress, @@ -283,13 +327,13 @@ export const makeRelayTransactionData = async ( ) } + // Step 3: Generate a state trie proof for the slot where the message is located. const messageSlot = ethers.utils.keccak256( ethers.utils.keccak256( encodeCrossDomainMessage(message) + remove0x(l2CrossDomainMessengerAddress) ) + '00'.repeat(32) ) - const stateTrieProof = await getStateTrieProof( l2RpcProvider, l2Transaction.blockNumber + NUM_L2_GENESIS_BLOCKS, @@ -297,20 +341,28 @@ export const makeRelayTransactionData = async ( messageSlot ) - const batch = await getStateRootBatchByIndex( + // Step 4: Find the full batch associated with the transaction. + const batch = await getStateRootBatchByTransactionIndex( l1RpcProvider, l1StateCommitmentChainAddress, l2Transaction.blockNumber - NUM_L2_GENESIS_BLOCKS ) + if (batch === null) { + throw new Error( + `unable to find state root batch for tx with hash: ${l2TransactionHash}` + ) + } + // Step 5: Generate a Merkle proof for the state root associated with the transaction inside of + // the Merkle tree of state roots published as a batch. const txIndexInBatch = l2Transaction.blockNumber - batch.header.prevTotalElements.toNumber() - const stateRootMerkleProof = getMerkleTreeProof( batch.stateRoots, txIndexInBatch ) + // Step 6: Generate the transaction data. const relayTransactionData = getContractInterface( 'OVM_L1CrossDomainMessenger' ).encodeFunctionData('relayMessage', [ From ad6332c92d01b7f59764cdd4cdf1bd015dc30d97 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 25 May 2021 14:23:46 -0400 Subject: [PATCH 05/13] add more comments and clean stuff up --- packages/message-relayer/src/index.ts | 1 + packages/message-relayer/src/relay-tx.ts | 36 ++++++++++++++++-------- 2 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 packages/message-relayer/src/index.ts diff --git a/packages/message-relayer/src/index.ts b/packages/message-relayer/src/index.ts new file mode 100644 index 000000000000..419b5715f12d --- /dev/null +++ b/packages/message-relayer/src/index.ts @@ -0,0 +1 @@ +export * from './relay-tx' diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts index c40ecb4a071e..7abc8e6c327f 100644 --- a/packages/message-relayer/src/relay-tx.ts +++ b/packages/message-relayer/src/relay-tx.ts @@ -53,6 +53,7 @@ const getMessageByTransactionHash = async ( l2CrossDomainMessengerAddress: string, l2TransactionHash: string ): Promise => { + // Complain if we can't find the given transaction. const transaction = await l2RpcProvider.getTransaction(l2TransactionHash) if (transaction === null) { throw new Error(`unable to find tx with hash: ${l2TransactionHash}`) @@ -64,22 +65,28 @@ const getMessageByTransactionHash = async ( l2RpcProvider ) + // Find all SentMessage events created in the same block as the given transaction. This is + // reliable because we should only have one transaction per block. const sentMessageEvents = await l2CrossDomainMessenger.queryFilter( l2CrossDomainMessenger.filters.SentMessage(), transaction.blockNumber, transaction.blockNumber ) + // If there were no SentMessage events then this transaction did not send any L2 => L1 messaages. if (sentMessageEvents.length === 0) { return null } + // Not sure exactly how to handle the special case that more than one message was sent in the + // same transaction. For now throwing an error seems fine. We can always deal with this later. if (sentMessageEvents.length > 1) { throw new Error( `can currently only support one message per transaction, found ${sentMessageEvents}` ) } + // Decode the message and turn it into a nicer struct. const encodedMessage = sentMessageEvents[0].args.message const decodedMessage = l2CrossDomainMessenger.interface.decodeFunctionData( 'relayMessage', @@ -141,11 +148,6 @@ export const getStateBatchAppendedEventByTransactionIndex = async ( } } - const totalBatches = await l1StateCommitmentChain.getTotalBatches() - if (totalBatches === 0) { - return null - } - const isEventHi = (event: ethers.Event, index: number) => { const prevTotalElements = event.args._prevTotalElements.toNumber() return index < prevTotalElements @@ -157,18 +159,28 @@ export const getStateBatchAppendedEventByTransactionIndex = async ( return index >= prevTotalElements + batchSize } - const lastBatchEvent = await getStateBatchAppendedEventByBatchIndex( - totalBatches - 1 - ) - if (isEventLo(lastBatchEvent, l2TransactionIndex)) { + const totalBatches = await l1StateCommitmentChain.getTotalBatches() + if (totalBatches === 0) { return null - } else if (!isEventHi(lastBatchEvent, l2TransactionIndex)) { - return lastBatchEvent } let lowerBound = 0 let upperBound = totalBatches - 1 - let batchEvent: ethers.Event | null = lastBatchEvent + let batchEvent: ethers.Event | null = await getStateBatchAppendedEventByBatchIndex( + upperBound + ) + + if (isEventLo(batchEvent, l2TransactionIndex)) { + // Upper bound is too low, means this transaction doesn't have a corresponding state batch yet. + return null + } else if (!isEventHi(batchEvent, l2TransactionIndex)) { + // Upper bound is not too low and also not too high. This means the upper bound event is the + // one we're looking for! Return it. + return batchEvent + } + + // Binary search to find the right event. The above checks will guarantee that the event does + // exist and that we'll find it during this search. while (lowerBound < upperBound) { const middleOfBounds = Math.floor((lowerBound + upperBound) / 2) batchEvent = await getStateBatchAppendedEventByBatchIndex(middleOfBounds) From 3b2dfb815b65663b751ead602bcbcb4de659fe08 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 25 May 2021 14:41:26 -0400 Subject: [PATCH 06/13] add empty test descriptions --- packages/message-relayer/src/relay-tx.ts | 2 +- .../test/unit-tests/relay-tx.spec.ts | 57 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts index 7abc8e6c327f..4c69f65c3301 100644 --- a/packages/message-relayer/src/relay-tx.ts +++ b/packages/message-relayer/src/relay-tx.ts @@ -48,7 +48,7 @@ interface StateTrieProof { * @param l2TransactionHash Hash of the L2 transaction to find a message for. * @returns Message assocaited with the transaction or null if no such message exists. */ -const getMessageByTransactionHash = async ( +export const getMessageByTransactionHash = async ( l2RpcProvider: ethers.providers.JsonRpcProvider, l2CrossDomainMessengerAddress: string, l2TransactionHash: string diff --git a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts index aba090f2fbc6..4dffc9a9e99f 100644 --- a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts +++ b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts @@ -4,8 +4,61 @@ import { expect } from '../setup' import { ethers } from 'ethers' /* Imports: Internal */ -import { makeRelayTransactionData } from '../../src/relay-tx' +import { + makeRelayTransactionData, + getMessageByTransactionHash, +} from '../../src/relay-tx' describe('relay transaction generation functions', () => { - describe('makeRelayTransactionData', () => {}) + describe('getMessageByTransactionHash', () => { + it('should throw an error if a transaction with the given hash does not exist', async () => {}) + + it('should return null if the transaction did not emit a SentMessage event', async () => {}) + + it('should throw an error if the transaction emitted more than one SentMessage event', async () => {}) + + it('should return the parsed event if the transaction emitted exactly one SentMessage event', async () => {}) + }) + + describe('getStateBatchAppendedEventByTransactionIndex', () => { + it('should return null if a batch for the index does not exist', async () => {}) + + it('should return null when there are no batches yet', async () => {}) + + it('should return the batch if the index is part of the last batch', async () => {}) + + it('should return the batch if the index is part of teh first batch', async () => {}) + + for (const numBatches of [1, 2, 8, 64, 128]) { + describe(`when there are ${numBatches} batch(es)`, () => { + for (const batchSize of [1, 2, 8, 64, 128]) { + describe(`when there are ${batchSize} element(s) per batch`, () => { + for ( + let i = batchSize - 1; + i < batchSize * numBatches; + i += batchSize + ) { + it(`should be able to get the correct batch for the ${batchSize}th/st/rd/whatever element`, async () => {}) + } + }) + } + }) + } + }) + + describe('getStateRootBatchByTransactionIndex', () => { + it('should return null if a batch for the index does not exist', async () => {}) + + it('should return the full batch for a given index when it exists', async () => {}) + }) + + describe('makeRelayTransactionData', () => { + it('should throw an error if the transaction does not exist', async () => {}) + + it('should throw an error if the transaction did not send a message', async () => {}) + + it('should throw an error if the corresponding state batch has not been submitted', async () => {}) + + it('should otherwise return the encoded transaction data', () => {}) + }) }) From b2b07b9de801f7672067301ab61827b6c6397a59 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 25 May 2021 18:56:15 -0400 Subject: [PATCH 07/13] add tests --- packages/message-relayer/package.json | 4 +- packages/message-relayer/src/relay-tx.ts | 15 +- .../MockL2CrossDomainMessenger.sol | 44 +++ .../test/unit-tests/relay-tx.spec.ts | 363 ++++++++++++++++-- 4 files changed, 391 insertions(+), 35 deletions(-) create mode 100644 packages/message-relayer/test/test-contracts/MockL2CrossDomainMessenger.sol diff --git a/packages/message-relayer/package.json b/packages/message-relayer/package.json index 3765d424a183..341381a4ac04 100644 --- a/packages/message-relayer/package.json +++ b/packages/message-relayer/package.json @@ -33,9 +33,10 @@ "@eth-optimism/common-ts": "^0.1.2", "@eth-optimism/contracts": "^0.3.1", "@eth-optimism/core-utils": "^0.4.3", + "@eth-optimism/smock": "^1.1.4", "bcfg": "^0.1.6", "dotenv": "^8.2.0", - "ethers": "^5.2.0", + "ethers": "^5.1.0", "google-spreadsheet": "^3.1.15", "merkletreejs": "^0.2.18", "rlp": "^2.2.6" @@ -50,6 +51,7 @@ "chai-as-promised": "^7.1.1", "ethereum-waffle": "^3.3.0", "hardhat": "^2.3.0", + "lodash": "^4.17.21", "mocha": "^8.4.0", "prettier": "^2.2.1", "tslint": "^6.1.3", diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts index 4c69f65c3301..7faa46c9db09 100644 --- a/packages/message-relayer/src/relay-tx.ts +++ b/packages/message-relayer/src/relay-tx.ts @@ -159,7 +159,9 @@ export const getStateBatchAppendedEventByTransactionIndex = async ( return index >= prevTotalElements + batchSize } - const totalBatches = await l1StateCommitmentChain.getTotalBatches() + const totalBatches = ( + await l1StateCommitmentChain.getTotalBatches() + ).toNumber() if (totalBatches === 0) { return null } @@ -304,23 +306,20 @@ const getStateTrieProof = async ( /** * Generates the transaction data to send to the L1CrossDomainMessenger in order to execute an * L2 => L1 message. - * @param l1RpcProviderUrl L1 RPC provider url. - * @param l2RpcProviderUrl L2 RPC provider url. + * @param l1RpcProvider L1 RPC provider. + * @param l2RpcProvider L2 RPC provider. * @param l1StateCommitmentChainAddress Address of the StateCommitmentChain. * @param l2CrossDomainMessengerAddress Address of the L2CrossDomainMessenger. * @param l2TransactionHash L2 transaction hash to generate a relay transaction for. * @returns 0x-prefixed transaction data as a hex string. */ export const makeRelayTransactionData = async ( - l1RpcProviderUrl: string, - l2RpcProviderUrl: string, + l1RpcProvider: ethers.providers.JsonRpcProvider, + l2RpcProvider: ethers.providers.JsonRpcProvider, l1StateCommitmentChainAddress: string, l2CrossDomainMessengerAddress: string, l2TransactionHash: string ): Promise => { - const l1RpcProvider = new ethers.providers.JsonRpcProvider(l1RpcProviderUrl) - const l2RpcProvider = new ethers.providers.JsonRpcProvider(l2RpcProviderUrl) - // Step 1: Find the transaction. const l2Transaction = await l2RpcProvider.getTransaction(l2TransactionHash) if (l2Transaction === null) { diff --git a/packages/message-relayer/test/test-contracts/MockL2CrossDomainMessenger.sol b/packages/message-relayer/test/test-contracts/MockL2CrossDomainMessenger.sol new file mode 100644 index 000000000000..7545691229b8 --- /dev/null +++ b/packages/message-relayer/test/test-contracts/MockL2CrossDomainMessenger.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.0 <0.9.0; +pragma experimental ABIEncoderV2; + +contract MockL2CrossDomainMessenger { + struct MessageData { + address target; + address sender; + bytes message; + uint256 messageNonce; + } + + event SentMessage(bytes message); + + function emitSentMessageEvent( + MessageData memory _message + ) + public + { + emit SentMessage( + abi.encodeWithSignature( + "relayMessage(address,address,bytes,uint256)", + _message.target, + _message.sender, + _message.message, + _message.messageNonce + ) + ); + } + + function emitMultipleSentMessageEvents( + MessageData[] memory _messages + ) + public + { + for (uint256 i = 0; i < _messages.length; i++) { + emitSentMessageEvent( + _messages[i] + ); + } + } + + function doNothing() public {} +} diff --git a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts index 4dffc9a9e99f..f4bb68d03f2c 100644 --- a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts +++ b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts @@ -1,45 +1,278 @@ import { expect } from '../setup' /* Imports: External */ -import { ethers } from 'ethers' +import hre from 'hardhat' +import { Contract, Signer } from 'ethers' +import { getContractFactory } from '@eth-optimism/contracts' +import { smockit } from '@eth-optimism/smock' +import { toPlainObject } from 'lodash' /* Imports: Internal */ import { makeRelayTransactionData, + getStateRootBatchByTransactionIndex, + getStateBatchAppendedEventByTransactionIndex, getMessageByTransactionHash, } from '../../src/relay-tx' describe('relay transaction generation functions', () => { + const ethers = (hre as any).ethers + const l1RpcProvider = ethers.provider + const l2RpcProvider = ethers.provider + + let signer1: Signer + before(async () => { + ;[signer1] = await ethers.getSigners() + }) + + let MockL2CrossDomainMessenger: Contract + beforeEach(async () => { + const factory = await ethers.getContractFactory( + 'MockL2CrossDomainMessenger' + ) + MockL2CrossDomainMessenger = await factory.deploy() + }) + + let StateCommitmentChain: Contract + beforeEach(async () => { + const factory1 = getContractFactory('Lib_AddressManager') + const factory2 = getContractFactory('OVM_ChainStorageContainer') + const factory3 = getContractFactory('OVM_StateCommitmentChain') + + const mockBondManager = await smockit(getContractFactory('OVM_BondManager')) + const mockCanonicalTransactionChain = await smockit( + getContractFactory('OVM_CanonicalTransactionChain') + ) + + mockBondManager.smocked.isCollateralized.will.return.with(true) + mockCanonicalTransactionChain.smocked.getTotalElements.will.return.with( + 999999 + ) + + const AddressManager = await factory1.connect(signer1).deploy() + const ChainStorageContainer = await factory2 + .connect(signer1) + .deploy(AddressManager.address, 'OVM_StateCommitmentChain') + StateCommitmentChain = await factory3 + .connect(signer1) + .deploy(AddressManager.address, 0, 0) + + await AddressManager.setAddress( + 'OVM_ChainStorageContainer:SCC:batches', + ChainStorageContainer.address + ) + + await AddressManager.setAddress( + 'OVM_StateCommitmentChain', + StateCommitmentChain.address + ) + + await AddressManager.setAddress('OVM_BondManager', mockBondManager.address) + + await AddressManager.setAddress( + 'OVM_CanonicalTransactionChain', + mockCanonicalTransactionChain.address + ) + }) + describe('getMessageByTransactionHash', () => { - it('should throw an error if a transaction with the given hash does not exist', async () => {}) + it('should throw an error if a transaction with the given hash does not exist', async () => { + await expect( + getMessageByTransactionHash( + l2RpcProvider, + MockL2CrossDomainMessenger.address, + ethers.constants.HashZero + ) + ).to.be.rejected + }) - it('should return null if the transaction did not emit a SentMessage event', async () => {}) + it('should return null if the transaction did not emit a SentMessage event', async () => { + const tx = await MockL2CrossDomainMessenger.doNothing() - it('should throw an error if the transaction emitted more than one SentMessage event', async () => {}) + expect( + await getMessageByTransactionHash( + l2RpcProvider, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.equal(null) + }) - it('should return the parsed event if the transaction emitted exactly one SentMessage event', async () => {}) + it('should throw an error if the transaction emitted more than one SentMessage event', async () => { + const tx = await MockL2CrossDomainMessenger.emitMultipleSentMessageEvents( + [ + { + target: ethers.constants.AddressZero, + sender: ethers.constants.AddressZero, + message: '0x', + messageNonce: 0, + }, + { + target: ethers.constants.AddressZero, + sender: ethers.constants.AddressZero, + message: '0x', + messageNonce: 1, + }, + ] + ) + + await expect( + getMessageByTransactionHash( + l2RpcProvider, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.be.rejected + }) + + it('should return the parsed event if the transaction emitted exactly one SentMessage event', async () => { + const message = { + target: ethers.constants.AddressZero, + sender: ethers.constants.AddressZero, + message: '0x', + messageNonce: 0, + } + const tx = await MockL2CrossDomainMessenger.emitSentMessageEvent(message) + + expect( + await getMessageByTransactionHash( + l2RpcProvider, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.deep.equal(message) + }) }) describe('getStateBatchAppendedEventByTransactionIndex', () => { - it('should return null if a batch for the index does not exist', async () => {}) + it('should return null when there are no batches yet', async () => { + expect( + await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 0 + ) + ).to.equal(null) + }) + + it('should return null if a batch for the index does not exist', async () => { + // Should have a total of 1 element now. + await StateCommitmentChain.appendStateBatch( + [ethers.constants.HashZero], + 0 + ) - it('should return null when there are no batches yet', async () => {}) + expect( + await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 1 // Index 0 is ok but 1 should return null + ) + ).to.equal(null) + }) - it('should return the batch if the index is part of the last batch', async () => {}) + it('should return the batch if the index is part of the first batch', async () => { + // 5 elements + await StateCommitmentChain.appendStateBatch( + [ + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ], + 0 + ) - it('should return the batch if the index is part of teh first batch', async () => {}) + // Add another 5 so we have two batches and can isolate tests against the first. + await StateCommitmentChain.appendStateBatch( + [ + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ], + 5 + ) - for (const numBatches of [1, 2, 8, 64, 128]) { - describe(`when there are ${numBatches} batch(es)`, () => { - for (const batchSize of [1, 2, 8, 64, 128]) { - describe(`when there are ${batchSize} element(s) per batch`, () => { - for ( - let i = batchSize - 1; - i < batchSize * numBatches; - i += batchSize - ) { - it(`should be able to get the correct batch for the ${batchSize}th/st/rd/whatever element`, async () => {}) - } + const event = await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 1 + ) + + expect(toPlainObject(event.args)).to.deep.include({ + _batchIndex: ethers.BigNumber.from(0), + _batchSize: ethers.BigNumber.from(5), + _prevTotalElements: ethers.BigNumber.from(0), + }) + }) + + it('should return the batch if the index is part of the last batch', async () => { + // 5 elements + await StateCommitmentChain.appendStateBatch( + [ + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ], + 0 + ) + + // Add another 5 so we have two batches and can isolate tests against the second. + await StateCommitmentChain.appendStateBatch( + [ + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ], + 5 + ) + + const event = await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 7 + ) + + expect(toPlainObject(event.args)).to.deep.include({ + _batchIndex: ethers.BigNumber.from(1), + _batchSize: ethers.BigNumber.from(5), + _prevTotalElements: ethers.BigNumber.from(5), + }) + }) + + for (const numBatches of [1, 2, 8]) { + const elementsPerBatch = 8 + describe(`when there are ${numBatches} batch(es) of ${elementsPerBatch} elements each`, () => { + const totalElements = numBatches * elementsPerBatch + beforeEach(async () => { + for (let i = 0; i < numBatches; i++) { + await StateCommitmentChain.appendStateBatch( + new Array(elementsPerBatch).fill(ethers.constants.HashZero), + i * elementsPerBatch + ) + } + }) + + for (let i = 0; i < totalElements; i += elementsPerBatch) { + it(`should be able to get the correct event for the ${i}th/st/rd/whatever element`, async () => { + const event = await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + i + ) + + expect(toPlainObject(event.args)).to.deep.include({ + _batchIndex: ethers.BigNumber.from(i / elementsPerBatch), + _batchSize: ethers.BigNumber.from(elementsPerBatch), + _prevTotalElements: ethers.BigNumber.from(i), + }) }) } }) @@ -47,18 +280,96 @@ describe('relay transaction generation functions', () => { }) describe('getStateRootBatchByTransactionIndex', () => { - it('should return null if a batch for the index does not exist', async () => {}) + it('should return null if a batch for the index does not exist', async () => { + // Should have a total of 1 element now. + await StateCommitmentChain.appendStateBatch( + [ethers.constants.HashZero], + 0 + ) + + expect( + await getStateRootBatchByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 1 // Index 0 is ok but 1 should return null + ) + ).to.equal(null) + }) + + it('should return the full batch for a given index when it exists', async () => { + // Should have a total of 1 element now. + await StateCommitmentChain.appendStateBatch( + [ethers.constants.HashZero], + 0 + ) - it('should return the full batch for a given index when it exists', async () => {}) + const batch = await getStateRootBatchByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 0 // Index 0 is ok but 1 should return null + ) + + expect(batch.header).to.deep.include({ + batchIndex: ethers.BigNumber.from(0), + batchSize: ethers.BigNumber.from(1), + prevTotalElements: ethers.BigNumber.from(0), + }) + + expect(batch.stateRoots).to.deep.equal([ethers.constants.HashZero]) + }) }) describe('makeRelayTransactionData', () => { - it('should throw an error if the transaction does not exist', async () => {}) + it('should throw an error if the transaction does not exist', async () => { + await expect( + makeRelayTransactionData( + l1RpcProvider, + l2RpcProvider, + StateCommitmentChain.address, + MockL2CrossDomainMessenger.address, + ethers.constants.HashZero + ) + ).to.be.rejected + }) + + it('should throw an error if the transaction did not send a message', async () => { + const tx = await MockL2CrossDomainMessenger.doNothing() - it('should throw an error if the transaction did not send a message', async () => {}) + await expect( + makeRelayTransactionData( + l1RpcProvider, + l2RpcProvider, + StateCommitmentChain.address, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.be.rejected + }) + + it('should throw an error if the corresponding state batcsenderh has not been submitted', async () => { + const tx = await MockL2CrossDomainMessenger.emitSentMessageEvent({ + target: ethers.constants.AddressZero, + sender: ethers.constants.AddressZero, + message: '0x', + messageNonce: 0, + }) - it('should throw an error if the corresponding state batch has not been submitted', async () => {}) + await expect( + makeRelayTransactionData( + l1RpcProvider, + l2RpcProvider, + StateCommitmentChain.address, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.be.rejected + }) - it('should otherwise return the encoded transaction data', () => {}) + // Unfortunately this is hard to test here because hardhat doesn't support eth_getProof. + // Because this function is embedded into the message relayer, we should be able to use + // integration tests to sufficiently test this. + it.skip('should otherwise return the encoded transaction data', () => { + // TODO? + }) }) }) From 0984e48eedcbcfa47cb0f2192ee71e004041a047 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 25 May 2021 20:56:25 -0400 Subject: [PATCH 08/13] move smock to dev deps --- packages/message-relayer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/message-relayer/package.json b/packages/message-relayer/package.json index 341381a4ac04..c4d6f7191142 100644 --- a/packages/message-relayer/package.json +++ b/packages/message-relayer/package.json @@ -33,7 +33,6 @@ "@eth-optimism/common-ts": "^0.1.2", "@eth-optimism/contracts": "^0.3.1", "@eth-optimism/core-utils": "^0.4.3", - "@eth-optimism/smock": "^1.1.4", "bcfg": "^0.1.6", "dotenv": "^8.2.0", "ethers": "^5.1.0", @@ -42,6 +41,7 @@ "rlp": "^2.2.6" }, "devDependencies": { + "@eth-optimism/smock": "^1.1.4", "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-waffle": "^2.0.1", "@types/chai": "^4.2.18", From c3419708e4e91bd10f26cc38a08e8e986b07b6f3 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 26 May 2021 15:53:56 -0400 Subject: [PATCH 09/13] chore: add changeset --- .changeset/sharp-files-knock.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sharp-files-knock.md diff --git a/.changeset/sharp-files-knock.md b/.changeset/sharp-files-knock.md new file mode 100644 index 000000000000..c74d2c14e4c3 --- /dev/null +++ b/.changeset/sharp-files-knock.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/message-relayer': patch +--- + +Adds a new set of tools for generating messages to be relayed and their proofs From 5f02c19526ec864181f5240a0220c011b2800b65 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 26 May 2021 18:01:11 -0400 Subject: [PATCH 10/13] minor cleanup to merkle tree proof function --- packages/message-relayer/src/relay-tx.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts index 7faa46c9db09..98eac3dae0a7 100644 --- a/packages/message-relayer/src/relay-tx.ts +++ b/packages/message-relayer/src/relay-tx.ts @@ -252,8 +252,12 @@ export const getStateRootBatchByTransactionIndex = async ( * @returns Merkle proof sibling leaves, as hex strings. */ const getMerkleTreeProof = (leaves: string[], index: number): string[] => { + // Our specific Merkle tree implementation requires that the number of leaves is a power of 2. + // If the number of given leaves is less than a power of 2, we need to round up to the next + // available power of 2. We fill the remaining space with the hash of bytes32(0). + const correctedTreeSize = Math.pow(2, Math.ceil(Math.log2(leaves.length))) const parsedLeaves = [] - for (let i = 0; i < Math.pow(2, Math.ceil(Math.log2(leaves.length))); i++) { + for (let i = 0; i < correctedTreeSize; i++) { if (i < leaves.length) { parsedLeaves.push(leaves[i]) } else { @@ -261,8 +265,8 @@ const getMerkleTreeProof = (leaves: string[], index: number): string[] => { } } + // merkletreejs prefers things to be Buffers. const bufLeaves = parsedLeaves.map(fromHexString) - const tree = new MerkleTree( bufLeaves, (el: Buffer | string): Buffer => { From f24fb7496924f98949c2f11ea494f0d4389a962d Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 26 May 2021 18:03:35 -0400 Subject: [PATCH 11/13] use bignumber math to avoid nested await --- packages/message-relayer/src/relay-tx.ts | 8 +++----- packages/message-relayer/test/unit-tests/relay-tx.spec.ts | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts index 98eac3dae0a7..18da81a5efa3 100644 --- a/packages/message-relayer/src/relay-tx.ts +++ b/packages/message-relayer/src/relay-tx.ts @@ -159,15 +159,13 @@ export const getStateBatchAppendedEventByTransactionIndex = async ( return index >= prevTotalElements + batchSize } - const totalBatches = ( - await l1StateCommitmentChain.getTotalBatches() - ).toNumber() - if (totalBatches === 0) { + const totalBatches: ethers.BigNumber = await l1StateCommitmentChain.getTotalBatches() + if (totalBatches.eq(0)) { return null } let lowerBound = 0 - let upperBound = totalBatches - 1 + let upperBound = totalBatches.toNumber() - 1 let batchEvent: ethers.Event | null = await getStateBatchAppendedEventByBatchIndex( upperBound ) diff --git a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts index f4bb68d03f2c..7bc6036de892 100644 --- a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts +++ b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts @@ -346,7 +346,7 @@ describe('relay transaction generation functions', () => { ).to.be.rejected }) - it('should throw an error if the corresponding state batcsenderh has not been submitted', async () => { + it('should throw an error if the corresponding state batch has not been submitted', async () => { const tx = await MockL2CrossDomainMessenger.emitSentMessageEvent({ target: ethers.constants.AddressZero, sender: ethers.constants.AddressZero, From 548e1df81271063082b9eedc0ef03eefeb26655d Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 26 May 2021 18:20:59 -0400 Subject: [PATCH 12/13] use a better interface --- packages/message-relayer/src/relay-tx.ts | 180 +++++++++--------- .../test/unit-tests/relay-tx.spec.ts | 76 ++++---- 2 files changed, 133 insertions(+), 123 deletions(-) diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts index 18da81a5efa3..a19eebdc0cde 100644 --- a/packages/message-relayer/src/relay-tx.ts +++ b/packages/message-relayer/src/relay-tx.ts @@ -36,23 +36,39 @@ interface CrossDomainMessage { messageNonce: number } +interface CrossDomainMessageProof { + stateRoot: string + stateRootBatchHeader: StateRootBatchHeader + stateRootProof: { + index: number + siblings: string[] + } + stateTrieWitness: string + storageTrieWitness: string +} + +interface CrossDomainMessagePair { + message: CrossDomainMessage + proof: CrossDomainMessageProof +} + interface StateTrieProof { accountProof: string storageProof: string } /** - * Finds the L2 => L1 message triggered by a given L2 transaction, if the message exists. + * Finds all L2 => L1 messages triggered by a given L2 transaction, if the message exists. * @param l2RpcProvider L2 RPC provider. * @param l2CrossDomainMessengerAddress Address of the L2CrossDomainMessenger. * @param l2TransactionHash Hash of the L2 transaction to find a message for. - * @returns Message assocaited with the transaction or null if no such message exists. + * @returns Messages associated with the transaction. */ -export const getMessageByTransactionHash = async ( +export const getMessagesByTransactionHash = async ( l2RpcProvider: ethers.providers.JsonRpcProvider, l2CrossDomainMessengerAddress: string, l2TransactionHash: string -): Promise => { +): Promise => { // Complain if we can't find the given transaction. const transaction = await l2RpcProvider.getTransaction(l2TransactionHash) if (transaction === null) { @@ -73,32 +89,23 @@ export const getMessageByTransactionHash = async ( transaction.blockNumber ) - // If there were no SentMessage events then this transaction did not send any L2 => L1 messaages. - if (sentMessageEvents.length === 0) { - return null - } - - // Not sure exactly how to handle the special case that more than one message was sent in the - // same transaction. For now throwing an error seems fine. We can always deal with this later. - if (sentMessageEvents.length > 1) { - throw new Error( - `can currently only support one message per transaction, found ${sentMessageEvents}` + // Decode the messages and turn them into a nicer struct. + const sentMessages = sentMessageEvents.map((sentMessageEvent) => { + const encodedMessage = sentMessageEvent.args.message + const decodedMessage = l2CrossDomainMessenger.interface.decodeFunctionData( + 'relayMessage', + encodedMessage ) - } - // Decode the message and turn it into a nicer struct. - const encodedMessage = sentMessageEvents[0].args.message - const decodedMessage = l2CrossDomainMessenger.interface.decodeFunctionData( - 'relayMessage', - encodedMessage - ) + return { + target: decodedMessage._target, + sender: decodedMessage._sender, + message: decodedMessage._message, + messageNonce: decodedMessage._messageNonce.toNumber(), + } + }) - return { - target: decodedMessage._target, - sender: decodedMessage._sender, - message: decodedMessage._message, - messageNonce: decodedMessage._messageNonce.toNumber(), - } + return sentMessages } /** @@ -306,55 +313,29 @@ const getStateTrieProof = async ( } /** - * Generates the transaction data to send to the L1CrossDomainMessenger in order to execute an - * L2 => L1 message. + * Finds all L2 => L1 messages sent in a given L2 transaction and generates proofs for each of + * those messages. * @param l1RpcProvider L1 RPC provider. * @param l2RpcProvider L2 RPC provider. * @param l1StateCommitmentChainAddress Address of the StateCommitmentChain. * @param l2CrossDomainMessengerAddress Address of the L2CrossDomainMessenger. * @param l2TransactionHash L2 transaction hash to generate a relay transaction for. - * @returns 0x-prefixed transaction data as a hex string. + * @returns An array of messages sent in the transaction and a proof of inclusion for each. */ -export const makeRelayTransactionData = async ( +export const getMessagesAndProofsForL2Transaction = async ( l1RpcProvider: ethers.providers.JsonRpcProvider, l2RpcProvider: ethers.providers.JsonRpcProvider, l1StateCommitmentChainAddress: string, l2CrossDomainMessengerAddress: string, l2TransactionHash: string -): Promise => { - // Step 1: Find the transaction. +): Promise => { const l2Transaction = await l2RpcProvider.getTransaction(l2TransactionHash) if (l2Transaction === null) { throw new Error(`unable to find tx with hash: ${l2TransactionHash}`) } - // Step 2: Find the message associated with the transaction. - const message = await getMessageByTransactionHash( - l2RpcProvider, - l2CrossDomainMessengerAddress, - l2TransactionHash - ) - if (message === null) { - throw new Error( - `unable to find a message to relay in tx with hash: ${l2TransactionHash}` - ) - } - - // Step 3: Generate a state trie proof for the slot where the message is located. - const messageSlot = ethers.utils.keccak256( - ethers.utils.keccak256( - encodeCrossDomainMessage(message) + - remove0x(l2CrossDomainMessengerAddress) - ) + '00'.repeat(32) - ) - const stateTrieProof = await getStateTrieProof( - l2RpcProvider, - l2Transaction.blockNumber + NUM_L2_GENESIS_BLOCKS, - predeploys.OVM_L2ToL1MessagePasser, - messageSlot - ) - - // Step 4: Find the full batch associated with the transaction. + // Need to find the state batch for the given transaction. If no state batch has been published + // yet then we will not be able to generate a proof. const batch = await getStateRootBatchByTransactionIndex( l1RpcProvider, l1StateCommitmentChainAddress, @@ -366,34 +347,61 @@ export const makeRelayTransactionData = async ( ) } - // Step 5: Generate a Merkle proof for the state root associated with the transaction inside of - // the Merkle tree of state roots published as a batch. - const txIndexInBatch = - l2Transaction.blockNumber - batch.header.prevTotalElements.toNumber() - const stateRootMerkleProof = getMerkleTreeProof( - batch.stateRoots, - txIndexInBatch + const messages = await getMessagesByTransactionHash( + l2RpcProvider, + l2CrossDomainMessengerAddress, + l2TransactionHash ) - // Step 6: Generate the transaction data. - const relayTransactionData = getContractInterface( - 'OVM_L1CrossDomainMessenger' - ).encodeFunctionData('relayMessage', [ - message.target, - message.sender, - message.message, - message.messageNonce, - { - stateRoot: batch.stateRoots[txIndexInBatch], - stateRootBatchHeader: batch.header, - stateRootProof: { - index: txIndexInBatch, - siblings: stateRootMerkleProof, + const messagePairs: CrossDomainMessagePair[] = [] + for (const message of messages) { + // We need to calculate the specific storage slot that demonstrates that this message was + // actually included in the L2 chain. The following calculation is based on the fact that + // messages are stored in the following mapping on L2: + // https://github.com/ethereum-optimism/optimism/blob/c84d3450225306abbb39b4e7d6d82424341df2be/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_L2ToL1MessagePasser.sol#L23 + // You can read more about how Solidity storage slots are computed for mappings here: + // https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays + const messageSlot = ethers.utils.keccak256( + ethers.utils.keccak256( + encodeCrossDomainMessage(message) + + remove0x(l2CrossDomainMessengerAddress) + ) + '00'.repeat(32) + ) + + // We need a Merkle trie proof for the given storage slot. This allows us to prove to L1 that + // the message was actually sent on L2. + const stateTrieProof = await getStateTrieProof( + l2RpcProvider, + l2Transaction.blockNumber + NUM_L2_GENESIS_BLOCKS, + predeploys.OVM_L2ToL1MessagePasser, + messageSlot + ) + + // State roots are published in batches to L1 and correspond 1:1 to transactions. We compute a + // Merkle root for these state roots so that we only need to store the minimum amount of + // information on-chain. So we need to create a Merkle proof for the specific state root that + // corresponds to this transaction. + const txIndexInBatch = + l2Transaction.blockNumber - batch.header.prevTotalElements.toNumber() + const stateRootMerkleProof = getMerkleTreeProof( + batch.stateRoots, + txIndexInBatch + ) + + messagePairs.push({ + message, + proof: { + stateRoot: batch.stateRoots[txIndexInBatch], + stateRootBatchHeader: batch.header, + stateRootProof: { + index: txIndexInBatch, + siblings: stateRootMerkleProof, + }, + stateTrieWitness: stateTrieProof.accountProof, + storageTrieWitness: stateTrieProof.storageProof, }, - stateTrieWitness: stateTrieProof.accountProof, - storageTrieWitness: stateTrieProof.storageProof, - }, - ]) + }) + } - return relayTransactionData + return messagePairs } diff --git a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts index 7bc6036de892..1745a620f5f2 100644 --- a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts +++ b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts @@ -9,10 +9,10 @@ import { toPlainObject } from 'lodash' /* Imports: Internal */ import { - makeRelayTransactionData, + getMessagesAndProofsForL2Transaction, getStateRootBatchByTransactionIndex, getStateBatchAppendedEventByTransactionIndex, - getMessageByTransactionHash, + getMessagesByTransactionHash, } from '../../src/relay-tx' describe('relay transaction generation functions', () => { @@ -78,7 +78,7 @@ describe('relay transaction generation functions', () => { describe('getMessageByTransactionHash', () => { it('should throw an error if a transaction with the given hash does not exist', async () => { await expect( - getMessageByTransactionHash( + getMessagesByTransactionHash( l2RpcProvider, MockL2CrossDomainMessenger.address, ethers.constants.HashZero @@ -90,39 +90,12 @@ describe('relay transaction generation functions', () => { const tx = await MockL2CrossDomainMessenger.doNothing() expect( - await getMessageByTransactionHash( + await getMessagesByTransactionHash( l2RpcProvider, MockL2CrossDomainMessenger.address, tx.hash ) - ).to.equal(null) - }) - - it('should throw an error if the transaction emitted more than one SentMessage event', async () => { - const tx = await MockL2CrossDomainMessenger.emitMultipleSentMessageEvents( - [ - { - target: ethers.constants.AddressZero, - sender: ethers.constants.AddressZero, - message: '0x', - messageNonce: 0, - }, - { - target: ethers.constants.AddressZero, - sender: ethers.constants.AddressZero, - message: '0x', - messageNonce: 1, - }, - ] - ) - - await expect( - getMessageByTransactionHash( - l2RpcProvider, - MockL2CrossDomainMessenger.address, - tx.hash - ) - ).to.be.rejected + ).to.deep.equal([]) }) it('should return the parsed event if the transaction emitted exactly one SentMessage event', async () => { @@ -135,12 +108,41 @@ describe('relay transaction generation functions', () => { const tx = await MockL2CrossDomainMessenger.emitSentMessageEvent(message) expect( - await getMessageByTransactionHash( + await getMessagesByTransactionHash( + l2RpcProvider, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.deep.equal([message]) + }) + + it('should return the parsed events if the transaction emitted more than one SentMessage event', async () => { + const messages = [ + { + target: ethers.constants.AddressZero, + sender: ethers.constants.AddressZero, + message: '0x', + messageNonce: 0, + }, + { + target: ethers.constants.AddressZero, + sender: ethers.constants.AddressZero, + message: '0x', + messageNonce: 1, + }, + ] + + const tx = await MockL2CrossDomainMessenger.emitMultipleSentMessageEvents( + messages + ) + + expect( + await getMessagesByTransactionHash( l2RpcProvider, MockL2CrossDomainMessenger.address, tx.hash ) - ).to.deep.equal(message) + ).to.deep.equal(messages) }) }) @@ -322,7 +324,7 @@ describe('relay transaction generation functions', () => { describe('makeRelayTransactionData', () => { it('should throw an error if the transaction does not exist', async () => { await expect( - makeRelayTransactionData( + getMessagesAndProofsForL2Transaction( l1RpcProvider, l2RpcProvider, StateCommitmentChain.address, @@ -336,7 +338,7 @@ describe('relay transaction generation functions', () => { const tx = await MockL2CrossDomainMessenger.doNothing() await expect( - makeRelayTransactionData( + getMessagesAndProofsForL2Transaction( l1RpcProvider, l2RpcProvider, StateCommitmentChain.address, @@ -355,7 +357,7 @@ describe('relay transaction generation functions', () => { }) await expect( - makeRelayTransactionData( + getMessagesAndProofsForL2Transaction( l1RpcProvider, l2RpcProvider, StateCommitmentChain.address, From ad51b4bba0218e396efe7931401ceb65be81502a Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 28 May 2021 20:19:39 -0400 Subject: [PATCH 13/13] minor fixes and simplifications --- packages/message-relayer/src/relay-tx.ts | 37 +++++++++++++++--------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts index a19eebdc0cde..547006ed7735 100644 --- a/packages/message-relayer/src/relay-tx.ts +++ b/packages/message-relayer/src/relay-tx.ts @@ -347,6 +347,16 @@ export const getMessagesAndProofsForL2Transaction = async ( ) } + // Adjust the transaction index based on the number of L2 genesis block we have. "Index" here + // refers to the position of the transaction within the *Canonical Transaction Chain*. + const l2TransactionIndex = l2Transaction.blockNumber - NUM_L2_GENESIS_BLOCKS + + // Here the index refers to the position of the state root that corresponds to this transaction + // within the batch of state roots in which that state root was published. + const txIndexInBatch = + l2TransactionIndex - batch.header.prevTotalElements.toNumber() + + // Find every message that was sent during this transaction. We'll then attach a proof for each. const messages = await getMessagesByTransactionHash( l2RpcProvider, l2CrossDomainMessengerAddress, @@ -372,7 +382,7 @@ export const getMessagesAndProofsForL2Transaction = async ( // the message was actually sent on L2. const stateTrieProof = await getStateTrieProof( l2RpcProvider, - l2Transaction.blockNumber + NUM_L2_GENESIS_BLOCKS, + l2Transaction.blockNumber, predeploys.OVM_L2ToL1MessagePasser, messageSlot ) @@ -381,25 +391,26 @@ export const getMessagesAndProofsForL2Transaction = async ( // Merkle root for these state roots so that we only need to store the minimum amount of // information on-chain. So we need to create a Merkle proof for the specific state root that // corresponds to this transaction. - const txIndexInBatch = - l2Transaction.blockNumber - batch.header.prevTotalElements.toNumber() const stateRootMerkleProof = getMerkleTreeProof( batch.stateRoots, txIndexInBatch ) + // We now have enough information to create the message proof. + const proof: CrossDomainMessageProof = { + stateRoot: batch.stateRoots[txIndexInBatch], + stateRootBatchHeader: batch.header, + stateRootProof: { + index: txIndexInBatch, + siblings: stateRootMerkleProof, + }, + stateTrieWitness: stateTrieProof.accountProof, + storageTrieWitness: stateTrieProof.storageProof, + } + messagePairs.push({ message, - proof: { - stateRoot: batch.stateRoots[txIndexInBatch], - stateRootBatchHeader: batch.header, - stateRootProof: { - index: txIndexInBatch, - siblings: stateRootMerkleProof, - }, - stateTrieWitness: stateTrieProof.accountProof, - storageTrieWitness: stateTrieProof.storageProof, - }, + proof, }) }