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

Merge pull request #1225 from ethereum-optimism/develop

Merge develop to master
parents 9ed1e38b bfa906bc
---
'@eth-optimism/integration-tests': minor
---
Various updates to integration tests so that they can be executed against production networks
---
'@eth-optimism/l2geth': patch
---
Update the `RollupClient` transaction type to use `hexutil.Big`
......@@ -92,6 +92,14 @@ jobs:
push: true
tags: ethereumoptimism/l2geth:${{ needs.release.outputs.l2geth }}
- name: Publish op_exporter
uses: docker/build-push-action@v2
with:
context: .
file: ./ops/docker/Dockerfile.op_exporter
push: true
tags: ethereumoptimism/op_exporter:${{ needs.release.outputs.l2geth }}
- name: Publish rpc-proxy
uses: docker/build-push-action@v2
with:
......
......@@ -61,8 +61,42 @@ jobs:
env:
CC_SECRET: ${{ secrets.CC_SECRET }}
# A hack that allows running a job only if a specific directory changed.
# Ref: https://github.community/t/run-job-only-if-folder-changed/118292
is-contracts-package:
name: Check files
outputs:
run_coverage: ${{ steps.check_files.outputs.run_coverage }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- run: git fetch origin $GITHUB_BASE_REF
- name: check modified files
id: check_files
run: |
echo "=============== list modified files ==============="
git diff --name-only origin/$GITHUB_BASE_REF HEAD -- .
echo "========== check paths of modified files =========="
git diff --name-only origin/$GITHUB_BASE_REF HEAD -- . > files.txt
while IFS= read -r file
do
echo $file
if [[ $file != packages/contracts/* ]]; then
echo "This modified files are not in the contracts package."
echo "::set-output name=run_coverage::false"
break
else
echo "::set-output name=run_coverage::true"
fi
done < files.txt
test-coverage:
name: Generate test coverage
needs: is-contracts-package
if: needs.is-contracts-package.outputs.run_coverage == 'true'
runs-on: ubuntu-latest
steps:
......
op_exporter
.env
\ No newline at end of file
SHELL := /bin/bash
VERSION := `git describe --abbrev=0`
GITCOMMIT := `git rev-parse HEAD`
BUILDDATE := `date +%Y-%m-%d`
BUILDUSER := `whoami`
LDFLAGSSTRING :=-X github.com/ethereum-optimism/optimism/go/op_exporter/version.Version=$(VERSION)
LDFLAGSSTRING +=-X github.com/ethereum-optimism/optimism/go/op_exporter/version.GitCommit=$(GITCOMMIT)
LDFLAGSSTRING +=-X github.com/ethereum-optimism/optimism/go/op_exporter/version.BuildDate=$(BUILDDATE)
LDFLAGSSTRING +=-X github.com/ethereum-optimism/optimism/go/op_exporter/version.BuildUser=$(BUILDUSER)
LDFLAGS :=-ldflags "$(LDFLAGSSTRING)"
.PHONY: all build
all: build
# Build binary
build:
CGO_ENABLED=0 go build $(LDFLAGS)
\ No newline at end of file
# op_exporter
A prometheus exporter to collect information from an Optimistic Ethereum node and serve metrics for collection
## Usage
```
make build && ./op_exporter --rpc.provider="https://kovan-sequencer.optimism.io" --label.network="kovan"
```
## Health endpoint `/health`
Returns json describing the health of the sequencer based on the time since a block height update.
```
$ curl http://localhost:9100/health
{ "healthy": "false" }
```
## Metrics endpoint `/metrics`
```
# HELP op_gasPrice Gas price.
# TYPE op_gasPrice gauge
op_gasPrice{layer="layer1",network="kovan"} 6.9e+09
op_gasPrice{layer="layer2",network="kovan"} 1
```
package main
import (
"github.com/prometheus/client_golang/prometheus"
)
//Define the metrics we wish to expose
var (
gasPrice = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "op_gasPrice",
Help: "Gas price."},
[]string{"network", "layer"},
)
blockNumber = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "op_blocknumber",
Help: "Current block number."},
[]string{"network", "layer"},
)
healthySequencer = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "op_healthy_sequencer",
Help: "Is the sequencer healthy?"},
[]string{"network"},
)
)
func init() {
//Register metrics with prometheus
prometheus.MustRegister(gasPrice)
prometheus.MustRegister(blockNumber)
prometheus.MustRegister(healthySequencer)
}
module github.com/ethereum-optimism/optimism/go/op_exporter
go 1.16
require (
github.com/ethereum/go-ethereum v1.10.4
github.com/prometheus/client_golang v1.4.0
github.com/sirupsen/logrus v1.4.2
github.com/ybbus/jsonrpc v2.1.2+incompatible
gopkg.in/alecthomas/kingpin.v2 v2.2.6
)
This diff is collapsed.
package main
import (
"fmt"
"net/http"
"os"
"sync"
"time"
"github.com/ethereum-optimism/optimism/go/op_exporter/version"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
"github.com/ybbus/jsonrpc"
"gopkg.in/alecthomas/kingpin.v2"
)
var (
listenAddress = kingpin.Flag(
"web.listen-address",
"Address on which to expose metrics and web interface.",
).Default(":9100").String()
rpcProvider = kingpin.Flag(
"rpc.provider",
"Address for RPC provider.",
).Default("http://127.0.0.1:8545").String()
networkLabel = kingpin.Flag(
"label.network",
"Label to apply to the metrics to identify the network.",
).Default("mainnet").String()
versionFlag = kingpin.Flag(
"version",
"Display binary version.",
).Default("False").Bool()
unhealthyTimePeriod = kingpin.Flag(
"wait.minutes",
"Number of minutes to wait for the next block before marking provider unhealthy.",
).Default("10").Int()
//unhealthyTimePeriod = time.Minute * 10
)
type healthCheck struct {
mu *sync.RWMutex
height uint64
healthy bool
updateTime time.Time
}
func healthHandler(health *healthCheck) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
health.mu.RLock()
defer health.mu.RUnlock()
w.Write([]byte(fmt.Sprintf(`{ "healthy": "%t" }`, health.healthy)))
}
}
func main() {
kingpin.HelpFlag.Short('h')
kingpin.Parse()
if *versionFlag {
fmt.Printf("(version=%s, gitcommit=%s)\n", version.Version, version.GitCommit)
fmt.Printf("(go=%s, user=%s, date=%s)\n", version.GoVersion, version.BuildUser, version.BuildDate)
os.Exit(0)
}
log.Infoln("exporter config", *listenAddress, *rpcProvider, *networkLabel)
log.Infoln("Starting op_exporter", version.Info())
log.Infoln("Build context", version.BuildContext())
health := healthCheck{
mu: new(sync.RWMutex),
height: 0,
healthy: false,
updateTime: time.Now(),
}
http.Handle("/metrics", promhttp.Handler())
http.Handle("/health", healthHandler(&health))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<html>
<head><title>OP Exporter</title></head>
<body>
<h1>OP Exporter</h1>
<p><a href="/metrics">Metrics</a></p>
<p><a href="/health">Health</a></p>
</body>
</html>`))
})
go getRollupGasPrices()
go getBlockNumber(&health)
log.Infoln("Listening on", *listenAddress)
if err := http.ListenAndServe(*listenAddress, nil); err != nil {
log.Fatal(err)
}
}
func getBlockNumber(health *healthCheck) {
rpcClient := jsonrpc.NewClientWithOpts(*rpcProvider, &jsonrpc.RPCClientOpts{})
var blockNumberResponse *string
for {
if err := rpcClient.CallFor(&blockNumberResponse, "eth_blockNumber"); err != nil {
health.mu.Lock()
health.healthy = false
health.mu.Unlock()
log.Warnln("Error calling eth_blockNumber, setting unhealthy", err)
} else {
log.Infoln("Got block number: ", *blockNumberResponse)
health.mu.Lock()
currentHeight, err := hexutil.DecodeUint64(*blockNumberResponse)
blockNumber.WithLabelValues(
*networkLabel, "layer2").Set(float64(currentHeight))
if err != nil {
log.Warnln("Error decoding block height", err)
continue
}
lastHeight := health.height
// If the currentHeight is the same as the lastHeight, check that
// the unhealthyTimePeriod has passed and update health.healthy
if currentHeight == lastHeight {
currentTime := time.Now()
lastTime := health.updateTime
log.Warnln(fmt.Sprintf("Heights are the same, %v, %v", currentTime, lastTime))
if lastTime.Add(time.Duration(*unhealthyTimePeriod) * time.Minute).Before(currentTime) {
health.healthy = false
log.Warnln("Heights are the same for the unhealthyTimePeriod, setting unhealthy")
}
} else {
log.Warnln("New block height detected, setting healthy")
health.height = currentHeight
health.updateTime = time.Now()
health.healthy = true
}
if health.healthy {
healthySequencer.WithLabelValues(
*networkLabel).Set(1)
} else {
healthySequencer.WithLabelValues(
*networkLabel).Set(0)
}
health.mu.Unlock()
}
time.Sleep(time.Duration(30) * time.Second)
}
}
func getRollupGasPrices() {
rpcClient := jsonrpc.NewClientWithOpts(*rpcProvider, &jsonrpc.RPCClientOpts{})
var rollupGasPrices *GetRollupGasPrices
for {
if err := rpcClient.CallFor(&rollupGasPrices, "rollup_gasPrices"); err != nil {
log.Warnln("Error calling rollup_gasPrices", err)
} else {
l1GasPriceString := rollupGasPrices.L1GasPrice
l1GasPrice, err := hexutil.DecodeUint64(l1GasPriceString)
if err != nil {
log.Warnln("Error converting gasPrice " + l1GasPriceString)
}
gasPrice.WithLabelValues(
*networkLabel, "layer1").Set(float64(l1GasPrice))
l2GasPriceString := rollupGasPrices.L2GasPrice
l2GasPrice, err := hexutil.DecodeUint64(l2GasPriceString)
if err != nil {
log.Warnln("Error converting gasPrice " + l2GasPriceString)
}
gasPrice.WithLabelValues(
*networkLabel, "layer2").Set(float64(l2GasPrice))
log.Infoln("Got L1 gas string: ", l1GasPriceString)
log.Infoln("Got L1 gas prices: ", l1GasPrice)
log.Infoln("Got L2 gas string: ", l2GasPriceString)
log.Infoln("Got L2 gas prices: ", l2GasPrice)
}
time.Sleep(time.Duration(30) * time.Second)
}
}
package main
// GetRollupGasPrices returns the rpc `rollup_gasPrices` status
type GetRollupGasPrices struct {
L1GasPrice string `json:"l1GasPrice"`
L2GasPrice string `json:"l2GasPrice"`
}
type GetBlockNumber struct {
BlockNumber string `json:"result"`
}
package version
import (
"fmt"
"runtime"
)
var (
Version string
GitCommit string
BuildUser string
BuildDate string
GoVersion = runtime.Version()
)
func Info() string {
return fmt.Sprintf("(version=%s, gitcommit=%s)", Version, GitCommit)
}
func BuildContext() string {
return fmt.Sprintf("(go=%s, user=%s, date=%s)", GoVersion, BuildUser, BuildDate)
}
# only need to fill these out if you want to test against a prod network
PRIVATE_KEY=
L1_URL=
L2_URL=
ADDRESS_MANAGER=
L2_CHAINID=
# @eth-optimism/integration-tests
## Setup
Follow installation + build instructions in the [primary README](../README.md).
Then, run:
```bash
yarn build:integration
```
## Running tests
### Testing a live network
Create an `.env` file and fill it out.
Look at `.env.example` to know which variables to include.
Once you have your environment set up, run:
```bash
yarn test:integration:live
```
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).
......@@ -9,14 +9,19 @@ import 'hardhat-gas-reporter'
const enableGasReport = !!process.env.ENABLE_GAS_REPORT
const config: HardhatUserConfig = {
mocha: {
timeout: 20000,
},
networks: {
optimism: {
url: process.env.L2_URL || 'http://localhost:8545',
ovm: true,
},
'optimism-live': {
url: process.env.L2_URL || 'http://localhost:8545',
ovm: true,
timeout: 150000,
},
},
mocha: {
timeout: 50000,
},
solidity: '0.7.6',
ovm: {
......
......@@ -13,6 +13,7 @@
"build:contracts": "hardhat compile",
"build:contracts:ovm": "hardhat compile --network optimism",
"test:integration": "hardhat --network optimism test",
"test:integration:live": "IS_LIVE_NETWORK=true hardhat --network optimism-live test",
"test:sync": "hardhat --network optimism test sync-tests/*.spec.ts --no-compile",
"clean": "rimraf cache artifacts artifacts-ovm cache-ovm"
},
......@@ -20,6 +21,7 @@
"@eth-optimism/contracts": "^0.4.2",
"@eth-optimism/core-utils": "^0.5.0",
"@eth-optimism/hardhat-ovm": "^0.2.2",
"@eth-optimism/message-relayer": "^0.1.6",
"@ethersproject/providers": "^5.0.24",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1",
......@@ -33,6 +35,7 @@
"chai": "^4.3.3",
"chai-as-promised": "^7.1.1",
"docker-compose": "^0.23.8",
"dotenv": "^10.0.0",
"envalid": "^7.1.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.27.0",
......
......@@ -3,13 +3,13 @@ import { expect } from 'chai'
/* Imports: External */
import { Contract, ContractFactory } from 'ethers'
import { predeploys, getContractInterface } from '@eth-optimism/contracts'
import { Direction } from './shared/watcher-utils'
/* Imports: Internal */
import l1SimpleStorageJson from '../artifacts/contracts/SimpleStorage.sol/SimpleStorage.json'
import l2SimpleStorageJson from '../artifacts-ovm/contracts/SimpleStorage.sol/SimpleStorage.json'
import l2ReverterJson from '../artifacts-ovm/contracts/Reverter.sol/Reverter.json'
import { OptimismEnv } from './shared/env'
import { Direction } from './shared/watcher-utils'
import { OptimismEnv, useDynamicTimeoutForWithdrawals } from './shared/env'
describe('Basic L1<>L2 Communication', async () => {
let Factory__L1SimpleStorage: ContractFactory
......@@ -49,7 +49,9 @@ describe('Basic L1<>L2 Communication', async () => {
})
describe('L2 => L1', () => {
it('should be able to perform a withdrawal from L2 -> L1', async () => {
it('should be able to perform a withdrawal from L2 -> L1', async function () {
await useDynamicTimeoutForWithdrawals(this, env)
const value = `0x${'77'.repeat(32)}`
// Send L2 -> L1 message.
......@@ -58,7 +60,8 @@ describe('Basic L1<>L2 Communication', async () => {
L1SimpleStorage.interface.encodeFunctionData('setValue', [value]),
5000000
)
await transaction.wait()
await env.relayXDomainMessages(transaction)
await env.waitForXDomainTransaction(transaction, Direction.L2ToL1)
expect(await L1SimpleStorage.msgSender()).to.equal(
......
......@@ -3,11 +3,12 @@ import chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised)
/* Imports: External */
import { BigNumber, Contract, utils } from 'ethers'
import { ethers, BigNumber, Contract, utils } from 'ethers'
import { TxGasLimit, TxGasPrice } from '@eth-optimism/core-utils'
import { predeploys, getContractInterface } from '@eth-optimism/contracts'
/* Imports: Internal */
import { IS_LIVE_NETWORK } from './shared/utils'
import { OptimismEnv } from './shared/env'
import { Direction } from './shared/watcher-utils'
......@@ -36,11 +37,11 @@ describe('Fee Payment Integration Tests', async () => {
it('Should estimateGas with recoverable L2 gasLimit', async () => {
const gas = await env.ovmEth.estimateGas.transfer(
other,
utils.parseEther('0.5')
utils.parseEther('0.0000001')
)
const tx = await env.ovmEth.populateTransaction.transfer(
other,
utils.parseEther('0.5')
utils.parseEther('0.0000001')
)
const executionGas = await (env.ovmEth.provider as any).send(
'eth_estimateExecutionGas',
......@@ -51,7 +52,7 @@ describe('Fee Payment Integration Tests', async () => {
})
it('Paying a nonzero but acceptable gasPrice fee', async () => {
const amount = utils.parseEther('0.5')
const amount = utils.parseEther('0.0000001')
const balanceBefore = await env.l2Wallet.getBalance()
const feeVaultBalanceBefore = await env.l2Wallet.provider.getBalance(
ovmSequencerFeeVault.address
......@@ -84,15 +85,23 @@ describe('Fee Payment Integration Tests', async () => {
await expect(ovmSequencerFeeVault.withdraw()).to.be.rejected
})
it('should be able to withdraw fees back to L1 once the minimum is met', async () => {
it('should be able to withdraw fees back to L1 once the minimum is met', async function () {
const l1FeeWallet = await ovmSequencerFeeVault.l1FeeWallet()
const balanceBefore = await env.l1Wallet.provider.getBalance(l1FeeWallet)
const withdrawalAmount = await ovmSequencerFeeVault.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.
await env.ovmEth.transfer(
ovmSequencerFeeVault.address,
await ovmSequencerFeeVault.MIN_WITHDRAWAL_AMOUNT()
)
await env.ovmEth.transfer(ovmSequencerFeeVault.address, withdrawalAmount)
const vaultBalance = await env.ovmEth.balanceOf(
ovmSequencerFeeVault.address
......
import { predeploys } from '@eth-optimism/contracts'
import { expect } from 'chai'
/* Imports: External */
import { Wallet, utils, BigNumber } from 'ethers'
import { predeploys } from '@eth-optimism/contracts'
/* Imports: Internal */
import { Direction } from './shared/watcher-utils'
import {
......@@ -9,7 +12,7 @@ import {
fundUser,
PROXY_SEQUENCER_ENTRYPOINT_ADDRESS,
} from './shared/utils'
import { OptimismEnv } from './shared/env'
import { OptimismEnv, useDynamicTimeoutForWithdrawals } from './shared/env'
const DEFAULT_TEST_GAS_L1 = 330_000
const DEFAULT_TEST_GAS_L2 = 1_300_000
......@@ -53,7 +56,7 @@ describe('Native ETH Integration Tests', async () => {
describe('estimateGas', () => {
it('Should estimate gas for ETH transfer', async () => {
const amount = utils.parseEther('0.5')
const amount = utils.parseEther('0.0000001')
const addr = '0x' + '1234'.repeat(10)
const gas = await env.ovmEth.estimateGas.transfer(addr, amount)
// Expect gas to be less than or equal to the target plus 1%
......@@ -61,7 +64,7 @@ describe('Native ETH Integration Tests', async () => {
})
it('Should estimate gas for ETH withdraw', async () => {
const amount = utils.parseEther('0.5')
const amount = utils.parseEther('0.0000001')
const gas = await env.l2Bridge.estimateGas.withdraw(
predeploys.OVM_ETH,
amount,
......@@ -186,14 +189,13 @@ describe('Native ETH Integration Tests', async () => {
await expect(
env.l1Bridge.depositETH(DEFAULT_TEST_GAS_L2, data, {
value: depositAmount,
gasLimit: 4_000_000,
})
).to.be.revertedWith(
'Transaction data size exceeds maximum for rollup transaction.'
)
).to.be.reverted
})
it('withdraw', async () => {
it('withdraw', async function () {
await useDynamicTimeoutForWithdrawals(this, env)
const withdrawAmount = BigNumber.from(3)
const preBalances = await getBalances(env)
expect(
......@@ -201,31 +203,43 @@ describe('Native ETH Integration Tests', async () => {
'Cannot run withdrawal test before any deposits...'
)
const transaction = await env.l2Bridge.withdraw(
predeploys.OVM_ETH,
withdrawAmount,
DEFAULT_TEST_GAS_L2,
'0xFFFF'
)
await transaction.wait()
await env.relayXDomainMessages(transaction)
const receipts = await env.waitForXDomainTransaction(
env.l2Bridge.withdraw(
predeploys.OVM_ETH,
withdrawAmount,
DEFAULT_TEST_GAS_L2,
'0xFFFF'
),
transaction,
Direction.L2ToL1
)
const fee = receipts.tx.gasLimit.mul(receipts.tx.gasPrice)
const postBalances = await getBalances(env)
expect(postBalances.l1BridgeBalance).to.deep.eq(
preBalances.l1BridgeBalance.sub(withdrawAmount)
// Approximate because there's a fee related to relaying the L2 => L1 message and it throws off the math.
expectApprox(
postBalances.l1BridgeBalance,
preBalances.l1BridgeBalance.sub(withdrawAmount),
{ upperPercentDeviation: 1 }
)
expect(postBalances.l2UserBalance).to.deep.eq(
preBalances.l2UserBalance.sub(withdrawAmount.add(fee))
expectApprox(
postBalances.l2UserBalance,
preBalances.l2UserBalance.sub(withdrawAmount.add(fee)),
{ upperPercentDeviation: 1 }
)
expect(postBalances.l1UserBalance).to.deep.eq(
preBalances.l1UserBalance.add(withdrawAmount)
expectApprox(
postBalances.l1UserBalance,
preBalances.l1UserBalance.add(withdrawAmount),
{ upperPercentDeviation: 1 }
)
})
it('withdrawTo', async () => {
it('withdrawTo', async function () {
await useDynamicTimeoutForWithdrawals(this, env)
const withdrawAmount = BigNumber.from(3)
const preBalances = await getBalances(env)
......@@ -235,14 +249,17 @@ describe('Native ETH Integration Tests', async () => {
'Cannot run withdrawal test before any deposits...'
)
const transaction = await env.l2Bridge.withdrawTo(
predeploys.OVM_ETH,
l1Bob.address,
withdrawAmount,
DEFAULT_TEST_GAS_L2,
'0xFFFF'
)
await transaction.wait()
await env.relayXDomainMessages(transaction)
const receipts = await env.waitForXDomainTransaction(
env.l2Bridge.withdrawTo(
predeploys.OVM_ETH,
l1Bob.address,
withdrawAmount,
DEFAULT_TEST_GAS_L2,
'0xFFFF'
),
transaction,
Direction.L2ToL1
)
const fee = receipts.tx.gasLimit.mul(receipts.tx.gasPrice)
......@@ -260,7 +277,9 @@ describe('Native ETH Integration Tests', async () => {
)
})
it('deposit, transfer, withdraw', async () => {
it('deposit, transfer, withdraw', async function () {
await useDynamicTimeoutForWithdrawals(this, env)
// 1. deposit
const amount = utils.parseEther('1')
await env.waitForXDomainTransaction(
......@@ -282,15 +301,18 @@ describe('Native ETH Integration Tests', async () => {
// 3. do withdrawal
const withdrawnAmount = utils.parseEther('0.95')
const transaction = await env.l2Bridge
.connect(other)
.withdraw(
predeploys.OVM_ETH,
withdrawnAmount,
DEFAULT_TEST_GAS_L1,
'0xFFFF'
)
await transaction.wait()
await env.relayXDomainMessages(transaction)
const receipts = await env.waitForXDomainTransaction(
env.l2Bridge
.connect(other)
.withdraw(
predeploys.OVM_ETH,
withdrawnAmount,
DEFAULT_TEST_GAS_L1,
'0xFFFF'
),
transaction,
Direction.L2ToL1
)
......
import { expect } from 'chai'
/* Imports: External */
import { ethers } from 'hardhat'
import { injectL2Context } from '@eth-optimism/core-utils'
import { expect } from 'chai'
import {
sleep,
l2Provider,
l1Provider,
getAddressManager,
} from './shared/utils'
import { Contract, BigNumber } from 'ethers'
/* Imports: Internal */
import { l2Provider, l1Provider, IS_LIVE_NETWORK } from './shared/utils'
import { OptimismEnv } from './shared/env'
import { getContractFactory } from '@eth-optimism/contracts'
import { Contract, ContractFactory, Wallet, BigNumber } from 'ethers'
import { Direction } from './shared/watcher-utils'
/**
* These tests cover the OVM execution contexts. In the OVM execution
......@@ -17,73 +16,51 @@ import { Contract, ContractFactory, Wallet, BigNumber } from 'ethers'
* must be equal to the blocknumber/timestamp of the L1 transaction.
*/
describe('OVM Context: Layer 2 EVM Context', () => {
let address: string
let CanonicalTransactionChain: Contract
let OVMMulticall: Contract
let OVMContextStorage: Contract
const L1Provider = l1Provider
const L2Provider = injectL2Context(l2Provider)
let env: OptimismEnv
before(async () => {
const env = await OptimismEnv.new()
// Create providers and signers
const l1Wallet = env.l1Wallet
const l2Wallet = env.l2Wallet
const addressManager = env.addressManager
env = await OptimismEnv.new()
})
// deploy the contract
let OVMMulticall: Contract
let OVMContextStorage: Contract
beforeEach(async () => {
const OVMContextStorageFactory = await ethers.getContractFactory(
'OVMContextStorage',
l2Wallet
)
OVMContextStorage = await OVMContextStorageFactory.deploy()
const receipt = await OVMContextStorage.deployTransaction.wait()
address = OVMContextStorage.address
const ctcAddress = await addressManager.getAddress(
'OVM_CanonicalTransactionChain'
env.l2Wallet
)
const CanonicalTransactionChainFactory = getContractFactory(
'OVM_CanonicalTransactionChain'
)
CanonicalTransactionChain =
CanonicalTransactionChainFactory.connect(l1Wallet).attach(ctcAddress)
const OVMMulticallFactory = await ethers.getContractFactory(
'OVMMulticall',
l2Wallet
env.l2Wallet
)
OVMContextStorage = await OVMContextStorageFactory.deploy()
await OVMContextStorage.deployTransaction.wait()
OVMMulticall = await OVMMulticallFactory.deploy()
await OVMMulticall.deployTransaction.wait()
})
it('Enqueue: `block.number` and `block.timestamp` have L1 values', async () => {
for (let i = 0; i < 5; i++) {
const l2Tip = await L2Provider.getBlock('latest')
const tx = await CanonicalTransactionChain.enqueue(
let numTxs = 5
if (IS_LIVE_NETWORK) {
// Tests take way too long if we don't reduce the number of txs here.
numTxs = 1
}
it('enqueue: `block.number` and `block.timestamp` have L1 values', async () => {
for (let i = 0; i < numTxs; i++) {
const tx = await env.l1Messenger.sendMessage(
OVMContextStorage.address,
500_000,
'0x'
'0x',
2_000_000
)
// Wait for the enqueue to be ingested
while (true) {
const tip = await L2Provider.getBlock('latest')
if (tip.number === l2Tip.number + 1) {
break
}
await sleep(500)
}
const receipt = await tx.wait()
// Get the receipt
const receipt = await tx.wait()
// The transaction did not revert
expect(receipt.status).to.equal(1)
await env.waitForXDomainTransaction(tx, Direction.L1ToL2)
// Get the L1 block that the enqueue transaction was in so that
// the timestamp can be compared against the layer two contract
const block = await l1Provider.getBlock(receipt.blockNumber)
......@@ -96,14 +73,18 @@ describe('OVM Context: Layer 2 EVM Context', () => {
const timestamp = await OVMContextStorage.timestamps(i)
expect(block.timestamp).to.deep.equal(timestamp.toNumber())
}
})
}).timeout(150000) // this specific test takes a while because it involves L1 to L2 txs
it('should set correct OVM Context for `eth_call`', async () => {
const tip = await L2Provider.getBlockWithTransactions('latest')
const start = Math.max(0, tip.number - 5)
for (let i = start; i < tip.number; i++) {
const block = await L2Provider.getBlockWithTransactions(i)
for (let i = 0; i < numTxs; i++) {
// Make an empty transaction to bump the latest block number.
const dummyTx = await env.l2Wallet.sendTransaction({
to: `0x${'11'.repeat(20)}`,
data: '0x',
})
await dummyTx.wait()
const block = await L2Provider.getBlockWithTransactions('latest')
const [, returnData] = await OVMMulticall.callStatic.aggregate(
[
[
......@@ -117,7 +98,7 @@ describe('OVM Context: Layer 2 EVM Context', () => {
OVMMulticall.interface.encodeFunctionData('getCurrentBlockNumber'),
],
],
{ blockTag: i }
{ blockTag: block.number }
)
const timestamp = BigNumber.from(returnData[0])
......
import { expect } from 'chai'
/* Imports: Internal */
import { providers } from 'ethers'
import { injectL2Context } from '@eth-optimism/core-utils'
import { sleep } from './shared/utils'
import { OptimismEnv } from './shared/env'
/* Imports: External */
import { providers } from 'ethers'
import { expect } from 'chai'
import { OptimismEnv } from './shared/env'
import { Direction } from './shared/watcher-utils'
// This test ensures that the transactions which get `enqueue`d get
// added to the L2 blocks by the Sync Service (which queries the DTL)
describe('Queue Ingestion', () => {
const RETRIES = 20
const numTxs = 5
let startBlock: number
let endBlock: number
let env: OptimismEnv
let l2Provider: providers.JsonRpcProvider
const receipts = []
before(async () => {
env = await OptimismEnv.new()
l2Provider = injectL2Context(env.l2Wallet.provider as any)
})
// The transactions are enqueue'd with a `to` address of i.repeat(40)
// meaning that the `to` value is different each iteration in a deterministic
// way. They need to be inserted into the L2 chain in an ascending order.
// Keep track of the receipts so that the blockNumber can be compared
// against the `L1BlockNumber` on the tx objects.
before(async () => {
// Keep track of the L2 tip before submitting any transactions so that
// the subsequent transactions can be queried for in the next test
startBlock = (await l2Provider.getBlockNumber()) + 1
endBlock = startBlock + numTxs - 1
// Enqueue some transactions by building the calldata and then sending
// the transaction to Layer 1
for (let i = 0; i < numTxs; i++) {
const input = ['0x' + `${i}`.repeat(40), 500_000, `0x0${i}`]
const calldata = env.ctc.interface.encodeFunctionData('enqueue', input)
const txResponse = await env.l1Wallet.sendTransaction({
data: calldata,
to: env.ctc.address,
})
const receipt = await txResponse.wait()
receipts.push(receipt)
}
})
// The batch submitter will notice that there are transactions
// that are in the queue and submit them. L2 will pick up the
// sequencer batch appended event and play the transactions.
it('should order transactions correctly', async () => {
// Wait until each tx from the previous test has
// been executed
let i: number
for (i = 0; i < RETRIES; i++) {
const tip = await l2Provider.getBlockNumber()
if (tip >= endBlock) {
break
}
await sleep(1000)
}
const numTxs = 5
if (i === RETRIES) {
throw new Error(
'timed out waiting for queued transactions to be inserted'
// Enqueue some transactions by building the calldata and then sending
// the transaction to Layer 1
const txs = []
for (let i = 0; i < numTxs; i++) {
const tx = await env.l1Messenger.sendMessage(
`0x${`${i}`.repeat(40)}`,
`0x0${i}`,
1_000_000
)
await tx.wait()
txs.push(tx)
}
const from = await env.l1Wallet.getAddress()
// Keep track of an index into the receipts list and
// increment it for each block fetched.
let receiptIndex = 0
// Fetch blocks
for (i = 0; i < numTxs; i++) {
const block = await l2Provider.getBlock(startBlock + i)
const hash = block.transactions[0]
// Use as any hack because additional properties are
// added to the transaction response
const tx = await (l2Provider.getTransaction(hash) as any)
for (let i = 0; i < numTxs; i++) {
const l1Tx = txs[i]
const l1TxReceipt = await txs[i].wait()
const receipt = await env.waitForXDomainTransaction(
l1Tx,
Direction.L1ToL2
)
const l2Tx = (await l2Provider.getTransaction(
receipt.remoteTx.hash
)) as any
const params = env.l2Messenger.interface.decodeFunctionData(
'relayMessage',
l2Tx.data
)
// The `to` addresses are defined in the previous test and
// increment sequentially.
expect(tx.to).to.be.equal('0x' + `${i}`.repeat(40))
// The queue origin is Layer 1
expect(tx.queueOrigin).to.be.equal('l1')
// the L1TxOrigin is equal to the Layer one from
expect(tx.l1TxOrigin).to.be.equal(from.toLowerCase())
expect(typeof tx.l1BlockNumber).to.be.equal('number')
// Get the receipt and increment the recept index
const receipt = receipts[receiptIndex++]
expect(tx.l1BlockNumber).to.be.equal(receipt.blockNumber)
expect(params._sender.toLowerCase()).to.equal(
env.l1Wallet.address.toLowerCase()
)
expect(params._target).to.equal('0x' + `${i}`.repeat(40))
expect(l2Tx.queueOrigin).to.equal('l1')
expect(l2Tx.l1TxOrigin.toLowerCase()).to.equal(
env.l1Messenger.address.toLowerCase()
)
expect(l2Tx.l1BlockNumber).to.equal(l1TxReceipt.blockNumber)
}
})
}).timeout(100_000)
})
......@@ -2,7 +2,6 @@ import {
injectL2Context,
TxGasLimit,
TxGasPrice,
toRpcHexString,
} from '@eth-optimism/core-utils'
import { Wallet, BigNumber, Contract, ContractFactory } from 'ethers'
import { ethers } from 'hardhat'
......@@ -13,6 +12,8 @@ import {
DEFAULT_TRANSACTION,
fundUser,
expectApprox,
L2_CHAINID,
IS_LIVE_NETWORK,
} from './shared/utils'
import chaiAsPromised from 'chai-as-promised'
import { OptimismEnv } from './shared/env'
......@@ -132,10 +133,9 @@ describe('Basic RPC tests', () => {
gasPrice: TxGasPrice,
}
const fee = tx.gasPrice.mul(tx.gasLimit)
const gasLimit = 5920001
await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith(
`fee too low: ${fee}, use at least tx.gasLimit = ${gasLimit} and tx.gasPrice = ${TxGasPrice.toString()}`
`fee too low: ${fee}, use at least tx.gasLimit =`
)
})
......@@ -317,7 +317,15 @@ describe('Basic RPC tests', () => {
// 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 () => {
//
// Needs to be skipped on Prod networks because this test doesn't work when
// other people are sending transactions to the Sequencer at the same time
// as this test is running.
it('should return the same result when new transactions are not applied', async function () {
if (IS_LIVE_NETWORK) {
this.skip()
}
// Get latest block once to start.
const prev = await provider.getBlockWithTransactions('latest')
......@@ -341,7 +349,7 @@ describe('Basic RPC tests', () => {
describe('eth_chainId', () => {
it('should get the correct chainid', async () => {
const { chainId } = await provider.getNetwork()
expect(chainId).to.be.eq(420)
expect(chainId).to.be.eq(L2_CHAINID)
})
})
......
/* Imports: External */
import { Contract, utils, Wallet } from 'ethers'
import { TransactionResponse } from '@ethersproject/providers'
import { getContractFactory, predeploys } from '@eth-optimism/contracts'
import { Watcher } from '@eth-optimism/core-utils'
import { Contract, utils, Wallet } from 'ethers'
import { getMessagesAndProofsForL2Transaction } from '@eth-optimism/message-relayer'
/* Imports: Internal */
import {
getAddressManager,
l1Provider,
......@@ -11,6 +16,8 @@ import {
getOvmEth,
getL1Bridge,
getL2Bridge,
IS_LIVE_NETWORK,
sleep,
} from './utils'
import {
initWatcher,
......@@ -18,7 +25,6 @@ import {
Direction,
waitForXDomainTransaction,
} from './watcher-utils'
import { TransactionResponse } from '@ethersproject/providers'
/// Helper class for instantiating a test environment with a funded account
export class OptimismEnv {
......@@ -27,6 +33,7 @@ export class OptimismEnv {
l1Bridge: Contract
l1Messenger: Contract
ctc: Contract
scc: Contract
// L2 Contracts
ovmEth: Contract
......@@ -53,6 +60,7 @@ export class OptimismEnv {
this.l1Wallet = args.l1Wallet
this.l2Wallet = args.l2Wallet
this.ctc = args.ctc
this.scc = args.scc
}
static async new(): Promise<OptimismEnv> {
......@@ -85,10 +93,18 @@ export class OptimismEnv {
.connect(l2Wallet)
.attach(predeploys.OVM_GasPriceOracle)
const sccAddress = await addressManager.getAddress(
'OVM_StateCommitmentChain'
)
const scc = getContractFactory('OVM_StateCommitmentChain')
.connect(l1Wallet)
.attach(sccAddress)
return new OptimismEnv({
addressManager,
l1Bridge,
ctc,
scc,
l1Messenger,
ovmEth,
gasPriceOracle,
......@@ -106,4 +122,94 @@ export class OptimismEnv {
): Promise<CrossDomainMessagePair> {
return waitForXDomainTransaction(this.watcher, tx, direction)
}
/**
* Relays all L2 => L1 messages found in a given L2 transaction.
*
* @param tx Transaction to find messages in.
*/
async relayXDomainMessages(
tx: Promise<TransactionResponse> | TransactionResponse
): Promise<void> {
tx = await tx
let messagePairs = []
while (true) {
try {
messagePairs = await getMessagesAndProofsForL2Transaction(
l1Provider,
l2Provider,
this.scc.address,
predeploys.OVM_L2CrossDomainMessenger,
tx.hash
)
break
} catch (err) {
if (err.message.includes('unable to find state root batch for tx')) {
await sleep(5000)
} else {
throw err
}
}
}
for (const { message, proof } of messagePairs) {
while (true) {
try {
const result = await this.l1Messenger
.connect(this.l1Wallet)
.relayMessage(
message.target,
message.sender,
message.message,
message.messageNonce,
proof
)
await result.wait()
break
} catch (err) {
if (err.message.includes('execution failed due to an exception')) {
await sleep(5000)
} else if (
err.message.includes('message has already been received')
) {
break
} else {
throw err
}
}
}
}
}
}
/**
* Sets the timeout of a test based on the challenge period of the current network. If the
* challenge period is greater than 60s (e.g., on Mainnet) then we skip this test entirely.
*
* @param testctx Function context of the test to modify (i.e. `this` when inside a test).
* @param env Optimism environment used to resolve the StateCommitmentChain.
*/
export const useDynamicTimeoutForWithdrawals = async (
testctx: any,
env: OptimismEnv
) => {
if (!IS_LIVE_NETWORK) {
return
}
const challengePeriod = await env.scc.FRAUD_PROOF_WINDOW()
if (challengePeriod.gt(60)) {
console.log(
`WARNING: challenge period is greater than 60s (${challengePeriod.toString()}s), skipping test`
)
testctx.skip()
}
// 60s for state root batch to be published + (challenge period x 4)
const timeoutMs = 60000 + challengePeriod.toNumber() * 1000 * 4
console.log(
`NOTICE: inside a withdrawal test on a prod network, dynamically setting timeout to ${timeoutMs}ms`
)
testctx.timeout(timeoutMs)
}
import { expect } from 'chai'
import { Direction, waitForXDomainTransaction } from './watcher-utils'
import {
getContractFactory,
getContractInterface,
predeploys,
} from '@eth-optimism/contracts'
import { injectL2Context, remove0x, Watcher } from '@eth-optimism/core-utils'
/* Imports: External */
import {
Contract,
Wallet,
......@@ -17,10 +10,24 @@ import {
BigNumber,
utils,
} from 'ethers'
import { cleanEnv, str, num } from 'envalid'
import {
getContractFactory,
getContractInterface,
predeploys,
} from '@eth-optimism/contracts'
import { injectL2Context, remove0x, Watcher } from '@eth-optimism/core-utils'
import { cleanEnv, str, num, bool } from 'envalid'
import dotenv from 'dotenv'
/* Imports: Internal */
import { Direction, waitForXDomainTransaction } from './watcher-utils'
export const GWEI = BigNumber.from(1e9)
if (process.env.IS_LIVE_NETWORK === 'true') {
dotenv.config()
}
const env = cleanEnv(process.env, {
L1_URL: str({ default: 'http://localhost:9545' }),
L2_URL: str({ default: 'http://localhost:8545' }),
......@@ -37,6 +44,8 @@ const env = cleanEnv(process.env, {
ADDRESS_MANAGER: str({
default: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
}),
L2_CHAINID: num({ default: 420 }),
IS_LIVE_NETWORK: bool({ default: false }),
})
// The hardhat instance
......@@ -64,6 +73,9 @@ export const PROXY_SEQUENCER_ENTRYPOINT_ADDRESS =
'0x4200000000000000000000000000000000000004'
export const OVM_ETH_ADDRESS = predeploys.OVM_ETH
export const L2_CHAINID = env.L2_CHAINID
export const IS_LIVE_NETWORK = env.IS_LIVE_NETWORK
export const getAddressManager = (provider: any) => {
return getContractFactory('Lib_AddressManager')
.connect(provider)
......
......@@ -72,7 +72,7 @@ type transaction struct {
BatchIndex uint64 `json:"batchIndex"`
BlockNumber uint64 `json:"blockNumber"`
Timestamp uint64 `json:"timestamp"`
Value hexutil.Uint64 `json:"value"`
Value *hexutil.Big `json:"value"`
GasLimit uint64 `json:"gasLimit,string"`
Target common.Address `json:"target"`
Origin *common.Address `json:"origin"`
......@@ -106,7 +106,7 @@ type signature struct {
// it means that the decoding failed.
type decoded struct {
Signature signature `json:"sig"`
Value hexutil.Uint64 `json:"value"`
Value *hexutil.Big `json:"value"`
GasLimit uint64 `json:"gasLimit,string"`
GasPrice uint64 `json:"gasPrice,string"`
Nonce uint64 `json:"nonce,string"`
......@@ -343,7 +343,7 @@ func batchedTransactionToTransaction(res *transaction, signer *types.EIP155Signe
if res.Decoded != nil {
nonce := res.Decoded.Nonce
to := res.Decoded.Target
value := new(big.Int).SetUint64(uint64(res.Decoded.Value))
value := (*big.Int)(res.Decoded.Value)
// Note: there are two gas limits, one top level and
// another on the raw transaction itself. Maybe maxGasLimit
// for the top level?
......@@ -396,7 +396,7 @@ func batchedTransactionToTransaction(res *transaction, signer *types.EIP155Signe
gasLimit := res.GasLimit
data := res.Data
origin := res.Origin
value := new(big.Int).SetUint64(uint64(res.Value))
value := (*big.Int)(res.Value)
tx := types.NewTransaction(nonce, target, value, gasLimit, big.NewInt(0), data)
txMeta := types.NewTransactionMeta(
new(big.Int).SetUint64(res.BlockNumber),
......
package rollup
import (
"encoding/json"
"errors"
"fmt"
"math/big"
......@@ -68,3 +69,40 @@ func TestRollupClientCannotConnect(t *testing.T) {
t.Fatalf("Incorrect error returned: %s", err)
}
}
func TestDecodedJSON(t *testing.T) {
str := []byte(`
{
"index": 643116,
"batchIndex": 21083,
"blockNumber": 25954867,
"timestamp": 1625605288,
"gasLimit": "11000000",
"target": "0x4200000000000000000000000000000000000005",
"origin": null,
"data": "0xf86d0283e4e1c08343eab8941a5245ea5210c3b57b7cfdf965990e63534a7b528901a055690d9db800008081aea019f7c6719f1718475f39fb9e5a6a897c3bd5057488a014666e5ad573ec71cf0fa008836030e686f3175dd7beb8350809b47791c23a19092a8c2fab1f0b4211a466",
"queueOrigin": "sequencer",
"value": "0x1a055690d9db80000",
"queueIndex": null,
"decoded": {
"nonce": "2",
"gasPrice": "15000000",
"gasLimit": "4451000",
"value": "0x1a055690d9db80000",
"target": "0x1a5245ea5210c3b57b7cfdf965990e63534a7b52",
"data": "0x",
"sig": {
"v": 1,
"r": "0x19f7c6719f1718475f39fb9e5a6a897c3bd5057488a014666e5ad573ec71cf0f",
"s": "0x08836030e686f3175dd7beb8350809b47791c23a19092a8c2fab1f0b4211a466"
}
},
"confirmed": true
}`)
tx := new(transaction)
json.Unmarshal(str, tx)
cmp, _ := new(big.Int).SetString("1a055690d9db80000", 16)
if tx.Value.ToInt().Cmp(cmp) != 0 {
t.Fatal("Cannot decode")
}
}
......@@ -13,6 +13,9 @@ COPY --from=builder /optimism/node_modules ./node_modules
COPY --from=builder /optimism/packages/core-utils/package.json ./packages/core-utils/package.json
COPY --from=builder /optimism/packages/core-utils/dist ./packages/core-utils/dist
COPY --from=builder /optimism/packages/message-relayer/package.json ./packages/message-relayer/package.json
COPY --from=builder /optimism/packages/message-relayer/dist ./packages/message-relayer/dist
COPY --from=builder /optimism/packages/hardhat-ovm/package.json ./packages/hardhat-ovm/package.json
COPY --from=builder /optimism/packages/hardhat-ovm/dist ./packages/hardhat-ovm/dist
......
FROM golang:1.16 as builder
ADD ./go/op_exporter /app/
WORKDIR /app/
RUN make build
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/op_exporter /usr/local/bin/
ENTRYPOINT ["op_exporter"]
CMD ["--help"]
......@@ -11,6 +11,15 @@ events {
http {
include mime.types;
index index.html;
# The JSONRPC POST body must fit inside this allocation for the method parsing to succeed.
# https://github.com/openresty/lua-nginx-module#ngxreqread_body
# http://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size
client_body_buffer_size 128k;
# client_max_body_size should match client_body_buffer_size
# Values that exceed client_body_buffer_size will be written to a temporary file, which we don't want
# Requests above this limit will also be denied with an HTTP 413 response (entity too large)
# http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size
client_max_body_size 128k;
# See Move default writable paths to a dedicated directory (#119)
# https://github.com/openresty/docker-openresty/issues/119
......@@ -38,7 +47,7 @@ http {
metric_sequencer_requests = prometheus:counter(
"nginx_eth_sequencer_requests", "Number of requests going to the sequencer", {"method", "host", "status"})
metric_replica_requests = prometheus:counter(
"nginx_eth_replica_requests", "Number of requests going to the replicas", {"host", "status"})
"nginx_eth_replica_requests", "Number of requests going to the replicas", {"host", "status"})
metric_latency = prometheus:histogram(
"nginx_http_request_duration_seconds", "HTTP request latency", {"host"})
metric_connections = prometheus:gauge(
......
......@@ -5636,6 +5636,11 @@ dot-prop@^6.0.1:
dependencies:
is-obj "^2.0.0"
dotenv@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
dotenv@^8.2.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
......
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