Commit e4103622 authored by smartcontracts's avatar smartcontracts Committed by GitHub

Merge branch 'develop' into regenesis/0.4.0

parents c4c7beaa 5e3c5d1c
---
'@eth-optimism/smock': patch
---
Fixes a bug that would break call assertions for overloaded smocked functions
---
'@eth-optimism/l2geth': patch
'@eth-optimism/data-transport-layer': patch
---
Fix gasLimit overflow
---
'@eth-optimism/message-relayer': patch
---
Adds a new set of tools for generating messages to be relayed and their proofs
......@@ -69,7 +69,7 @@ type transaction struct {
BlockNumber uint64 `json:"blockNumber"`
Timestamp uint64 `json:"timestamp"`
Value hexutil.Uint64 `json:"value"`
GasLimit uint64 `json:"gasLimit"`
GasLimit uint64 `json:"gasLimit,string"`
Target common.Address `json:"target"`
Origin *common.Address `json:"origin"`
Data hexutil.Bytes `json:"data"`
......@@ -83,7 +83,7 @@ type Enqueue struct {
Index *uint64 `json:"ctcIndex"`
Target *common.Address `json:"target"`
Data *hexutil.Bytes `json:"data"`
GasLimit *uint64 `json:"gasLimit"`
GasLimit *uint64 `json:"gasLimit,string"`
Origin *common.Address `json:"origin"`
BlockNumber *uint64 `json:"blockNumber"`
Timestamp *uint64 `json:"timestamp"`
......
......@@ -379,8 +379,9 @@ export class TransportDB {
if (index === null) {
return null
}
return this.db.get<TEntry>(`${key}:index`, index)
let entry = await this.db.get<TEntry>(`${key}:index`, index)
entry = stringify(entry)
return entry
}
private async _getEntries<TEntry extends Indexed>(
......@@ -388,6 +389,28 @@ export class TransportDB {
startIndex: number,
endIndex: number
): Promise<TEntry[] | []> {
return this.db.range<TEntry>(`${key}:index`, startIndex, endIndex)
const entries = await this.db.range<TEntry>(
`${key}:index`,
startIndex,
endIndex
)
const results = []
for (const entry of entries) {
results.push(stringify(entry))
}
return results
}
}
function stringify(entry) {
if (entry === null || entry === undefined) {
return entry
}
if (entry.gasLimit) {
entry.gasLimit = BigNumber.from(entry.gasLimit).toString()
}
if (entry.decoded) {
entry.decoded.gasLimit = BigNumber.from(entry.decoded.gasLimit).toString()
}
return entry
}
......@@ -69,7 +69,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet<
submitter: l1Transaction.from,
l1TransactionHash: l1Transaction.hash,
l1TransactionData: l1Transaction.data,
gasLimit: SEQUENCER_GAS_LIMIT,
gasLimit: `${SEQUENCER_GAS_LIMIT}`,
prevTotalElements: batchSubmissionEvent.args._prevTotalElements,
batchIndex: batchSubmissionEvent.args._batchIndex,
......@@ -115,7 +115,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet<
batchIndex: extraData.batchIndex.toNumber(),
blockNumber: BigNumber.from(context.blockNumber).toNumber(),
timestamp: BigNumber.from(context.timestamp).toNumber(),
gasLimit: BigNumber.from(extraData.gasLimit).toNumber(),
gasLimit: BigNumber.from(extraData.gasLimit).toString(),
target: SEQUENCER_ENTRYPOINT_ADDRESS,
origin: null,
data: toHexString(sequencerTransaction),
......@@ -147,7 +147,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet<
batchIndex: extraData.batchIndex.toNumber(),
blockNumber: BigNumber.from(0).toNumber(),
timestamp: BigNumber.from(0).toNumber(),
gasLimit: BigNumber.from(0).toNumber(),
gasLimit: BigNumber.from(0).toString(),
target: constants.AddressZero,
origin: constants.AddressZero,
data: '0x',
......
......@@ -17,7 +17,7 @@ export const handleEventsTransactionEnqueued: EventHandlerSet<
index: event.args._queueIndex.toNumber(),
target: event.args._target,
data: event.args._data,
gasLimit: event.args._gasLimit.toNumber(),
gasLimit: event.args._gasLimit.toString(),
origin: event.args._l1TxOrigin,
blockNumber: BigNumber.from(event.blockNumber).toNumber(),
timestamp: event.args._timestamp.toNumber(),
......
......@@ -57,7 +57,7 @@ export const handleSequencerBlock = {
transactionEntry = {
...transactionEntry,
gasLimit: SEQUENCER_GAS_LIMIT, // ?
gasLimit: `${SEQUENCER_GAS_LIMIT}`, // ?
target: SEQUENCER_ENTRYPOINT_ADDRESS,
origin: null,
data: serialize(
......@@ -82,7 +82,7 @@ export const handleSequencerBlock = {
} else {
transactionEntry = {
...transactionEntry,
gasLimit: BigNumber.from(transaction.gas).toNumber(),
gasLimit: BigNumber.from(transaction.gas).toString(),
target: ethers.utils.getAddress(transaction.to),
origin: ethers.utils.getAddress(transaction.l1TxOrigin),
data: transaction.input,
......
......@@ -16,7 +16,7 @@ export interface EnqueueEntry {
index: number
target: string
data: string
gasLimit: number
gasLimit: string
origin: string
blockNumber: number
timestamp: number
......@@ -28,7 +28,7 @@ export interface TransactionEntry {
data: string
blockNumber: number
timestamp: number
gasLimit: number
gasLimit: string
target: string
origin: string
value: string
......
......@@ -40,7 +40,7 @@ export interface SequencerBatchAppendedExtraData {
submitter: string
l1TransactionData: string
l1TransactionHash: string
gasLimit: number
gasLimit: string
// Stuff from TransactionBatchAppended.
prevTotalElements: BigNumber
......
......@@ -18,7 +18,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.SequencerBatchAppended',
submitter: '0xfd7d4de366850c08ee2cba32d851385a3071ec8d',
l1TransactionHash:
'0x6effe006836b841205ace4d99d7ae1b74ee96aac499a3f358b97fccd32ee9af2',
gasLimit: 548976,
gasLimit: '548976',
prevTotalElements: BigNumber.from(73677),
batchIndex: BigNumber.from(743),
batchSize: BigNumber.from(101),
......
......@@ -93,7 +93,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', ()
}
})
it('should have a gasLimit equal to the integer value of the _gasLimit argument', () => {
it('should have a gasLimit equal to the string value of the _gasLimit argument', () => {
for (
let i = 0;
i < Number.MAX_SAFE_INTEGER;
......@@ -113,7 +113,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', ()
const output1 = handleEventsTransactionEnqueued.parseEvent(...input1)
const expected1 = BigNumber.from(i).toNumber()
const expected1 = BigNumber.from(i).toString()
expect(output1).to.have.property('gasLimit', expected1)
}
......
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?
})
})
})
......@@ -79,18 +79,25 @@ const smockifyFunction = (
let data: any = toHexString(calldataBuf)
try {
data = contract.interface.decodeFunctionData(fragment.name, data)
data = contract.interface.decodeFunctionData(
fragment.format(),
data
)
} catch (e) {
console.error(e)
}
return {
functionName: fragment.name,
functionSignature: fragment.format(),
data,
}
})
.filter((functionResult: any) => {
return functionResult.functionName === functionName
return (
functionResult.functionName === functionName ||
functionResult.functionSignature === functionName
)
})
.map((functionResult: any) => {
return functionResult.data
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract TestHelpers_MockCaller {
function callMock(address _target, bytes memory _data) public {
_target.call(_data);
}
}
/* Imports: External */
import hre from 'hardhat'
import { expect } from 'chai'
import { Contract } from 'ethers'
/* Imports: Internal */
import { MockContract, smockit } from '../../src'
describe('[smock]: call assertion tests', () => {
const ethers = (hre as any).ethers
let mock: MockContract
beforeEach(async () => {
mock = await smockit('TestHelpers_BasicReturnContract')
})
let mockCaller: Contract
before(async () => {
const mockCallerFactory = await ethers.getContractFactory(
'TestHelpers_MockCaller'
)
mockCaller = await mockCallerFactory.deploy()
})
describe('call assertions for functions', () => {
it('should be able to make assertions about a non-overloaded function', async () => {
mock.smocked.getInputtedUint256.will.return.with(0)
const expected1 = ethers.BigNumber.from(1234)
await mockCaller.callMock(
mock.address,
mock.interface.encodeFunctionData('getInputtedUint256(uint256)', [
expected1,
])
)
expect(mock.smocked.getInputtedUint256.calls[0]).to.deep.equal([
expected1,
])
})
it('should be able to make assertions about both versions of an overloaded function', async () => {
mock.smocked['overloadedFunction(uint256)'].will.return.with(0)
mock.smocked['overloadedFunction(uint256,uint256)'].will.return.with(0)
const expected1 = ethers.BigNumber.from(1234)
await mockCaller.callMock(
mock.address,
mock.interface.encodeFunctionData('overloadedFunction(uint256)', [
expected1,
])
)
expect(
mock.smocked['overloadedFunction(uint256)'].calls[0]
).to.deep.equal([expected1])
const expected2 = ethers.BigNumber.from(5678)
await mockCaller.callMock(
mock.address,
mock.interface.encodeFunctionData(
'overloadedFunction(uint256,uint256)',
[expected2, expected2]
)
)
expect(
mock.smocked['overloadedFunction(uint256,uint256)'].calls[0]
).to.deep.equal([expected2, expected2])
})
})
})
......@@ -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