Commit cee2a464 authored by Matthew Slipper's avatar Matthew Slipper Committed by Kelvin Fichter

feat: integration-tests core-utils Kovan integration tests

parent 499d8736
---
'@eth-optimism/core-utils': patch
---
Add awaitCondition to core utils
......@@ -13,19 +13,23 @@ yarn build
### Testing a live network
Create an `.env` file and fill it out.
Look at `.env.example` to know which variables to include.
Testing on a live network is a bit more complicated than testing locally. You'll need the following in order to do so:
Once you have your environment set up, run:
1. A pre-funded wallet with at least 40 ETH.
2. URLs to an L1 and L2 node.
3. The address of the address manager contract.
4. The chain ID of the L2.
Once you have all the necessary info, create a `.env` file like the one in `.env.example` and fill it in with the values above. Then, run:
```bash
yarn test:integration:live
```
This will take quite a long time. Kovan, for example, takes about 30 minutes to complete.
You can also set environment variables on the command line instead of inside `.env` if you want:
```bash
L1_URL=whatever L2_URL=whatever yarn test:integration:live
```
Note that this can take an extremely long time (~1hr).
......@@ -4,6 +4,7 @@ import { HardhatUserConfig } from 'hardhat/types'
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'
import 'hardhat-gas-reporter'
import { isLiveNetwork } from './test/shared/utils'
const enableGasReport = !!process.env.ENABLE_GAS_REPORT
......@@ -14,7 +15,7 @@ const config: HardhatUserConfig = {
},
},
mocha: {
timeout: 75_000,
timeout: isLiveNetwork() ? 300_000 : 75_000,
},
solidity: {
version: '0.8.9',
......
......@@ -11,7 +11,7 @@
"lint:check": "eslint .",
"build": "hardhat compile",
"test:integration": "hardhat --network optimism test",
"test:integration:live": "IS_LIVE_NETWORK=true hardhat --network optimism test",
"test:integration:live": "NO_NETWORK=true IS_LIVE_NETWORK=true hardhat --network optimism test",
"test:sync": "hardhat --network optimism test sync-tests/*.spec.ts --no-compile",
"clean": "rimraf cache artifacts"
},
......
......@@ -2,13 +2,14 @@ import { expect } from 'chai'
/* Imports: External */
import { Contract, ContractFactory } from 'ethers'
import { applyL1ToL2Alias, sleep } from '@eth-optimism/core-utils'
import { applyL1ToL2Alias } from '@eth-optimism/core-utils'
/* Imports: Internal */
import simpleStorageJson from '../artifacts/contracts/SimpleStorage.sol/SimpleStorage.json'
import l2ReverterJson from '../artifacts/contracts/Reverter.sol/Reverter.json'
import { Direction } from './shared/watcher-utils'
import { OptimismEnv, useDynamicTimeoutForWithdrawals } from './shared/env'
import { OptimismEnv } from './shared/env'
import { awaitCondition } from '@eth-optimism/core-utils'
describe('Basic L1<>L2 Communication', async () => {
let Factory__L1SimpleStorage: ContractFactory
......@@ -48,9 +49,7 @@ describe('Basic L1<>L2 Communication', async () => {
})
describe('L2 => L1', () => {
it('should be able to perform a withdrawal from L2 -> L1', async function () {
await useDynamicTimeoutForWithdrawals(this, env)
it('should be able to perform a withdrawal from L2 -> L1', async () => {
const value = `0x${'77'.repeat(32)}`
// Send L2 -> L1 message.
......@@ -114,8 +113,14 @@ describe('Basic L1<>L2 Communication', async () => {
])
)
// TODO: We need to have a function that can wait for enqueued txs.
await sleep(10000)
await awaitCondition(
async () => {
const sender = await L2SimpleStorage.msgSender()
return sender === env.l1Wallet.address
},
2000,
60
)
// No aliasing when an EOA goes directly to L2.
expect(await L2SimpleStorage.msgSender()).to.equal(env.l1Wallet.address)
......
......@@ -13,11 +13,14 @@ import {
} from '@eth-optimism/contracts'
/* Imports: Internal */
import { IS_LIVE_NETWORK } from './shared/utils'
import { isLiveNetwork } from './shared/utils'
import { OptimismEnv } from './shared/env'
import { Direction } from './shared/watcher-utils'
const setPrices = async (env: OptimismEnv, value: number | BigNumber) => {
if (isLiveNetwork()) {
return
}
const gasPrice = await env.gasPriceOracle.setGasPrice(value)
await gasPrice.wait()
const baseFee = await env.gasPriceOracle.setL1BaseFee(value)
......@@ -32,20 +35,22 @@ describe('Fee Payment Integration Tests', async () => {
env = await OptimismEnv.new()
})
it(`should return eth_gasPrice equal to OVM_GasPriceOracle.gasPrice`, async () => {
const assertGasPrice = async () => {
const gasPrice = await env.l2Wallet.getGasPrice()
const oracleGasPrice = await env.gasPriceOracle.gasPrice()
expect(gasPrice).to.deep.equal(oracleGasPrice)
}
if (!isLiveNetwork()) {
it(`should return eth_gasPrice equal to OVM_GasPriceOracle.gasPrice`, async () => {
const assertGasPrice = async () => {
const gasPrice = await env.l2Wallet.getGasPrice()
const oracleGasPrice = await env.gasPriceOracle.gasPrice()
expect(gasPrice).to.deep.equal(oracleGasPrice)
}
assertGasPrice()
// update the gas price
const tx = await env.gasPriceOracle.setGasPrice(1000)
await tx.wait()
assertGasPrice()
// update the gas price
const tx = await env.gasPriceOracle.setGasPrice(1000)
await tx.wait()
assertGasPrice()
})
assertGasPrice()
})
}
it('Paying a nonzero but acceptable gasPrice fee', async () => {
await setPrices(env, 1000)
......@@ -152,19 +157,16 @@ describe('Fee Payment Integration Tests', async () => {
})
it('should be able to withdraw fees back to L1 once the minimum is met', async function () {
if (isLiveNetwork()) {
this.skip()
return
}
const l1FeeWallet = await env.sequencerFeeVault.l1FeeWallet()
const balanceBefore = await env.l1Wallet.provider.getBalance(l1FeeWallet)
const withdrawalAmount = await env.sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT()
const l2WalletBalance = await env.l2Wallet.getBalance()
if (IS_LIVE_NETWORK && l2WalletBalance.lt(withdrawalAmount)) {
console.log(
`NOTICE: must have at least ${ethers.utils.formatEther(
withdrawalAmount
)} ETH on L2 to execute this test, skipping`
)
this.skip()
}
// Transfer the minimum required to withdraw.
const tx = await env.l2Wallet.sendTransaction({
......
import { BigNumber, Contract, ContractFactory, Wallet } from 'ethers'
import {
BigNumber,
BigNumberish,
Contract,
ContractFactory,
Wallet,
} from 'ethers'
import { ethers } from 'hardhat'
import chai, { expect } from 'chai'
import { GWEI, fundUser, encodeSolidityRevertMessage } from './shared/utils'
import {
fundUser,
encodeSolidityRevertMessage,
gasPriceForL2,
} from './shared/utils'
import { OptimismEnv } from './shared/env'
import { solidity } from 'ethereum-waffle'
import { sleep } from '../../packages/core-utils/dist'
import {
getContractFactory,
getContractInterface,
} from '../../packages/contracts/dist'
import { Interface } from 'ethers/lib/utils'
chai.use(solidity)
......@@ -32,15 +36,16 @@ describe('Native ETH value integration tests', () => {
]
}
const checkBalances = async (
expectedBalances: BigNumber[]
): Promise<void> => {
const realBalances = await getBalances()
expect(realBalances[0]).to.deep.eq(expectedBalances[0])
expect(realBalances[1]).to.deep.eq(expectedBalances[1])
const expectBalancesWithinRange = (
bal: BigNumber,
lte: BigNumber,
gte: BigNumber
) => {
expect(bal.lte(lte)).to.be.true
expect(bal.gte(gte)).to.be.true
}
const value = 10
const value = ethers.utils.parseEther('0.01')
await fundUser(env.watcher, env.l1Bridge, value, wallet.address)
const initialBalances = await getBalances()
......@@ -48,23 +53,40 @@ describe('Native ETH value integration tests', () => {
const there = await wallet.sendTransaction({
to: other.address,
value,
gasPrice: 0,
gasPrice: await gasPriceForL2(),
})
await there.wait()
const thereReceipt = await there.wait()
const thereGas = thereReceipt.gasUsed.mul(there.gasPrice)
await checkBalances([
const thereBalances = await getBalances()
const thereWithGas = initialBalances[0].sub(value).sub(thereGas).sub(100000)
expectBalancesWithinRange(
thereBalances[0],
initialBalances[0].sub(value),
initialBalances[1].add(value),
])
thereWithGas
)
expect(initialBalances[1].add(value).eq(thereBalances[1]))
const backVal = ethers.utils.parseEther('0.005')
const backAgain = await other.sendTransaction({
to: wallet.address,
value,
gasPrice: 0,
value: backVal,
gasPrice: await gasPriceForL2(),
})
await backAgain.wait()
await checkBalances(initialBalances)
const backReceipt = await backAgain.wait()
const backGas = backReceipt.gasUsed.mul(backAgain.gasPrice)
const backBalances = await getBalances()
expectBalancesWithinRange(
backBalances[0],
initialBalances[0].sub(thereGas).sub(backVal),
initialBalances[0].sub(thereGas).sub(backVal).sub(200000)
)
expectBalancesWithinRange(
backBalances[1],
initialBalances[1].add(backVal).sub(backGas),
initialBalances[1].add(backVal).sub(backGas).sub(200000)
)
})
describe(`calls between OVM contracts with native ETH value and relevant opcodes`, async () => {
......@@ -155,7 +177,7 @@ describe('Native ETH value integration tests', () => {
it('should allow ETH to be sent', async () => {
const sendAmount = 15
const tx = await ValueCalls0.simpleSend(ValueCalls1.address, sendAmount, {
gasPrice: 0,
gasPrice: await gasPriceForL2(),
})
await tx.wait()
......
......@@ -183,9 +183,7 @@ describe('Native ETH Integration Tests', async () => {
).to.be.reverted
})
it('withdraw', async function () {
await useDynamicTimeoutForWithdrawals(this, env)
it('withdraw', async () => {
const withdrawAmount = BigNumber.from(3)
const preBalances = await getBalances(env)
expect(
......@@ -227,9 +225,7 @@ describe('Native ETH Integration Tests', async () => {
)
})
it('withdrawTo', async function () {
await useDynamicTimeoutForWithdrawals(this, env)
it('withdrawTo', async () => {
const withdrawAmount = BigNumber.from(3)
const preBalances = await getBalances(env)
......@@ -287,9 +283,7 @@ describe('Native ETH Integration Tests', async () => {
)
})
it('deposit, transfer, withdraw', async function () {
await useDynamicTimeoutForWithdrawals(this, env)
it('deposit, transfer, withdraw', async () => {
// 1. deposit
const amount = utils.parseEther('1')
await env.waitForXDomainTransaction(
......
......@@ -7,6 +7,7 @@ import { injectL2Context, applyL1ToL2Alias } from '@eth-optimism/core-utils'
/* Imports: External */
import { OptimismEnv } from './shared/env'
import { Direction } from './shared/watcher-utils'
import { isLiveNetwork } from './shared/utils'
describe('Queue Ingestion', () => {
let env: OptimismEnv
......@@ -61,5 +62,5 @@ describe('Queue Ingestion', () => {
)
expect(l2Tx.l1BlockNumber).to.equal(l1TxReceipt.blockNumber)
}
}).timeout(100_000)
}).timeout(isLiveNetwork() ? 300_000 : 100_000)
})
......@@ -6,10 +6,12 @@ import chai, { expect } from 'chai'
import {
sleep,
l2Provider,
DEFAULT_TRANSACTION,
defaultTransactionFactory,
fundUser,
L2_CHAINID,
IS_LIVE_NETWORK,
isLiveNetwork,
gasPriceForL2,
} from './shared/utils'
import chaiAsPromised from 'chai-as-promised'
import { OptimismEnv } from './shared/env'
......@@ -57,7 +59,8 @@ describe('Basic RPC tests', () => {
describe('eth_sendRawTransaction', () => {
it('should correctly process a valid transaction', async () => {
const tx = DEFAULT_TRANSACTION
const tx = defaultTransactionFactory()
tx.gasPrice = await gasPriceForL2()
const nonce = await wallet.getTransactionCount()
const result = await wallet.sendTransaction(tx)
......@@ -70,7 +73,8 @@ describe('Basic RPC tests', () => {
it('should not accept a transaction with the wrong chain ID', async () => {
const tx = {
...DEFAULT_TRANSACTION,
...defaultTransactionFactory(),
gasPrice: await gasPriceForL2(),
chainId: (await wallet.getChainId()) + 1,
}
......@@ -81,7 +85,8 @@ describe('Basic RPC tests', () => {
it('should not accept a transaction without a chain ID', async () => {
const tx = {
...DEFAULT_TRANSACTION,
...defaultTransactionFactory(),
gasPrice: await gasPriceForL2(),
chainId: null, // Disables EIP155 transaction signing.
}
......@@ -92,7 +97,8 @@ describe('Basic RPC tests', () => {
it('should accept a transaction with a value', async () => {
const tx = {
...DEFAULT_TRANSACTION,
...defaultTransactionFactory(),
gasPrice: await gasPriceForL2(),
chainId: await env.l2Wallet.getChainId(),
data: '0x',
value: ethers.utils.parseEther('0.1'),
......@@ -103,15 +109,16 @@ describe('Basic RPC tests', () => {
const receipt = await result.wait()
expect(receipt.status).to.deep.equal(1)
expect(await provider.getBalance(env.l2Wallet.address)).to.deep.equal(
balanceBefore.sub(ethers.utils.parseEther('0.1'))
)
const balAfter = await provider.getBalance(env.l2Wallet.address)
expect(balAfter.lte(balanceBefore.sub(ethers.utils.parseEther('0.1')))).to
.be.true
})
it('should reject a transaction with higher value than user balance', async () => {
const balance = await env.l2Wallet.getBalance()
const tx = {
...DEFAULT_TRANSACTION,
...defaultTransactionFactory(),
gasPrice: await gasPriceForL2(),
chainId: await env.l2Wallet.getChainId(),
data: '0x',
value: balance.add(ethers.utils.parseEther('1')),
......@@ -240,7 +247,7 @@ describe('Basic RPC tests', () => {
it('includes L1 gas price and L1 gas used', async () => {
const tx = await env.l2Wallet.populateTransaction({
to: env.l2Wallet.address,
gasPrice: 1,
gasPrice: isLiveNetwork() ? 10000 : 1,
})
const raw = serialize({
......@@ -274,7 +281,8 @@ describe('Basic RPC tests', () => {
describe('eth_getTransactionByHash', () => {
it('should be able to get all relevant l1/l2 transaction data', async () => {
const tx = DEFAULT_TRANSACTION
const tx = defaultTransactionFactory()
tx.gasPrice = await gasPriceForL2()
const result = await wallet.sendTransaction(tx)
await result.wait()
......@@ -288,7 +296,8 @@ describe('Basic RPC tests', () => {
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 tx = defaultTransactionFactory()
tx.gasPrice = await gasPriceForL2()
const result = await wallet.sendTransaction(tx)
const receipt = await result.wait()
......@@ -362,7 +371,7 @@ describe('Basic RPC tests', () => {
let lastEstimate: BigNumber
for (let i = 0; i < 10; i++) {
const estimate = await l2Provider.estimateGas({
to: DEFAULT_TRANSACTION.to,
to: defaultTransactionFactory().to,
value: 0,
})
......@@ -376,7 +385,7 @@ describe('Basic RPC tests', () => {
it('should return a gas estimate for txs with empty data', async () => {
const estimate = await l2Provider.estimateGas({
to: DEFAULT_TRANSACTION.to,
to: defaultTransactionFactory().to,
value: 0,
})
// Expect gas to be less than or equal to the target plus 1%
......
......@@ -185,6 +185,16 @@ export class OptimismEnv {
await sleep(5000)
} else if (err.message.includes('Nonce too low')) {
await sleep(5000)
} else if (err.message.includes('transaction was replaced')) {
// this happens when we run tests in parallel
await sleep(5000)
} else if (
err.message.includes(
'another transaction with same nonce in the queue'
)
) {
// this happens when we run tests in parallel
await sleep(5000)
} else if (
err.message.includes('message has already been received')
) {
......
......@@ -4,6 +4,7 @@ import { ethers } from 'ethers'
/* Imports: Internal */
import { OptimismEnv } from './env'
import { Direction } from './watcher-utils'
import { gasPriceForL1, gasPriceForL2, sleep } from './utils'
interface TransactionParams {
contract: ethers.Contract
......@@ -14,13 +15,29 @@ interface TransactionParams {
// Arbitrary big amount of gas for the L1<>L2 messages.
const MESSAGE_GAS = 8_000_000
export const executeL1ToL2Transactions = async (
export const fundRandomWallet = async (
env: OptimismEnv,
txs: TransactionParams[]
wallet: ethers.Wallet,
value: ethers.BigNumber
): Promise<ethers.Wallet> => {
const fundTx = await env.l1Wallet.sendTransaction({
gasLimit: 25_000,
to: wallet.address,
gasPrice: await gasPriceForL1(env),
value,
})
await fundTx.wait()
return wallet
}
export const executeL1ToL2Transaction = async (
env: OptimismEnv,
wallet: ethers.Wallet,
tx: TransactionParams
) => {
for (const tx of txs) {
const signer = ethers.Wallet.createRandom().connect(env.l1Wallet.provider)
const receipt = await env.l1Messenger
const signer = wallet.connect(env.l1Wallet.provider)
const receipt = await retryOnNonceError(async () =>
env.l1Messenger
.connect(signer)
.sendMessage(
tx.contract.address,
......@@ -30,21 +47,21 @@ export const executeL1ToL2Transactions = async (
),
MESSAGE_GAS,
{
gasPrice: 0,
gasPrice: await gasPriceForL1(env),
}
)
await env.waitForXDomainTransaction(receipt, Direction.L1ToL2)
}
)
await env.waitForXDomainTransaction(receipt, Direction.L1ToL2)
}
export const executeL2ToL1Transactions = async (
export const executeL2ToL1Transaction = async (
env: OptimismEnv,
txs: TransactionParams[]
wallet: ethers.Wallet,
tx: TransactionParams
) => {
for (const tx of txs) {
const signer = ethers.Wallet.createRandom().connect(env.l2Wallet.provider)
const receipt = await env.l2Messenger
const signer = wallet.connect(env.l2Wallet.provider)
const receipt = await retryOnNonceError(() =>
env.l2Messenger
.connect(signer)
.sendMessage(
tx.contract.address,
......@@ -54,162 +71,105 @@ export const executeL2ToL1Transactions = async (
),
MESSAGE_GAS,
{
gasPrice: 0,
gasPrice: gasPriceForL2(),
}
)
)
await env.relayXDomainMessages(receipt)
await env.waitForXDomainTransaction(receipt, Direction.L2ToL1)
}
await env.relayXDomainMessages(receipt)
await env.waitForXDomainTransaction(receipt, Direction.L2ToL1)
}
export const executeL2Transactions = async (
export const executeL2Transaction = async (
env: OptimismEnv,
txs: TransactionParams[]
wallet: ethers.Wallet,
tx: TransactionParams
) => {
for (const tx of txs) {
const signer = ethers.Wallet.createRandom().connect(env.l2Wallet.provider)
const result = await tx.contract
const signer = wallet.connect(env.l2Wallet.provider)
const result = await retryOnNonceError(() =>
tx.contract
.connect(signer)
.functions[tx.functionName](...tx.functionParams, {
gasPrice: 0,
gasPrice: gasPriceForL2(),
})
await result.wait()
}
)
await result.wait()
}
export const executeRepeatedL1ToL2Transactions = async (
env: OptimismEnv,
tx: TransactionParams,
count: number
wallets: ethers.Wallet[],
tx: TransactionParams
) => {
await executeL1ToL2Transactions(
env,
[...Array(count).keys()].map(() => tx)
)
for (const wallet of wallets) {
await executeL1ToL2Transaction(env, wallet, tx)
}
}
export const executeRepeatedL2ToL1Transactions = async (
env: OptimismEnv,
tx: TransactionParams,
count: number
wallets: ethers.Wallet[],
tx: TransactionParams
) => {
await executeL2ToL1Transactions(
env,
[...Array(count).keys()].map(() => tx)
)
for (const wallet of wallets) {
await executeL2ToL1Transaction(env, wallet, tx)
}
}
export const executeRepeatedL2Transactions = async (
env: OptimismEnv,
tx: TransactionParams,
count: number
wallets: ethers.Wallet[],
tx: TransactionParams
) => {
await executeL2Transactions(
env,
[...Array(count).keys()].map(() => tx)
)
for (const wallet of wallets) {
await executeL2Transaction(env, wallet, tx)
}
}
export const executeL1ToL2TransactionsParallel = async (
env: OptimismEnv,
txs: TransactionParams[]
wallets: ethers.Wallet[],
tx: TransactionParams
) => {
await Promise.all(
txs.map(async (tx) => {
const signer = ethers.Wallet.createRandom().connect(env.l1Wallet.provider)
const receipt = await env.l1Messenger
.connect(signer)
.sendMessage(
tx.contract.address,
tx.contract.interface.encodeFunctionData(
tx.functionName,
tx.functionParams
),
MESSAGE_GAS,
{
gasPrice: 0,
}
)
await env.waitForXDomainTransaction(receipt, Direction.L1ToL2)
})
)
await Promise.all(wallets.map((w) => executeL1ToL2Transaction(env, w, tx)))
}
export const executeL2ToL1TransactionsParallel = async (
env: OptimismEnv,
txs: TransactionParams[]
wallets: ethers.Wallet[],
tx: TransactionParams
) => {
await Promise.all(
txs.map(async (tx) => {
const signer = ethers.Wallet.createRandom().connect(env.l2Wallet.provider)
const receipt = await env.l2Messenger
.connect(signer)
.sendMessage(
tx.contract.address,
tx.contract.interface.encodeFunctionData(
tx.functionName,
tx.functionParams
),
MESSAGE_GAS,
{
gasPrice: 0,
}
)
await env.relayXDomainMessages(receipt)
await env.waitForXDomainTransaction(receipt, Direction.L2ToL1)
})
)
await Promise.all(wallets.map((w) => executeL2ToL1Transaction(env, w, tx)))
}
export const executeL2TransactionsParallel = async (
env: OptimismEnv,
txs: TransactionParams[]
) => {
await Promise.all(
txs.map(async (tx) => {
const signer = ethers.Wallet.createRandom().connect(env.l2Wallet.provider)
const result = await tx.contract
.connect(signer)
.functions[tx.functionName](...tx.functionParams, {
gasPrice: 0,
})
await result.wait()
})
)
}
export const executeRepeatedL1ToL2TransactionsParallel = async (
env: OptimismEnv,
tx: TransactionParams,
count: number
) => {
await executeL1ToL2TransactionsParallel(
env,
[...Array(count).keys()].map(() => tx)
)
}
export const executeRepeatedL2ToL1TransactionsParallel = async (
env: OptimismEnv,
tx: TransactionParams,
count: number
wallets: ethers.Wallet[],
tx: TransactionParams
) => {
await executeL2ToL1TransactionsParallel(
env,
[...Array(count).keys()].map(() => tx)
)
await Promise.all(wallets.map((w) => executeL2Transaction(env, w, tx)))
}
export const executeRepeatedL2TransactionsParallel = async (
env: OptimismEnv,
tx: TransactionParams,
count: number
) => {
await executeL2TransactionsParallel(
env,
[...Array(count).keys()].map(() => tx)
)
const retryOnNonceError = async (cb: () => Promise<any>): Promise<any> => {
while (true) {
try {
return await cb()
} catch (err) {
const msg = err.message.toLowerCase()
if (
msg.includes('nonce too low') ||
msg.includes('nonce has already been used') ||
msg.includes('transaction was replaced') ||
msg.includes('another transaction with same nonce in the queue') ||
msg.includes('reverted without a reason')
) {
console.warn('Retrying transaction after nonce error.')
await sleep(5000)
continue
}
throw err
}
}
}
......@@ -19,10 +19,15 @@ import dotenv from 'dotenv'
/* Imports: Internal */
import { Direction, waitForXDomainTransaction } from './watcher-utils'
import { OptimismEnv } from './env'
export const GWEI = BigNumber.from(1e9)
if (process.env.IS_LIVE_NETWORK === 'true') {
export const isLiveNetwork = () => {
return process.env.IS_LIVE_NETWORK === 'true'
}
if (isLiveNetwork()) {
dotenv.config()
}
......@@ -144,12 +149,14 @@ export const encodeSolidityRevertMessage = (_reason: string): string => {
return '0x08c379a0' + remove0x(abiCoder.encode(['string'], [_reason]))
}
export const DEFAULT_TRANSACTION = {
to: '0x' + '1234'.repeat(10),
gasLimit: 8_000_000,
gasPrice: 0,
data: '0x',
value: 0,
export const defaultTransactionFactory = () => {
return {
to: '0x' + '1234'.repeat(10),
gasLimit: 8_000_000,
gasPrice: BigNumber.from(0),
data: '0x',
value: 0,
}
}
export const waitForL2Geth = async (
......@@ -166,3 +173,44 @@ export const waitForL2Geth = async (
}
return injectL2Context(provider)
}
export const awaitCondition = async (
cond: () => Promise<boolean>,
rate = 1000,
attempts = 10
) => {
for (let i = 0; i < attempts; i++) {
const ok = await cond()
if (ok) {
return
}
await sleep(rate)
}
throw new Error('Timed out.')
}
export const gasPriceForL2 = async () => {
if (isLiveNetwork()) {
return Promise.resolve(BigNumber.from(10000))
}
return Promise.resolve(BigNumber.from(0))
}
// eslint-disable-next-line @typescript-eslint/no-shadow
export const gasPriceForL1 = async (env: OptimismEnv) => {
const chainId = await env.l1Wallet.getChainId()
switch (chainId) {
case 1:
return env.l1Wallet.getGasPrice()
case 3:
case 42:
return utils.parseUnits('10', 'gwei')
case 5:
return utils.parseUnits('2', 'gwei')
default:
return BigNumber.from(0)
}
}
......@@ -60,6 +60,11 @@ export const waitForXDomainTransaction = async (
// get the message hash which was created on the SentMessage
const [xDomainMsgHash] = await watcher.getMessageHashesFromTx(src, tx.hash)
if (!xDomainMsgHash) {
throw new Error(`No x-domain message hash for tx hash ${tx.hash}, bailing.`)
}
// Get the transaction and receipt on the remote layer
const remoteReceipt = await watcher.getTransactionReceipt(
dest,
......
import { expect } from 'chai'
/* Imports: External */
import { Contract, ContractFactory } from 'ethers'
import { Contract, ContractFactory, Wallet, utils } from 'ethers'
/* Imports: Internal */
import { OptimismEnv } from './shared/env'
import {
executeL1ToL2TransactionsParallel,
executeL2ToL1TransactionsParallel,
executeL2TransactionsParallel,
executeRepeatedL1ToL2Transactions,
executeRepeatedL2ToL1Transactions,
executeRepeatedL2Transactions,
executeRepeatedL1ToL2TransactionsParallel,
executeRepeatedL2ToL1TransactionsParallel,
executeRepeatedL2TransactionsParallel,
fundRandomWallet,
} from './shared/stress-test-helpers'
/* Imports: Artifacts */
import simpleStorageJson from '../artifacts/contracts/SimpleStorage.sol/SimpleStorage.json'
import { fundUser, isLiveNetwork } from './shared/utils'
// Need a big timeout to allow for all transactions to be processed.
// For some reason I can't figure out how to set the timeout on a per-suite basis
// so I'm instead setting it for every test.
const STRESS_TEST_TIMEOUT = 500_000
const STRESS_TEST_TIMEOUT = isLiveNetwork() ? 500_000 : 1_200_000
describe('stress tests', () => {
const numTransactions = 3
let env: OptimismEnv
const wallets: Wallet[] = []
before(async () => {
env = await OptimismEnv.new()
for (let i = 0; i < numTransactions; i++) {
wallets.push(Wallet.createRandom())
}
for (const wallet of wallets) {
await fundRandomWallet(env, wallet, utils.parseEther('0.1'))
}
for (const wallet of wallets) {
await fundUser(
env.watcher,
env.l1Bridge,
utils.parseEther('0.1'),
wallet.address
)
}
})
let L2SimpleStorage: Contract
......@@ -48,106 +72,76 @@ describe('stress tests', () => {
})
describe('L1 => L2 stress tests', () => {
const numTransactions = 10
it(`${numTransactions} L1 => L2 transactions (serial)`, async () => {
await executeRepeatedL1ToL2Transactions(
env,
{
contract: L2SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
)
await executeRepeatedL1ToL2Transactions(env, wallets, {
contract: L2SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
})
expect((await L2SimpleStorage.totalCount()).toNumber()).to.equal(
numTransactions
wallets.length
)
}).timeout(STRESS_TEST_TIMEOUT)
it(`${numTransactions} L1 => L2 transactions (parallel)`, async () => {
await executeRepeatedL1ToL2TransactionsParallel(
env,
{
contract: L2SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
)
await executeL1ToL2TransactionsParallel(env, wallets, {
contract: L2SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
})
expect((await L2SimpleStorage.totalCount()).toNumber()).to.equal(
numTransactions
wallets.length
)
}).timeout(STRESS_TEST_TIMEOUT)
})
describe('L2 => L1 stress tests', () => {
const numTransactions = 10
it(`${numTransactions} L2 => L1 transactions (serial)`, async () => {
await executeRepeatedL2ToL1Transactions(
env,
{
contract: L1SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
)
await executeRepeatedL2ToL1Transactions(env, wallets, {
contract: L1SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
})
expect((await L1SimpleStorage.totalCount()).toNumber()).to.equal(
numTransactions
wallets.length
)
}).timeout(STRESS_TEST_TIMEOUT)
it(`${numTransactions} L2 => L1 transactions (parallel)`, async () => {
await executeRepeatedL2ToL1TransactionsParallel(
env,
{
contract: L1SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
)
await executeL2ToL1TransactionsParallel(env, wallets, {
contract: L1SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
})
expect((await L1SimpleStorage.totalCount()).toNumber()).to.equal(
numTransactions
wallets.length
)
}).timeout(STRESS_TEST_TIMEOUT)
})
describe('L2 transaction stress tests', () => {
const numTransactions = 10
it(`${numTransactions} L2 transactions (serial)`, async () => {
await executeRepeatedL2Transactions(
env,
{
contract: L2SimpleStorage,
functionName: 'setValueNotXDomain',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
)
await executeRepeatedL2Transactions(env, wallets, {
contract: L2SimpleStorage,
functionName: 'setValueNotXDomain',
functionParams: [`0x${'42'.repeat(32)}`],
})
expect((await L2SimpleStorage.totalCount()).toNumber()).to.equal(
numTransactions
wallets.length
)
}).timeout(STRESS_TEST_TIMEOUT)
it(`${numTransactions} L2 transactions (parallel)`, async () => {
await executeRepeatedL2TransactionsParallel(
env,
{
contract: L2SimpleStorage,
functionName: 'setValueNotXDomain',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
)
await executeL2TransactionsParallel(env, wallets, {
contract: L2SimpleStorage,
functionName: 'setValueNotXDomain',
functionParams: [`0x${'42'.repeat(32)}`],
})
expect((await L2SimpleStorage.totalCount()).toNumber()).to.equal(
numTransactions
......@@ -156,85 +150,59 @@ describe('stress tests', () => {
})
describe('C-C-C-Combo breakers', () => {
const numTransactions = 10
it(`${numTransactions} L2 transactions, L1 => L2 transactions, L2 => L1 transactions (txs serial, suites parallel)`, async () => {
await Promise.all([
executeRepeatedL1ToL2Transactions(
env,
{
contract: L2SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
),
executeRepeatedL2ToL1Transactions(
env,
{
contract: L1SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
),
executeRepeatedL2Transactions(
env,
{
contract: L2SimpleStorage,
functionName: 'setValueNotXDomain',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
),
executeRepeatedL1ToL2Transactions(env, wallets, {
contract: L2SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
}),
executeRepeatedL2ToL1Transactions(env, wallets, {
contract: L1SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
}),
executeRepeatedL2Transactions(env, wallets, {
contract: L2SimpleStorage,
functionName: 'setValueNotXDomain',
functionParams: [`0x${'42'.repeat(32)}`],
}),
])
expect((await L2SimpleStorage.totalCount()).toNumber()).to.equal(
numTransactions * 2
wallets.length * 2
)
expect((await L1SimpleStorage.totalCount()).toNumber()).to.equal(
numTransactions
wallets.length
)
}).timeout(STRESS_TEST_TIMEOUT)
it(`${numTransactions} L2 transactions, L1 => L2 transactions, L2 => L1 transactions (all parallel)`, async () => {
await Promise.all([
executeRepeatedL1ToL2TransactionsParallel(
env,
{
contract: L2SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
),
executeRepeatedL2ToL1TransactionsParallel(
env,
{
contract: L1SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
),
executeRepeatedL2TransactionsParallel(
env,
{
contract: L2SimpleStorage,
functionName: 'setValueNotXDomain',
functionParams: [`0x${'42'.repeat(32)}`],
},
numTransactions
),
executeL1ToL2TransactionsParallel(env, wallets, {
contract: L2SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
}),
executeL2ToL1TransactionsParallel(env, wallets, {
contract: L1SimpleStorage,
functionName: 'setValue',
functionParams: [`0x${'42'.repeat(32)}`],
}),
executeL2TransactionsParallel(env, wallets, {
contract: L2SimpleStorage,
functionName: 'setValueNotXDomain',
functionParams: [`0x${'42'.repeat(32)}`],
}),
])
expect((await L2SimpleStorage.totalCount()).toNumber()).to.equal(
numTransactions * 2
wallets.length * 2
)
expect((await L1SimpleStorage.totalCount()).toNumber()).to.equal(
numTransactions
wallets.length
)
}).timeout(STRESS_TEST_TIMEOUT)
})
......
......@@ -5,9 +5,9 @@ import {
getContractFromArtifact,
fundAccount,
sendImpersonatedTx,
waitUntilTrue,
BIG_BALANCE,
} from '../src/hardhat-deploy-ethers'
import { awaitCondition } from '@eth-optimism/core-utils'
const deployFn: DeployFunction = async (hre) => {
if ((hre as any).deployConfig.forked !== 'true') {
......@@ -39,9 +39,13 @@ const deployFn: DeployFunction = async (hre) => {
})
console.log(`Waiting for owner to be correctly set...`)
await waitUntilTrue(async () => {
return (await Lib_AddressManager.owner()) === deployer
})
await awaitCondition(
async () => {
return (await Lib_AddressManager.owner()) === deployer
},
5000,
100
)
// Get a reference to the L1StandardBridge contract.
const Proxy__OVM_L1StandardBridge = await getContractFromArtifact(
......@@ -63,13 +67,17 @@ const deployFn: DeployFunction = async (hre) => {
})
console.log(`Waiting for owner to be correctly set...`)
await waitUntilTrue(async () => {
return (
(await Proxy__OVM_L1StandardBridge.callStatic.getOwner({
from: hre.ethers.constants.AddressZero,
})) === deployer
)
})
await awaitCondition(
async () => {
return (
(await Proxy__OVM_L1StandardBridge.callStatic.getOwner({
from: hre.ethers.constants.AddressZero,
})) === deployer
)
},
5000,
100
)
}
deployFn.tags = ['hardhat', 'upgrade']
......
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import { hexStringEquals } from '@eth-optimism/core-utils'
import { hexStringEquals, awaitCondition } from '@eth-optimism/core-utils'
/* Imports: Internal */
import {
deployAndPostDeploy,
getContractFromArtifact,
waitUntilTrue,
} from '../src/hardhat-deploy-ethers'
const deployFn: DeployFunction = async (hre) => {
......@@ -29,12 +28,16 @@ const deployFn: DeployFunction = async (hre) => {
await contract.initialize(Lib_AddressManager.address)
console.log(`Checking that contract was correctly initialized...`)
await waitUntilTrue(async () => {
return hexStringEquals(
await contract.libAddressManager(),
Lib_AddressManager.address
)
})
await awaitCondition(
async () => {
return hexStringEquals(
await contract.libAddressManager(),
Lib_AddressManager.address
)
},
5000,
100
)
},
})
}
......
......@@ -6,7 +6,6 @@ import { hexStringEquals } from '@eth-optimism/core-utils'
import {
deployAndPostDeploy,
getContractFromArtifact,
waitUntilTrue,
} from '../src/hardhat-deploy-ethers'
const deployFn: DeployFunction = async (hre) => {
......
/* Imports: External */
import { hexStringEquals } from '@eth-optimism/core-utils'
import { hexStringEquals, awaitCondition } from '@eth-optimism/core-utils'
import { ethers } from 'hardhat'
import { DeployFunction } from 'hardhat-deploy/dist/types'
/* Imports: Internal */
import {
getContractFromArtifact,
waitUntilTrue,
} from '../src/hardhat-deploy-ethers'
import { getContractFromArtifact } from '../src/hardhat-deploy-ethers'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
......@@ -92,18 +89,16 @@ const deployFn: DeployFunction = async (hre) => {
}
// Wait for ownership to be transferred to the AddressDictator contract.
await waitUntilTrue(
await awaitCondition(
async () => {
return hexStringEquals(
await Lib_AddressManager.owner(),
AddressDictator.address
)
},
{
// Try every 30 seconds for 500 minutes.
delay: 30_000,
retries: 1000,
}
// Try every 30 seconds for 500 minutes.
30000,
1000
)
// Set the addresses!
......@@ -112,9 +107,13 @@ const deployFn: DeployFunction = async (hre) => {
// Make sure ownership has been correctly sent back to the original owner.
console.log('Verifying final ownership of Lib_AddressManager...')
await waitUntilTrue(async () => {
return hexStringEquals(await Lib_AddressManager.owner(), finalOwner)
})
await awaitCondition(
async () => {
return hexStringEquals(await Lib_AddressManager.owner(), finalOwner)
},
500,
1000
)
}
deployFn.tags = ['set-addresses', 'upgrade']
......
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import { hexStringEquals } from '@eth-optimism/core-utils'
import { hexStringEquals, awaitCondition } from '@eth-optimism/core-utils'
/* Imports: Internal */
import {
getContractFromArtifact,
waitUntilTrue,
} from '../src/hardhat-deploy-ethers'
import { getContractFromArtifact } from '../src/hardhat-deploy-ethers'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
......@@ -34,12 +31,16 @@ const deployFn: DeployFunction = async (hre) => {
await Proxy__OVM_L1CrossDomainMessenger.initialize(Lib_AddressManager.address)
console.log(`Checking that contract was correctly initialized...`)
await waitUntilTrue(async () => {
return hexStringEquals(
await Proxy__OVM_L1CrossDomainMessenger.libAddressManager(),
Lib_AddressManager.address
)
})
await awaitCondition(
async () => {
return hexStringEquals(
await Proxy__OVM_L1CrossDomainMessenger.libAddressManager(),
Lib_AddressManager.address
)
},
5000,
100
)
}
deployFn.tags = ['finalize']
......
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import { ethers } from 'ethers'
import { hexStringEquals } from '@eth-optimism/core-utils'
import { hexStringEquals, awaitCondition } from '@eth-optimism/core-utils'
/* Imports: Internal */
import { getContractDefinition } from '../src/contract-defs'
import {
getContractFromArtifact,
waitUntilTrue,
deployAndPostDeploy,
} from '../src/hardhat-deploy-ethers'
......@@ -100,7 +99,7 @@ const deployFn: DeployFunction = async (hre) => {
}
// Wait for ownership to be transferred to the AddressDictator contract.
await waitUntilTrue(
await awaitCondition(
async () => {
return hexStringEquals(
await Proxy__OVM_L1StandardBridge.connect(
......@@ -111,11 +110,8 @@ const deployFn: DeployFunction = async (hre) => {
ChugSplashDictator.address
)
},
{
// Try every 30 seconds for 500 minutes.
delay: 30_000,
retries: 1000,
}
30000,
1000
)
// Set the addresses!
......@@ -123,16 +119,20 @@ const deployFn: DeployFunction = async (hre) => {
await ChugSplashDictator.doActions(bridgeCode)
console.log(`Confirming that owner address was correctly set...`)
await waitUntilTrue(async () => {
return hexStringEquals(
await Proxy__OVM_L1StandardBridge.connect(
Proxy__OVM_L1StandardBridge.signer.provider
).callStatic.getOwner({
from: ethers.constants.AddressZero,
}),
finalOwner
)
})
await awaitCondition(
async () => {
return hexStringEquals(
await Proxy__OVM_L1StandardBridge.connect(
Proxy__OVM_L1StandardBridge.signer.provider
).callStatic.getOwner({
from: ethers.constants.AddressZero,
}),
finalOwner
)
},
5000,
100
)
// Deploy a copy of the implementation so it can be successfully verified on Etherscan.
console.log(`Deploying a copy of the bridge for Etherscan verification...`)
......
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
import { hexStringEquals } from '@eth-optimism/core-utils'
import { hexStringEquals, awaitCondition } from '@eth-optimism/core-utils'
/* Imports: Internal */
import {
getContractFromArtifact,
waitUntilTrue,
} from '../src/hardhat-deploy-ethers'
import { getContractFromArtifact } from '../src/hardhat-deploy-ethers'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
......@@ -31,9 +28,13 @@ const deployFn: DeployFunction = async (hre) => {
await Lib_AddressManager.transferOwnership(owner)
console.log(`Confirming transfer was successful...`)
await waitUntilTrue(async () => {
return hexStringEquals(await Lib_AddressManager.owner(), owner)
})
await awaitCondition(
async () => {
return hexStringEquals(await Lib_AddressManager.owner(), owner)
},
5000,
100
)
console.log(`✓ Set owner of Lib_AddressManager to: ${owner}`)
}
......
......@@ -2,29 +2,13 @@
import { ethers, Contract } from 'ethers'
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { sleep, hexStringEquals } from '@eth-optimism/core-utils'
import {
sleep,
hexStringEquals,
awaitCondition,
} from '@eth-optimism/core-utils'
import { HttpNetworkConfig } from 'hardhat/types'
export const waitUntilTrue = async (
check: () => Promise<boolean>,
opts: {
retries?: number
delay?: number
} = {}
) => {
opts.retries = opts.retries || 100
opts.delay = opts.delay || 5000
let retries = 0
while (!(await check())) {
if (retries > opts.retries) {
throw new Error(`check failed after ${opts.retries} attempts`)
}
retries++
await sleep(opts.delay)
}
}
export const deployAndPostDeploy = async ({
hre,
name,
......@@ -152,10 +136,14 @@ export const fundAccount = async (
])
console.log(`Waiting for balance to reflect...`)
await waitUntilTrue(async () => {
const balance = await hre.ethers.provider.getBalance(address)
return balance.gte(amount)
})
await awaitCondition(
async () => {
const balance = await hre.ethers.provider.getBalance(address)
return balance.gte(amount)
},
5000,
100
)
console.log(`Account successfully funded.`)
}
......
import { expect } from 'chai'
import { BigNumber } from 'ethers'
import { sleep } from './misc'
interface deviationRanges {
percentUpperDeviation?: number
......@@ -8,6 +9,23 @@ interface deviationRanges {
absoluteLowerDeviation?: number
}
export const awaitCondition = async (
cond: () => Promise<boolean>,
rate = 1000,
attempts = 10
) => {
for (let i = 0; i < attempts; i++) {
const ok = await cond()
if (ok) {
return
}
await sleep(rate)
}
throw new Error('Timed out.')
}
/**
* Assert that a number lies within a custom defined range of the target.
*/
......
import { expect } from '../setup'
/* Imports: Internal */
import { expectApprox } from '../../src'
import { expectApprox, awaitCondition } from '../../src'
import { assert } from 'chai'
describe('awaitCondition', () => {
it('should try the condition fn until it returns true', async () => {
let i = 0
const condFn = async () => {
i++
return Promise.resolve(i === 2)
}
await awaitCondition(condFn, 50, 3);
expect(i).to.equal(2)
})
it('should only try the configured number of attempts', async () => {
let i = 0
const condFn = async () => {
i++
return Promise.resolve(i === 2)
}
try {
await awaitCondition(condFn, 50, 1);
} catch (e) {
return;
}
assert.fail('Condition never failed, but it should have.');
})
})
describe('expectApprox', () => {
it('should pass when the actual number is higher, but within the expected range of the target', async () => {
expectApprox(119, 100, {
......
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