Commit 86708bb5 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat[message-relayer]: relay tx generator (#952)

* feat[message-relayer]: relay tx generator

* whoops, I burned our infura key

* fix minor bug

* add comments

* add more comments and clean stuff up

* add empty test descriptions

* add tests

* move smock to dev deps

* chore: add changeset

* minor cleanup to merkle tree proof function

* use bignumber math to avoid nested await

* use a better interface

* minor fixes and simplifications
parent 5a798497
---
'@eth-optimism/message-relayer': patch
---
Adds a new set of tools for generating messages to be relayed and their proofs
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
......@@ -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.3",
"@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,18 @@
"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",
"@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",
"hardhat": "^2.3.0",
"lodash": "^4.17.21",
"mocha": "^8.4.0",
"prettier": "^2.2.1",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
......
export * from './relay-tx'
/* Imports: External */
import { ethers } from 'ethers'
import {
fromHexString,
remove0x,
toHexString,
toRpcHexString,
} from '@eth-optimism/core-utils'
import { getContractInterface, predeploys } 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 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 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 Messages associated with the transaction.
*/
export const getMessagesByTransactionHash = async (
l2RpcProvider: ethers.providers.JsonRpcProvider,
l2CrossDomainMessengerAddress: string,
l2TransactionHash: string
): Promise<CrossDomainMessage[]> => {
// 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}`)
}
const l2CrossDomainMessenger = new ethers.Contract(
l2CrossDomainMessengerAddress,
getContractInterface('OVM_L2CrossDomainMessenger'),
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
)
// 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
)
return {
target: decodedMessage._target,
sender: decodedMessage._sender,
message: decodedMessage._message,
messageNonce: decodedMessage._messageNonce.toNumber(),
}
})
return sentMessages
}
/**
* Encodes a cross domain message.
* @param message Message to encode.
* @returns Encoded message.
*/
const encodeCrossDomainMessage = (message: CrossDomainMessage): string => {
return getContractInterface(
'OVM_L2CrossDomainMessenger'
).encodeFunctionData('relayMessage', [
message.target,
message.sender,
message.message,
message.messageNonce,
])
}
/**
* 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,
l2TransactionIndex: number
): Promise<ethers.Event | null> => {
const l1StateCommitmentChain = new ethers.Contract(
l1StateCommitmentChainAddress,
getContractInterface('OVM_StateCommitmentChain'),
l1RpcProvider
)
const getStateBatchAppendedEventByBatchIndex = async (
index: number
): Promise<ethers.Event | null> => {
const eventQueryResult = await l1StateCommitmentChain.queryFilter(
l1StateCommitmentChain.filters.StateBatchAppended(index)
)
if (eventQueryResult.length === 0) {
return null
} else {
return eventQueryResult[0]
}
}
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 totalBatches: ethers.BigNumber = await l1StateCommitmentChain.getTotalBatches()
if (totalBatches.eq(0)) {
return null
}
let lowerBound = 0
let upperBound = totalBatches.toNumber() - 1
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)
if (isEventHi(batchEvent, l2TransactionIndex)) {
upperBound = middleOfBounds
} else if (isEventLo(batchEvent, l2TransactionIndex)) {
lowerBound = middleOfBounds
} else {
break
}
}
return batchEvent
}
/**
* 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
): Promise<StateRootBatch | null> => {
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,
}
}
/**
* 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[] => {
// 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 < correctedTreeSize; i++) {
if (i < leaves.length) {
parsedLeaves.push(leaves[i])
} else {
parsedLeaves.push(ethers.utils.keccak256('0x' + '00'.repeat(32)))
}
}
// merkletreejs prefers things to be Buffers.
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
}
/**
* 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,
address: string,
slot: string
): Promise<StateTrieProof> => {
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)),
}
}
/**
* 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 An array of messages sent in the transaction and a proof of inclusion for each.
*/
export const getMessagesAndProofsForL2Transaction = async (
l1RpcProvider: ethers.providers.JsonRpcProvider,
l2RpcProvider: ethers.providers.JsonRpcProvider,
l1StateCommitmentChainAddress: string,
l2CrossDomainMessengerAddress: string,
l2TransactionHash: string
): Promise<CrossDomainMessagePair[]> => {
const l2Transaction = await l2RpcProvider.getTransaction(l2TransactionHash)
if (l2Transaction === null) {
throw new Error(`unable to find tx with hash: ${l2TransactionHash}`)
}
// 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,
l2Transaction.blockNumber - NUM_L2_GENESIS_BLOCKS
)
if (batch === null) {
throw new Error(
`unable to find state root batch for tx with hash: ${l2TransactionHash}`
)
}
// 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,
l2TransactionHash
)
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,
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 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,
})
}
return messagePairs
}
/* 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 }
// 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 {}
}
import { expect } from '../setup'
/* Imports: External */
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 {
getMessagesAndProofsForL2Transaction,
getStateRootBatchByTransactionIndex,
getStateBatchAppendedEventByTransactionIndex,
getMessagesByTransactionHash,
} 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 () => {
await expect(
getMessagesByTransactionHash(
l2RpcProvider,
MockL2CrossDomainMessenger.address,
ethers.constants.HashZero
)
).to.be.rejected
})
it('should return null if the transaction did not emit a SentMessage event', async () => {
const tx = await MockL2CrossDomainMessenger.doNothing()
expect(
await getMessagesByTransactionHash(
l2RpcProvider,
MockL2CrossDomainMessenger.address,
tx.hash
)
).to.deep.equal([])
})
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 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(messages)
})
})
describe('getStateBatchAppendedEventByTransactionIndex', () => {
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
)
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 first 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 first.
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,
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),
})
})
}
})
}
})
describe('getStateRootBatchByTransactionIndex', () => {
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
)
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 () => {
await expect(
getMessagesAndProofsForL2Transaction(
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()
await expect(
getMessagesAndProofsForL2Transaction(
l1RpcProvider,
l2RpcProvider,
StateCommitmentChain.address,
MockL2CrossDomainMessenger.address,
tx.hash
)
).to.be.rejected
})
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,
message: '0x',
messageNonce: 0,
})
await expect(
getMessagesAndProofsForL2Transaction(
l1RpcProvider,
l2RpcProvider,
StateCommitmentChain.address,
MockL2CrossDomainMessenger.address,
tx.hash
)
).to.be.rejected
})
// 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?
})
})
})
......@@ -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"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment