Commit 7503cc36 authored by Georgios Konstantopoulos's avatar Georgios Konstantopoulos Committed by GitHub

More integration tests (#38)

* refactor: move utils.ts to specific file

* refactor: introduce watcher utils for x-domain monitoring

* test: add rpc tests

* fix: use injected l2 context and remove the unused ethsign methods
parent 94baf79e
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
"@ethersproject/providers": "^5.0.24", "@ethersproject/providers": "^5.0.24",
"@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-ethers": "^2.0.2",
"chai": "^4.3.3", "chai": "^4.3.3",
"chai-as-promised": "^7.1.1",
"ethereum-waffle": "^3.3.0", "ethereum-waffle": "^3.3.0",
"ethers": "^5.0.32", "ethers": "^5.0.32",
"hardhat": "^2.1.2", "hardhat": "^2.1.2",
......
...@@ -3,6 +3,11 @@ import { expect } from 'chai' ...@@ -3,6 +3,11 @@ import { expect } from 'chai'
/* Imports: External */ /* Imports: External */
import { Contract, ContractFactory, Wallet, providers } from 'ethers' import { Contract, ContractFactory, Wallet, providers } from 'ethers'
import { Watcher } from '@eth-optimism/core-utils' import { Watcher } from '@eth-optimism/core-utils'
import {
initWatcher,
waitForXDomainTransaction,
Direction,
} from './shared/watcher-utils'
import { getContractFactory } from '@eth-optimism/contracts' import { getContractFactory } from '@eth-optimism/contracts'
/* Imports: Internal */ /* Imports: Internal */
...@@ -53,31 +58,13 @@ describe('Basic L1<>L2 Communication', async () => { ...@@ -53,31 +58,13 @@ describe('Basic L1<>L2 Communication', async () => {
l2Wallet l2Wallet
) )
const l1MessengerAddress = await AddressManager.getAddress( watcher = await initWatcher(l1Provider, l2Provider, AddressManager)
'Proxy__OVM_L1CrossDomainMessenger'
)
const l2MessengerAddress = await AddressManager.getAddress(
'OVM_L2CrossDomainMessenger'
)
L1CrossDomainMessenger = getContractFactory('iOVM_L1CrossDomainMessenger') L1CrossDomainMessenger = getContractFactory('iOVM_L1CrossDomainMessenger')
.connect(l1Wallet) .connect(l1Wallet)
.attach(l1MessengerAddress) .attach(watcher.l1.messengerAddress)
L2CrossDomainMessenger = getContractFactory('iOVM_L2CrossDomainMessenger') L2CrossDomainMessenger = getContractFactory('iOVM_L2CrossDomainMessenger')
.connect(l2Wallet) .connect(l2Wallet)
.attach(l2MessengerAddress) .attach(watcher.l2.messengerAddress)
watcher = new Watcher({
l1: {
provider: l1Provider,
messengerAddress: L1CrossDomainMessenger.address,
},
l2: {
provider: l2Provider,
messengerAddress: L2CrossDomainMessenger.address,
},
})
}) })
beforeEach(async () => { beforeEach(async () => {
...@@ -98,14 +85,7 @@ describe('Basic L1<>L2 Communication', async () => { ...@@ -98,14 +85,7 @@ describe('Basic L1<>L2 Communication', async () => {
{ gasLimit: 7000000 } { gasLimit: 7000000 }
) )
// Wait for the L2 transaction to be mined. await waitForXDomainTransaction(watcher, transaction, Direction.L2ToL1)
await transaction.wait()
// Wait for the transaction to be relayed on L1.
const messageHashes = await watcher.getMessageHashesFromL2Tx(
transaction.hash
)
await watcher.getL1TransactionReceipt(messageHashes[0])
expect(await L1SimpleStorage.msgSender()).to.equal( expect(await L1SimpleStorage.msgSender()).to.equal(
L1CrossDomainMessenger.address L1CrossDomainMessenger.address
...@@ -126,14 +106,7 @@ describe('Basic L1<>L2 Communication', async () => { ...@@ -126,14 +106,7 @@ describe('Basic L1<>L2 Communication', async () => {
{ gasLimit: 7000000 } { gasLimit: 7000000 }
) )
// Wait for the L1 transaction to be mined. await waitForXDomainTransaction(watcher, transaction, Direction.L1ToL2)
await transaction.wait()
// Wait for the transaction to be included on L2.
const messageHashes = await watcher.getMessageHashesFromL1Tx(
transaction.hash
)
await watcher.getL2TransactionReceipt(messageHashes[0])
expect(await L2SimpleStorage.msgSender()).to.equal( expect(await L2SimpleStorage.msgSender()).to.equal(
L2CrossDomainMessenger.address L2CrossDomainMessenger.address
......
/* Imports: Internal */ /* Imports: Internal */
import { getContractFactory } from '@eth-optimism/contracts' import { getContractFactory } from '@eth-optimism/contracts'
import { injectL2Context } from './utils' import { injectL2Context } from './shared/l2provider'
/* Imports: External */ /* Imports: External */
import { Contract, Signer, Wallet, providers } from 'ethers' import { Contract, Signer, Wallet, providers } from 'ethers'
import { expect } from 'chai' import { expect } from 'chai'
import { sleep } from './shared/utils'
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// This test ensures that the transactions which get `enqueue`d get // This test ensures that the transactions which get `enqueue`d get
// added to the L2 blocks by the Sync Service (which queries the DTL) // added to the L2 blocks by the Sync Service (which queries the DTL)
......
import { ethers } from 'hardhat'
import { Wallet, BigNumber } from 'ethers'
import chai, { expect } from 'chai'
import { sleep } from './shared/utils'
import { injectL2Context } from './shared/l2provider'
import chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised)
describe('Basic RPC tests', () => {
const DEFAULT_TRANSACTION = {
to: '0x' + '1234'.repeat(10),
gasLimit: 4000000,
gasPrice: 0,
data: '0x',
value: 0,
}
const provider = injectL2Context(ethers.provider)
const wallet = Wallet.createRandom().connect(provider)
describe('eth_sendRawTransaction', () => {
it('should correctly process a valid transaction', async () => {
const tx = DEFAULT_TRANSACTION
const nonce = await wallet.getTransactionCount()
const result = await wallet.sendTransaction(tx)
expect(result.from).to.equal(wallet.address)
expect(result.nonce).to.equal(nonce)
expect(result.gasLimit.toNumber()).to.equal(tx.gasLimit)
expect(result.gasPrice.toNumber()).to.equal(tx.gasPrice)
expect(result.data).to.equal(tx.data)
})
it('should not accept a transaction with the wrong chain ID', async () => {
const tx = {
...DEFAULT_TRANSACTION,
chainId: (await wallet.getChainId()) + 1,
}
await expect(
provider.sendTransaction(await wallet.signTransaction(tx))
).to.be.rejectedWith('invalid transaction: invalid sender')
})
it('should not accept a transaction without a chain ID', async () => {
const tx = {
...DEFAULT_TRANSACTION,
chainId: null, // Disables EIP155 transaction signing.
}
await expect(
provider.sendTransaction(await wallet.signTransaction(tx))
).to.be.rejectedWith('Cannot submit unprotected transaction')
})
})
describe('eth_getTransactionByHash', () => {
it('should be able to get all relevant l1/l2 transaction data', async () => {
const tx = DEFAULT_TRANSACTION
const result = await wallet.sendTransaction(tx)
await result.wait()
const transaction = (await provider.getTransaction(result.hash)) as any
expect(transaction.txType).to.equal('EIP155')
expect(transaction.queueOrigin).to.equal('sequencer')
expect(transaction.transactionIndex).to.be.eq(0)
expect(transaction.gasLimit).to.be.deep.eq(BigNumber.from(tx.gasLimit))
})
})
describe('eth_getBlockByHash', () => {
it('should return the block and all included transactions', async () => {
// Send a transaction and wait for it to be mined.
const tx = DEFAULT_TRANSACTION
const result = await wallet.sendTransaction(tx)
const receipt = await result.wait()
const block = (await provider.getBlockWithTransactions(
receipt.blockHash
)) as any
expect(block.number).to.not.equal(0)
expect(typeof block.stateRoot).to.equal('string')
expect(block.transactions.length).to.equal(1)
expect(block.transactions[0].txType).to.equal('EIP155')
expect(block.transactions[0].queueOrigin).to.equal('sequencer')
expect(block.transactions[0].l1TxOrigin).to.equal(null)
})
})
describe('eth_getBlockByNumber', () => {
// There was a bug that causes transactions to be reingested over
// and over again only when a single transaction was in the
// canonical transaction chain. This test catches this by
// querying for the latest block and then waits and then queries
// the latest block again and then asserts that they are the same.
it('should return the same result when new transactions are not applied', async () => {
// Get latest block once to start.
const prev = await provider.getBlockWithTransactions('latest')
// Over ten seconds, repeatedly check the latest block to make sure nothing has changed.
for (let i = 0; i < 5; i++) {
const latest = await provider.getBlockWithTransactions('latest')
expect(latest).to.deep.equal(prev)
await sleep(2000)
}
})
})
describe('eth_chainId', () => {
it('should get the correct chainid', async () => {
const { chainId } = await provider.getNetwork()
expect(chainId).to.be.eq(420)
})
})
describe('eth_gasPrice', () => {
it('gas price should be 0', async () => {
const expected = 0
const price = await provider.getGasPrice()
expect(price.toNumber()).to.equal(expected)
})
})
describe('eth_estimateGas', () => {
it('should return a gas estimate', async () => {
// Repeat this test for a series of possible transaction sizes.
for (const size of [0, 2, 8, 64, 256]) {
const estimate = await provider.estimateGas({
...DEFAULT_TRANSACTION,
data: '0x' + '00'.repeat(size),
})
// Ths gas estimation is set to always be the max gas limit - 1.
expect(estimate.toNumber()).to.be.eq(8999999)
}
})
})
})
import { remove0x } from '@eth-optimism/core-utils'
import { JsonRpcProvider } from '@ethersproject/providers' import { JsonRpcProvider } from '@ethersproject/providers'
import cloneDeep from 'lodash/cloneDeep' import cloneDeep from 'lodash/cloneDeep'
import { providers } from 'ethers'
import { utils, providers, Transaction } from 'ethers'
/** /**
* Helper for adding additional L2 context to transactions * Helper for adding additional L2 context to transactions
*/ */
export const injectL2Context = (l1Provider: providers.JsonRpcProvider) => { export const injectL2Context = (l1Provider: providers.JsonRpcProvider) => {
const provider = cloneDeep(l1Provider) const provider = cloneDeep(l1Provider)
const format = provider.formatter.transaction.bind(provider.formatter)
provider.formatter.transaction = (transaction) => {
const tx = format(transaction)
const sig = utils.joinSignature(tx)
const hash = sighashEthSign(tx)
tx.from = utils.verifyMessage(hash, sig)
return tx
}
// Pass through the state root // Pass through the state root
const blockFormat = provider.formatter.block.bind(provider.formatter) const blockFormat = provider.formatter.block.bind(provider.formatter)
...@@ -67,29 +57,3 @@ export const injectL2Context = (l1Provider: providers.JsonRpcProvider) => { ...@@ -67,29 +57,3 @@ export const injectL2Context = (l1Provider: providers.JsonRpcProvider) => {
return provider return provider
} }
function serializeEthSignTransaction(transaction: Transaction): any {
const encoded = utils.defaultAbiCoder.encode(
['uint256', 'uint256', 'uint256', 'uint256', 'address', 'bytes'],
[
transaction.nonce,
transaction.gasLimit,
transaction.gasPrice,
transaction.chainId,
transaction.to,
transaction.data,
]
)
return Buffer.from(encoded.slice(2), 'hex')
}
// Use this function as input to `eth_sign`. It does not
// add the prefix because `eth_sign` does that. It does
// serialize the transaction and hash the serialized
// transaction.
function sighashEthSign(transaction: any): Buffer {
const serialized = serializeEthSignTransaction(transaction)
const hash = remove0x(utils.keccak256(serialized))
return Buffer.from(hash, 'hex')
}
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
import {
JsonRpcProvider,
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/providers'
import { Watcher, Layer } from '@eth-optimism/core-utils'
import { Contract, Transaction } from 'ethers'
export const initWatcher = async (
l1Provider: JsonRpcProvider,
l2Provider: JsonRpcProvider,
AddressManager: Contract
) => {
const l1MessengerAddress = await AddressManager.getAddress(
'Proxy__OVM_L1CrossDomainMessenger'
)
const l2MessengerAddress = await AddressManager.getAddress(
'OVM_L2CrossDomainMessenger'
)
return new Watcher({
l1: {
provider: l1Provider,
messengerAddress: l1MessengerAddress,
},
l2: {
provider: l2Provider,
messengerAddress: l2MessengerAddress,
},
})
}
export interface CrossDomainMessagePair {
tx: Transaction
receipt: TransactionReceipt
remoteTx: Transaction
remoteReceipt: TransactionReceipt
}
export enum Direction {
L1ToL2,
L2ToL1,
}
export const waitForXDomainTransaction = async (
watcher: Watcher,
tx: Promise<TransactionResponse> | TransactionResponse,
direction: Direction
): Promise<CrossDomainMessagePair> => {
const { src, dest } =
direction === Direction.L1ToL2
? { src: watcher.l1, dest: watcher.l2 }
: { src: watcher.l2, dest: watcher.l1 }
// await it if needed
tx = await tx
// get the receipt and the full transaction
const receipt = await tx.wait()
const fullTx = await src.provider.getTransaction(tx.hash)
// get the message hash which was created on the SentMessage
const [xDomainMsgHash] = await watcher.getMessageHashesFromTx(src, tx.hash)
// Get the transaction and receipt on the remote layer
const remoteReceipt = await watcher.getTransactionReceipt(
dest,
xDomainMsgHash
)
const remoteTx = await dest.provider.getTransaction(
remoteReceipt.transactionHash
)
return {
tx: fullTx,
receipt,
remoteTx,
remoteReceipt,
}
}
/* External Imports */ /* External Imports */
import { ethers } from 'ethers' import { ethers } from 'ethers'
import { Provider } from '@ethersproject/abstract-provider' import { Provider, TransactionReceipt } from '@ethersproject/abstract-provider'
export interface Layer { export interface Layer {
provider: Provider provider: Provider
...@@ -23,31 +23,30 @@ export class Watcher { ...@@ -23,31 +23,30 @@ export class Watcher {
} }
public async getMessageHashesFromL1Tx(l1TxHash: string): Promise<string[]> { public async getMessageHashesFromL1Tx(l1TxHash: string): Promise<string[]> {
return this._getMessageHashesFromTx(true, l1TxHash) return this.getMessageHashesFromTx(this.l1, l1TxHash)
} }
public async getMessageHashesFromL2Tx(l2TxHash: string): Promise<string[]> { public async getMessageHashesFromL2Tx(l2TxHash: string): Promise<string[]> {
return this._getMessageHashesFromTx(false, l2TxHash) return this.getMessageHashesFromTx(this.l2, l2TxHash)
} }
public async getL1TransactionReceipt( public async getL1TransactionReceipt(
l2ToL1MsgHash: string, l2ToL1MsgHash: string,
pollForPending: boolean = true pollForPending: boolean = true
): Promise<any> { ): Promise<TransactionReceipt> {
return this._getLXTransactionReceipt(true, l2ToL1MsgHash, pollForPending) return this.getTransactionReceipt(this.l2, l2ToL1MsgHash, pollForPending)
} }
public async getL2TransactionReceipt( public async getL2TransactionReceipt(
l1ToL2MsgHash: string, l1ToL2MsgHash: string,
pollForPending: boolean = true pollForPending: boolean = true
): Promise<any> { ): Promise<TransactionReceipt> {
return this._getLXTransactionReceipt(false, l1ToL2MsgHash, pollForPending) return this.getTransactionReceipt(this.l2, l1ToL2MsgHash, pollForPending)
} }
private async _getMessageHashesFromTx( public async getMessageHashesFromTx(
isL1: boolean, layer: Layer,
txHash: string txHash: string
): Promise<string[]> { ): Promise<string[]> {
const layer = isL1 ? this.l1 : this.l2
const receipt = await layer.provider.getTransactionReceipt(txHash) const receipt = await layer.provider.getTransactionReceipt(txHash)
if (!receipt) { if (!receipt) {
return [] return []
...@@ -69,12 +68,11 @@ export class Watcher { ...@@ -69,12 +68,11 @@ export class Watcher {
return msgHashes return msgHashes
} }
public async _getLXTransactionReceipt( public async getTransactionReceipt(
isL1: boolean, layer: Layer,
msgHash: string, msgHash: string,
pollForPending: boolean pollForPending: boolean = true
): Promise<any> { ): Promise<TransactionReceipt> {
const layer = isL1 ? this.l1 : this.l2
const blockNumber = await layer.provider.getBlockNumber() const blockNumber = await layer.provider.getBlockNumber()
const startingBlock = Math.max(blockNumber - this.NUM_BLOCKS_TO_FETCH, 0) const startingBlock = Math.max(blockNumber - this.NUM_BLOCKS_TO_FETCH, 0)
const filter = { const filter = {
......
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