Commit 88832fc6 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

Merge pull request #1103 from ethereum-optimism/feat/gas-pricer

feat[geth]: add gas pricer for congestion fees
parents 94cb32b9 c718bcde
---
'@eth-optimism/gas-oracle': patch
---
Initial implementation of the `gas-oracle`
...@@ -12,3 +12,4 @@ tests/testdata ...@@ -12,3 +12,4 @@ tests/testdata
l2geth/signer/fourbyte l2geth/signer/fourbyte
l2geth/cmd/puppeth l2geth/cmd/puppeth
l2geth/cmd/clef l2geth/cmd/clef
go/gas-oracle/gas-oracle
name: gas-oracle unit tests
on:
push:
paths:
- 'go/gas-oracle/**'
branches:
- 'master'
- 'develop'
- '*rc'
- 'regenesis/*'
pull_request:
paths:
- 'go/gas-oracle/**'
branches:
- 'master'
- 'develop'
- '*rc'
- 'regenesis/*'
workflow_dispatch:
defaults:
run:
working-directory: ./go/gas-oracle
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.16.x
- name: Checkout code
uses: actions/checkout@v2
- name: Install
run: make
- name: Test
run: make test
name: golangci-lint
on:
push:
paths:
- 'go/gas-oracle/**'
branches:
- 'master'
- 'develop'
- '*rc'
- 'regenesis/*'
pull_request:
paths:
- 'go/gas-oracle/**'
branches:
- 'master'
- 'develop'
- '*rc'
- 'regenesis/*'
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.29
working-directory: go/gas-oracle
...@@ -17,6 +17,7 @@ jobs: ...@@ -17,6 +17,7 @@ jobs:
message-relayer: ${{ steps.packages.outputs.message-relayer }} message-relayer: ${{ steps.packages.outputs.message-relayer }}
data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }} data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }}
contracts: ${{ steps.packages.outputs.contracts }} contracts: ${{ steps.packages.outputs.contracts }}
gas-oracle: ${{ steps.packages.outputs.gas-oracle }}
steps: steps:
- name: Checkout Repo - name: Checkout Repo
...@@ -108,6 +109,32 @@ jobs: ...@@ -108,6 +109,32 @@ jobs:
push: true push: true
tags: ethereumoptimism/rpc-proxy:${{ needs.release.outputs.l2geth }} tags: ethereumoptimism/rpc-proxy:${{ needs.release.outputs.l2geth }}
gas-oracle:
name: Publish Gas Oracle Version ${{ needs.release.outputs.gas-oracle }}
needs: release
if: needs.release.outputs.gas-oracle != ''
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_USERNAME }}
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_SECRET }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Publish Gas Oracle
uses: docker/build-push-action@v2
with:
context: .
file: ./ops/docker/Dockerfile.gas-oracle
push: true
tags: ethereumoptimism/gas-oracle:${{ needs.release.outputs.gas-oracle }}
# pushes the base builder image to dockerhub # pushes the base builder image to dockerhub
builder: builder:
name: Prepare the base builder image for the services name: Prepare the base builder image for the services
......
SHELL := /bin/bash
GITCOMMIT := $(shell git rev-parse HEAD)
GITDATE := $(shell git show -s --format='%ct')
GITVERSION := $(shell cat package.json | jq .version)
LDFLAGSSTRING +=-X main.GitCommit=$(GITCOMMIT)
LDFLAGSSTRING +=-X main.GitDate=$(GITDATE)
LDFLAGSSTRING +=-X main.GitVersion=$(GITVERSION)
LDFLAGS :=-ldflags "$(LDFLAGSSTRING)"
gas-oracle:
env GO111MODULE=on go build $(LDFLAGS)
clean:
rm gas-oracle
test:
go test -v ./...
lint:
golangci-lint run ./...
binding:
$(eval temp := $(shell mktemp))
cat abis/OVM_GasPriceOracle.json \
| jq -r .bin > $(temp)
cat abis/OVM_GasPriceOracle.json \
| jq .abi \
| abigen --pkg bindings \
--abi - \
--out bindings/gaspriceoracle.go \
--type GasPriceOracle \
--bin $(temp)
rm $(temp)
# gas-oracle
This service is responsible for sending transactions to the Sequencer to update
the L2 gas price over time. It consists of a set of functions found in the
`gasprices` package that define the parameters of how the gas prices are updated
and then the `oracle` package is responsible for observing the Sequencer over
time and send transactions that actually do update the gas prices.
### Generating the Bindings
Note: this only needs to happen if the ABI of the `OVM_GasPriceOracle` is
updated.
This project uses `abigen` to automatically create smart contract bindings in
Go. To generate the bindings, be sure that the latest ABI and bytecode are
committed into the repository in the `abis` directory.
Use the following command to generate the bindings:
```bash
$ make binding
```
Be sure to use `abigen` built with the same version of `go-ethereum` as what is
in the `go.mod` file.
### Building the service
The service can be built with the `Makefile`. A binary will be produced
called the `gas-oracle`.
```bash
$ make gas-oracle
```
### Running the service
Use the `--help` flag when running the `gas-oracle` to see it's configuration
options.
```
NAME:
gas-oracle - Remotely Control the Optimistic Ethereum Gas Price
USAGE:
gas-oracle [global options] command [command options] [arguments...]
VERSION:
0.0.0-1.10.4-stable
DESCRIPTION:
Configure with a private key and an Optimistic Ethereum HTTP endpoint to send transactions that update the L2 gas price.
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--ethereum-http-url value Sequencer HTTP Endpoint (default: "http://127.0.0.1:8545") [$GAS_PRICE_ORACLE_ETHEREUM_HTTP_URL]
--chain-id value L2 Chain ID (default: 0) [$GAS_PRICE_ORACLE_CHAIN_ID]
--gas-price-oracle-address value Address of OVM_GasPriceOracle (default: "0x420000000000000000000000000000000000000F") [$GAS_PRICE_ORACLE_GAS_PRICE_ORACLE_ADDRESS]
--private-key value Private Key corresponding to OVM_GasPriceOracle Owner [$GAS_PRICE_ORACLE_PRIVATE_KEY]
--transaction-gas-price value Hardcoded tx.gasPrice, not setting it uses gas estimation (default: 0) [$GAS_PRICE_ORACLE_TRANSACTION_GAS_PRICE]
--loglevel value log level to emit to the screen (default: 3) [$GAS_PRICE_ORACLE_LOG_LEVEL]
--floor-price value gas price floor (default: 1) [$GAS_PRICE_ORACLE_FLOOR_PRICE]
--target-gas-per-second value target gas per second (default: 11000000) [$GAS_PRICE_ORACLE_TARGET_GAS_PER_SECOND]
--max-percent-change-per-epoch value max percent change of gas price per second (default: 0.1) [$GAS_PRICE_ORACLE_MAX_PERCENT_CHANGE_PER_EPOCH]
--average-block-gas-limit-per-epoch value average block gas limit per epoch (default: 1.1e+07) [$GAS_PRICE_ORACLE_AVERAGE_BLOCK_GAS_LIMIT_PER_EPOCH]
--epoch-length-seconds value length of epochs in seconds (default: 10) [$GAS_PRICE_ORACLE_EPOCH_LENGTH_SECONDS]
--significant-factor value only update when the gas price changes by more than this factor (default: 0.05) [$GAS_PRICE_ORACLE_SIGNIFICANT_FACTOR]
--wait-for-receipt wait for receipts when sending transactions [$GAS_PRICE_ORACLE_WAIT_FOR_RECEIPT]
--metrics Enable metrics collection and reporting [$GAS_PRICE_ORACLE_METRICS_ENABLE]
--metrics.addr value Enable stand-alone metrics HTTP server listening interface (default: "127.0.0.1") [$GAS_PRICE_ORACLE_METRICS_HTTP]
--metrics.port value Metrics HTTP server listening port (default: 6060) [$GAS_PRICE_ORACLE_METRICS_PORT]
--metrics.influxdb Enable metrics export/push to an external InfluxDB database [$GAS_PRICE_ORACLE_METRICS_ENABLE_INFLUX_DB]
--metrics.influxdb.endpoint value InfluxDB API endpoint to report metrics to (default: "http://localhost:8086") [$GAS_PRICE_ORACLE_METRICS_INFLUX_DB_ENDPOINT]
--metrics.influxdb.database value InfluxDB database name to push reported metrics to (default: "gas-oracle") [$GAS_PRICE_ORACLE_METRICS_INFLUX_DB_DATABASE]
--metrics.influxdb.username value Username to authorize access to the database (default: "test") [$GAS_PRICE_ORACLE_METRICS_INFLUX_DB_USERNAME]
--metrics.influxdb.password value Password to authorize access to the database (default: "test") [$GAS_PRICE_ORACLE_METRICS_INFLUX_DB_PASSWORD]
--help, -h show help
--version, -v print the version
```
### Testing the service
The service can be tested with the `Makefile`
```
$ make test
```
{
"abi": [
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
},
{
"internalType": "uint256",
"name": "_initialGasPrice",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"inputs": [],
"name": "gasPrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_gasPrice",
"type": "uint256"
}
],
"name": "setGasPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
],
"bin": "0x608060405234801561001057600080fd5b5060405161061d38038061061d8339818101604052604081101561003357600080fd5b5080516020909101516000610046610097565b600080546001600160a01b0319166001600160a01b0383169081178255604051929350916000805160206105fd833981519152908290a3506100878161009b565b61009082610102565b5050610201565b3390565b6100a3610097565b6001600160a01b03166100b46101f2565b6001600160a01b0316146100fd576040805162461bcd60e51b815260206004820181905260248201526000805160206105dd833981519152604482015290519081900360640190fd5b600155565b61010a610097565b6001600160a01b031661011b6101f2565b6001600160a01b031614610164576040805162461bcd60e51b815260206004820181905260248201526000805160206105dd833981519152604482015290519081900360640190fd5b6001600160a01b0381166101a95760405162461bcd60e51b81526004018080602001828103825260268152602001806105b76026913960400191505060405180910390fd5b600080546040516001600160a01b03808516939216916000805160206105fd83398151915291a3600080546001600160a01b0319166001600160a01b0392909216919091179055565b6000546001600160a01b031690565b6103a7806102106000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c8063715018a61461005c5780638da5cb5b14610066578063bf1fe4201461008a578063f2fde38b146100a7578063fe173b97146100cd575b600080fd5b6100646100e7565b005b61006e6101a5565b604080516001600160a01b039092168252519081900360200190f35b610064600480360360208110156100a057600080fd5b50356101b4565b610064600480360360208110156100bd57600080fd5b50356001600160a01b031661022d565b6100d5610341565b60408051918252519081900360200190f35b6100ef610347565b6001600160a01b03166101006101a5565b6001600160a01b03161461015b576040805162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b600080546040516001600160a01b03909116907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908390a3600080546001600160a01b0319169055565b6000546001600160a01b031690565b6101bc610347565b6001600160a01b03166101cd6101a5565b6001600160a01b031614610228576040805162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b600155565b610235610347565b6001600160a01b03166102466101a5565b6001600160a01b0316146102a1576040805162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b6001600160a01b0381166102e65760405162461bcd60e51b815260040180806020018281038252602681526020018061034c6026913960400191505060405180910390fd5b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0319166001600160a01b0392909216919091179055565b60015481565b339056fe4f776e61626c653a206e6577206f776e657220697320746865207a65726f2061646472657373a26469706673582212205ffb3c08a20124b777934c7f2adbd124e8d73ee3f782032330e9b5c98715395a64736f6c634300070600334f776e61626c653a206e6577206f776e657220697320746865207a65726f20616464726573734f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65728be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0"
}
This diff is collapsed.
package flags
import (
"github.com/urfave/cli"
)
var (
EthereumHttpUrlFlag = cli.StringFlag{
Name: "ethereum-http-url",
Value: "http://127.0.0.1:8545",
Usage: "Sequencer HTTP Endpoint",
EnvVar: "GAS_PRICE_ORACLE_ETHEREUM_HTTP_URL",
}
ChainIDFlag = cli.Uint64Flag{
Name: "chain-id",
Usage: "L2 Chain ID",
EnvVar: "GAS_PRICE_ORACLE_CHAIN_ID",
}
GasPriceOracleAddressFlag = cli.StringFlag{
Name: "gas-price-oracle-address",
Usage: "Address of OVM_GasPriceOracle",
Value: "0x420000000000000000000000000000000000000F",
EnvVar: "GAS_PRICE_ORACLE_GAS_PRICE_ORACLE_ADDRESS",
}
PrivateKeyFlag = cli.StringFlag{
Name: "private-key",
Usage: "Private Key corresponding to OVM_GasPriceOracle Owner",
EnvVar: "GAS_PRICE_ORACLE_PRIVATE_KEY",
}
TransactionGasPriceFlag = cli.Uint64Flag{
Name: "transaction-gas-price",
Usage: "Hardcoded tx.gasPrice, not setting it uses gas estimation",
EnvVar: "GAS_PRICE_ORACLE_TRANSACTION_GAS_PRICE",
}
LogLevelFlag = cli.IntFlag{
Name: "loglevel",
Value: 3,
Usage: "log level to emit to the screen",
EnvVar: "GAS_PRICE_ORACLE_LOG_LEVEL",
}
FloorPriceFlag = cli.Uint64Flag{
Name: "floor-price",
Value: 1,
Usage: "gas price floor",
EnvVar: "GAS_PRICE_ORACLE_FLOOR_PRICE",
}
TargetGasPerSecondFlag = cli.Uint64Flag{
Name: "target-gas-per-second",
Value: 11_000_000,
Usage: "target gas per second",
EnvVar: "GAS_PRICE_ORACLE_TARGET_GAS_PER_SECOND",
}
MaxPercentChangePerEpochFlag = cli.Float64Flag{
Name: "max-percent-change-per-epoch",
Value: 0.1,
Usage: "max percent change of gas price per second",
EnvVar: "GAS_PRICE_ORACLE_MAX_PERCENT_CHANGE_PER_EPOCH",
}
AverageBlockGasLimitPerEpochFlag = cli.Float64Flag{
Name: "average-block-gas-limit-per-epoch",
Value: 11_000_000,
Usage: "average block gas limit per epoch",
EnvVar: "GAS_PRICE_ORACLE_AVERAGE_BLOCK_GAS_LIMIT_PER_EPOCH",
}
EpochLengthSecondsFlag = cli.Uint64Flag{
Name: "epoch-length-seconds",
Value: 10,
Usage: "length of epochs in seconds",
EnvVar: "GAS_PRICE_ORACLE_EPOCH_LENGTH_SECONDS",
}
SignificanceFactorFlag = cli.Float64Flag{
Name: "significant-factor",
Value: 0.05,
Usage: "only update when the gas price changes by more than this factor",
EnvVar: "GAS_PRICE_ORACLE_SIGNIFICANT_FACTOR",
}
WaitForReceiptFlag = cli.BoolFlag{
Name: "wait-for-receipt",
Usage: "wait for receipts when sending transactions",
EnvVar: "GAS_PRICE_ORACLE_WAIT_FOR_RECEIPT",
}
MetricsEnabledFlag = cli.BoolFlag{
Name: "metrics",
Usage: "Enable metrics collection and reporting",
EnvVar: "GAS_PRICE_ORACLE_METRICS_ENABLE",
}
MetricsHTTPFlag = cli.StringFlag{
Name: "metrics.addr",
Usage: "Enable stand-alone metrics HTTP server listening interface",
Value: "127.0.0.1",
EnvVar: "GAS_PRICE_ORACLE_METRICS_HTTP",
}
MetricsPortFlag = cli.IntFlag{
Name: "metrics.port",
Usage: "Metrics HTTP server listening port",
Value: 6060,
EnvVar: "GAS_PRICE_ORACLE_METRICS_PORT",
}
MetricsEnableInfluxDBFlag = cli.BoolFlag{
Name: "metrics.influxdb",
Usage: "Enable metrics export/push to an external InfluxDB database",
EnvVar: "GAS_PRICE_ORACLE_METRICS_ENABLE_INFLUX_DB",
}
MetricsInfluxDBEndpointFlag = cli.StringFlag{
Name: "metrics.influxdb.endpoint",
Usage: "InfluxDB API endpoint to report metrics to",
Value: "http://localhost:8086",
EnvVar: "GAS_PRICE_ORACLE_METRICS_INFLUX_DB_ENDPOINT",
}
MetricsInfluxDBDatabaseFlag = cli.StringFlag{
Name: "metrics.influxdb.database",
Usage: "InfluxDB database name to push reported metrics to",
Value: "gas-oracle",
EnvVar: "GAS_PRICE_ORACLE_METRICS_INFLUX_DB_DATABASE",
}
MetricsInfluxDBUsernameFlag = cli.StringFlag{
Name: "metrics.influxdb.username",
Usage: "Username to authorize access to the database",
Value: "test",
EnvVar: "GAS_PRICE_ORACLE_METRICS_INFLUX_DB_USERNAME",
}
MetricsInfluxDBPasswordFlag = cli.StringFlag{
Name: "metrics.influxdb.password",
Usage: "Password to authorize access to the database",
Value: "test",
EnvVar: "GAS_PRICE_ORACLE_METRICS_INFLUX_DB_PASSWORD",
}
)
var Flags = []cli.Flag{
EthereumHttpUrlFlag,
ChainIDFlag,
GasPriceOracleAddressFlag,
PrivateKeyFlag,
TransactionGasPriceFlag,
LogLevelFlag,
FloorPriceFlag,
TargetGasPerSecondFlag,
MaxPercentChangePerEpochFlag,
AverageBlockGasLimitPerEpochFlag,
EpochLengthSecondsFlag,
SignificanceFactorFlag,
WaitForReceiptFlag,
MetricsEnabledFlag,
MetricsHTTPFlag,
MetricsPortFlag,
MetricsEnableInfluxDBFlag,
MetricsInfluxDBEndpointFlag,
MetricsInfluxDBDatabaseFlag,
MetricsInfluxDBUsernameFlag,
MetricsInfluxDBPasswordFlag,
}
package gasprices
import (
"errors"
"sync"
"github.com/ethereum/go-ethereum/log"
)
type GetLatestBlockNumberFn func() (uint64, error)
type UpdateL2GasPriceFn func(uint64) error
type GasPriceUpdater struct {
mu *sync.RWMutex
gasPricer *GasPricer
epochStartBlockNumber uint64
averageBlockGasLimit float64
epochLengthSeconds uint64
getLatestBlockNumberFn GetLatestBlockNumberFn
updateL2GasPriceFn UpdateL2GasPriceFn
}
func GetAverageGasPerSecond(
epochStartBlockNumber uint64,
latestBlockNumber uint64,
epochLengthSeconds uint64,
averageBlockGasLimit uint64,
) float64 {
blocksPassed := latestBlockNumber - epochStartBlockNumber
return float64(blocksPassed * averageBlockGasLimit / epochLengthSeconds)
}
func NewGasPriceUpdater(
gasPricer *GasPricer,
epochStartBlockNumber uint64,
averageBlockGasLimit float64,
epochLengthSeconds uint64,
getLatestBlockNumberFn GetLatestBlockNumberFn,
updateL2GasPriceFn UpdateL2GasPriceFn,
) (*GasPriceUpdater, error) {
if averageBlockGasLimit < 1 {
return nil, errors.New("averageBlockGasLimit cannot be less than 1 gas")
}
if epochLengthSeconds < 1 {
return nil, errors.New("epochLengthSeconds cannot be less than 1 second")
}
return &GasPriceUpdater{
mu: new(sync.RWMutex),
gasPricer: gasPricer,
epochStartBlockNumber: epochStartBlockNumber,
epochLengthSeconds: epochLengthSeconds,
averageBlockGasLimit: averageBlockGasLimit,
getLatestBlockNumberFn: getLatestBlockNumberFn,
updateL2GasPriceFn: updateL2GasPriceFn,
}, nil
}
func (g *GasPriceUpdater) UpdateGasPrice() error {
g.mu.Lock()
defer g.mu.Unlock()
latestBlockNumber, err := g.getLatestBlockNumberFn()
if err != nil {
return err
}
if latestBlockNumber < uint64(g.epochStartBlockNumber) {
return errors.New("Latest block number less than the last epoch's block number")
}
averageGasPerSecond := GetAverageGasPerSecond(
g.epochStartBlockNumber,
latestBlockNumber,
uint64(g.epochLengthSeconds),
uint64(g.averageBlockGasLimit),
)
log.Debug("UpdateGasPrice", "averageGasPerSecond", averageGasPerSecond, "current-price", g.gasPricer.curPrice)
_, err = g.gasPricer.CompleteEpoch(averageGasPerSecond)
if err != nil {
return err
}
g.epochStartBlockNumber = latestBlockNumber
err = g.updateL2GasPriceFn(g.gasPricer.curPrice)
if err != nil {
return err
}
return nil
}
func (g *GasPriceUpdater) GetGasPrice() uint64 {
g.mu.RLock()
defer g.mu.RUnlock()
return g.gasPricer.curPrice
}
package gasprices
import (
"testing"
)
type MockEpoch struct {
numBlocks uint64
repeatCount uint64
postHook func(prevGasPrice uint64, gasPriceUpdater *GasPriceUpdater)
}
func TestGetAverageGasPerSecond(t *testing.T) {
// Let's sanity check this function with some simple inputs.
// A 10 block epoch
epochStartBlockNumber := 10
latestBlockNumber := 20
// That lasts 10 seconds (1 block per second)
epochLengthSeconds := 10
// And each block has a gas limit of 1
averageBlockGasLimit := 1
// We expect a gas per second to be 1!
expectedGps := 1.0
gps := GetAverageGasPerSecond(uint64(epochStartBlockNumber), uint64(latestBlockNumber), uint64(epochLengthSeconds), uint64(averageBlockGasLimit))
if gps != expectedGps {
t.Fatalf("Gas per second not calculated correctly. Got: %v expected: %v", gps, expectedGps)
}
}
// Return a gas pricer that targets 3 blocks per epoch & 10% max change per epoch.
func makeTestGasPricerAndUpdater(curPrice uint64) (*GasPricer, *GasPriceUpdater, func(uint64), error) {
gpsTarget := 3300000.0
getGasTarget := func() float64 { return gpsTarget }
epochLengthSeconds := uint64(10)
averageBlockGasLimit := 11000000.0
// Based on our 10 second epoch, we are targetting 3 blocks per epoch.
gasPricer, err := NewGasPricer(curPrice, 1, getGasTarget, 10)
if err != nil {
return nil, nil, nil, err
}
curBlock := uint64(10)
incrementCurrentBlock := func(newBlockNum uint64) { curBlock += newBlockNum }
getLatestBlockNumber := func() (uint64, error) { return curBlock, nil }
updateL2GasPrice := func(x uint64) error {
return nil
}
startBlock, _ := getLatestBlockNumber()
gasUpdater, err := NewGasPriceUpdater(
gasPricer,
startBlock,
averageBlockGasLimit,
epochLengthSeconds,
getLatestBlockNumber,
updateL2GasPrice,
)
if err != nil {
return nil, nil, nil, err
}
return gasPricer, gasUpdater, incrementCurrentBlock, nil
}
func TestUpdateGasPriceCallsUpdateL2GasPriceFn(t *testing.T) {
_, gasUpdater, incrementCurrentBlock, err := makeTestGasPricerAndUpdater(1)
if err != nil {
t.Fatal(err)
}
wasCalled := false
gasUpdater.updateL2GasPriceFn = func(gasPrice uint64) error {
wasCalled = true
return nil
}
incrementCurrentBlock(3)
if err := gasUpdater.UpdateGasPrice(); err != nil {
t.Fatal(err)
}
if wasCalled != true {
t.Fatalf("Expected updateL2GasPrice to be called.")
}
}
func TestUpdateGasPriceCorrectlyUpdatesAZeroBlockEpoch(t *testing.T) {
gasPricer, gasUpdater, _, err := makeTestGasPricerAndUpdater(100)
if err != nil {
t.Fatal(err)
}
gasPriceBefore := gasPricer.curPrice
gasPriceAfter := gasPricer.curPrice
gasUpdater.updateL2GasPriceFn = func(gasPrice uint64) error {
gasPriceAfter = gasPrice
return nil
}
if err := gasUpdater.UpdateGasPrice(); err != nil {
t.Fatal(err)
}
if gasPriceBefore < gasPriceAfter {
t.Fatalf("Expected gasPrice to go down because we had fewer than 3 blocks in the epoch.")
}
}
func TestUpdateGasPriceFailsIfBlockNumberGoesBackwards(t *testing.T) {
_, gasUpdater, _, err := makeTestGasPricerAndUpdater(1)
if err != nil {
t.Fatal(err)
}
gasUpdater.epochStartBlockNumber = 10
gasUpdater.getLatestBlockNumberFn = func() (uint64, error) { return 0, nil }
err = gasUpdater.UpdateGasPrice()
if err == nil {
t.Fatalf("Expected UpdateGasPrice to fail when block number goes backwards.")
}
}
func TestUsageOfGasPriceUpdater(t *testing.T) {
_, gasUpdater, incrementCurrentBlock, err := makeTestGasPricerAndUpdater(1000)
if err != nil {
t.Fatal(err)
}
// In these mock epochs the gas price shold go up and then down again after the time has passed
mockEpochs := []MockEpoch{
// First jack up the price to show that it will grow over time
MockEpoch{
numBlocks: 10,
repeatCount: 3,
// Make sure the gas price is increasing
postHook: func(prevGasPrice uint64, gasPriceUpdater *GasPriceUpdater) {
curPrice := gasPriceUpdater.gasPricer.curPrice
if prevGasPrice >= curPrice {
t.Fatalf("Expected gas price to increase.")
}
},
},
// Then stabilize around the GPS we want
MockEpoch{
numBlocks: 3,
repeatCount: 5,
postHook: func(prevGasPrice uint64, gasPriceUpdater *GasPriceUpdater) {},
},
MockEpoch{
numBlocks: 3,
repeatCount: 0,
postHook: func(prevGasPrice uint64, gasPriceUpdater *GasPriceUpdater) {
curPrice := gasPriceUpdater.gasPricer.curPrice
if prevGasPrice != curPrice {
t.Fatalf("Expected gas price to stablize.")
}
},
},
// Then reduce the demand to show the fee goes back down to the floor
MockEpoch{
numBlocks: 1,
repeatCount: 5,
postHook: func(prevGasPrice uint64, gasPriceUpdater *GasPriceUpdater) {
curPrice := gasPriceUpdater.gasPricer.curPrice
if prevGasPrice <= curPrice && curPrice != gasPriceUpdater.gasPricer.floorPrice {
t.Fatalf("Expected gas price either reduce or be at the floor.")
}
},
},
}
loop := func(epoch MockEpoch) {
prevGasPrice := gasUpdater.gasPricer.curPrice
incrementCurrentBlock(epoch.numBlocks)
err = gasUpdater.UpdateGasPrice()
if err != nil {
t.Fatal(err)
}
epoch.postHook(prevGasPrice, gasUpdater)
}
for _, epoch := range mockEpochs {
for i := 0; i < int(epoch.repeatCount)+1; i++ {
loop(epoch)
}
}
}
package gasprices
import (
"errors"
"fmt"
"math"
"github.com/ethereum/go-ethereum/log"
)
type GetTargetGasPerSecond func() float64
type GasPricer struct {
curPrice uint64
floorPrice uint64
getTargetGasPerSecond GetTargetGasPerSecond
maxChangePerEpoch float64
}
// LinearInterpolation can be used to dynamically update target gas per second
func GetLinearInterpolationFn(getX func() float64, x1 float64, x2 float64, y1 float64, y2 float64) func() float64 {
return func() float64 {
return y1 + ((getX()-x1)/(x2-x1))*(y2-y1)
}
}
// NewGasPricer creates a GasPricer and checks its config beforehand
func NewGasPricer(curPrice, floorPrice uint64, getTargetGasPerSecond GetTargetGasPerSecond, maxPercentChangePerEpoch float64) (*GasPricer, error) {
if floorPrice < 1 {
return nil, errors.New("floorPrice must be greater than or equal to 1")
}
if maxPercentChangePerEpoch <= 0 {
return nil, errors.New("maxPercentChangePerEpoch must be between (0,100]")
}
return &GasPricer{
curPrice: max(curPrice, floorPrice),
floorPrice: floorPrice,
getTargetGasPerSecond: getTargetGasPerSecond,
maxChangePerEpoch: maxPercentChangePerEpoch,
}, nil
}
// CalcNextEpochGasPrice calculates the next gas price given some average
// gas per second over the last epoch
func (p *GasPricer) CalcNextEpochGasPrice(avgGasPerSecondLastEpoch float64) (uint64, error) {
targetGasPerSecond := p.getTargetGasPerSecond()
if avgGasPerSecondLastEpoch < 0 {
return 0.0, fmt.Errorf("avgGasPerSecondLastEpoch cannot be negative, got %f", avgGasPerSecondLastEpoch)
}
if targetGasPerSecond < 1 {
return 0.0, fmt.Errorf("gasPerSecond cannot be less than 1, got %f", targetGasPerSecond)
}
// The percent difference between our current average gas & our target gas
proportionOfTarget := avgGasPerSecondLastEpoch / targetGasPerSecond
// The percent that we should adjust the gas price to reach our target gas
proportionToChangeBy := 0.0
if proportionOfTarget >= 1 { // If average avgGasPerSecondLastEpoch is GREATER than our target
proportionToChangeBy = math.Min(proportionOfTarget, 1+p.maxChangePerEpoch)
} else {
proportionToChangeBy = math.Max(proportionOfTarget, 1-p.maxChangePerEpoch)
}
log.Debug("CalcNextEpochGasPrice", "proportionToChangeBy", proportionToChangeBy, "proportionOfTarget", proportionOfTarget)
updated := float64(max(1, p.curPrice)) * proportionToChangeBy
return max(p.floorPrice, uint64(math.Ceil(updated))), nil
}
// CompleteEpoch ends the current epoch and updates the current gas price for the next epoch
func (p *GasPricer) CompleteEpoch(avgGasPerSecondLastEpoch float64) (uint64, error) {
gp, err := p.CalcNextEpochGasPrice(avgGasPerSecondLastEpoch)
if err != nil {
return gp, err
}
p.curPrice = gp
return gp, nil
}
func max(a, b uint64) uint64 {
if a >= b {
return a
}
return b
}
package gasprices
import (
"math"
"testing"
)
type CalcGasPriceTestCase struct {
name string
avgGasPerSecondLastEpoch float64
expectedNextGasPrice uint64
}
func returnConstFn(retVal uint64) func() float64 {
return func() float64 { return float64(retVal) }
}
func runCalcGasPriceTests(gp GasPricer, tcs []CalcGasPriceTestCase, t *testing.T) {
for _, tc := range tcs {
nextEpochGasPrice, err := gp.CalcNextEpochGasPrice(tc.avgGasPerSecondLastEpoch)
if tc.expectedNextGasPrice != nextEpochGasPrice || err != nil {
t.Fatalf("failed on test: %s", tc.name)
}
}
}
func TestCalcGasPriceFarFromFloor(t *testing.T) {
gp := GasPricer{
curPrice: 100,
floorPrice: 1,
getTargetGasPerSecond: returnConstFn(10),
maxChangePerEpoch: 0.5,
}
tcs := []CalcGasPriceTestCase{
// No change
{
name: "No change expected when already at target",
avgGasPerSecondLastEpoch: 10,
expectedNextGasPrice: 100,
},
// Price reduction
{
name: "Max % change bounds the reduction in price",
avgGasPerSecondLastEpoch: 1,
expectedNextGasPrice: 50,
},
{
// We're half of our target, so reduce by half
name: "Reduce fee by half if at 50% capacity",
avgGasPerSecondLastEpoch: 5,
expectedNextGasPrice: 50,
},
{
name: "Reduce fee by 75% if at 75% capacity",
avgGasPerSecondLastEpoch: 7.5,
expectedNextGasPrice: 75,
},
// Price increase
{
name: "Max % change bounds the increase in price",
avgGasPerSecondLastEpoch: 100,
expectedNextGasPrice: 150,
},
{
name: "Increase fee by 25% if at 125% capacity",
avgGasPerSecondLastEpoch: 12.5,
expectedNextGasPrice: 125,
},
}
runCalcGasPriceTests(gp, tcs, t)
}
func TestCalcGasPriceAtFloor(t *testing.T) {
gp := GasPricer{
curPrice: 100,
floorPrice: 100,
getTargetGasPerSecond: returnConstFn(10),
maxChangePerEpoch: 0.5,
}
tcs := []CalcGasPriceTestCase{
// No change
{
name: "No change expected when already at target",
avgGasPerSecondLastEpoch: 10,
expectedNextGasPrice: 100,
},
// Price reduction
{
name: "No change expected when at floorPrice",
avgGasPerSecondLastEpoch: 1,
expectedNextGasPrice: 100,
},
// Price increase
{
name: "Max % change bounds the increase in price",
avgGasPerSecondLastEpoch: 100,
expectedNextGasPrice: 150,
},
}
runCalcGasPriceTests(gp, tcs, t)
}
func TestGasPricerUpdates(t *testing.T) {
gp := GasPricer{
curPrice: 100,
floorPrice: 100,
getTargetGasPerSecond: returnConstFn(10),
maxChangePerEpoch: 0.5,
}
_, err := gp.CompleteEpoch(12.5)
if err != nil {
t.Fatal(err)
}
if gp.curPrice != 125 {
t.Fatalf("gp.curPrice not updated correctly. Got: %v, expected: %v", gp.curPrice, 125)
}
}
func TestGetLinearInterpolationFn(t *testing.T) {
mockTimestamp := float64(0) // start at timestamp 0
// TargetGasPerSecond is dynamic based on the current "mocktimestamp"
mockTimeNow := func() float64 {
return mockTimestamp
}
l := GetLinearInterpolationFn(mockTimeNow, 0, 10, 0, 100)
for expected := 0.0; expected < 100; expected += 10 {
mockTimestamp = expected / 10 // To prove this is not identity function, divide by 10
got := l()
if got != expected {
t.Fatalf("linear interpolation incorrect. Got: %v expected: %v", got, expected)
}
}
}
func TestGasPricerDynamicTarget(t *testing.T) {
// In prod we will be committing to a gas per second schedule in order to
// meter usage over time. This linear interpolation between a start time, end time,
// start gas per second, and end gas per second is an example of how we can introduce
// acceleration in our gas pricer
startTimestamp := float64(0)
startGasPerSecond := float64(10)
endTimestamp := float64(100)
endGasPerSecond := float64(100)
mockTimestamp := float64(0) // start at timestamp 0
// TargetGasPerSecond is dynamic based on the current "mocktimestamp"
mockTimeNow := func() float64 {
return mockTimestamp
}
// TargetGasPerSecond is dynamic based on the current "mocktimestamp"
dynamicGetTarget := GetLinearInterpolationFn(mockTimeNow, startTimestamp, endTimestamp, startGasPerSecond, endGasPerSecond)
gp := GasPricer{
curPrice: 100,
floorPrice: 1,
getTargetGasPerSecond: dynamicGetTarget,
maxChangePerEpoch: 0.5,
}
gasPerSecondDemanded := returnConstFn(15)
for i := 0; i < 10; i++ {
mockTimestamp = float64(i * 10)
expectedPrice := math.Ceil(float64(gp.curPrice) * math.Max(0.5, gasPerSecondDemanded()/dynamicGetTarget()))
_, err := gp.CompleteEpoch(gasPerSecondDemanded())
if err != nil {
t.Fatal(err)
}
if gp.curPrice != uint64(expectedPrice) {
t.Fatalf("gp.curPrice not updated correctly. Got: %v expected: %v", gp.curPrice, expectedPrice)
}
}
}
module github.com/ethereum-optimism/optimism/go/gas-oracle
go 1.16
require (
github.com/ethereum/go-ethereum v1.10.4
github.com/urfave/cli v1.20.0
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
)
This diff is collapsed.
package main
import (
"fmt"
"os"
"time"
"github.com/ethereum-optimism/optimism/go/gas-oracle/flags"
ometrics "github.com/ethereum-optimism/optimism/go/gas-oracle/metrics"
"github.com/ethereum-optimism/optimism/go/gas-oracle/oracle"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics/influxdb"
"github.com/ethereum/go-ethereum/params"
"github.com/urfave/cli"
)
var (
GitVersion = ""
GitCommit = ""
GitDate = ""
)
func main() {
app := cli.NewApp()
app.Flags = flags.Flags
app.Version = GitVersion + "-" + params.VersionWithCommit(GitCommit, GitDate)
app.Name = "gas-oracle"
app.Usage = "Remotely Control the Optimistic Ethereum Gas Price"
app.Description = "Configure with a private key and an Optimistic Ethereum HTTP endpoint " +
"to send transactions that update the L2 gas price."
// Configure the logging
app.Before = func(ctx *cli.Context) error {
loglevel := ctx.GlobalUint64(flags.LogLevelFlag.Name)
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(loglevel), log.StreamHandler(os.Stdout, log.TerminalFormat(true))))
return nil
}
// Define the functionality of the application
app.Action = func(ctx *cli.Context) error {
if args := ctx.Args(); len(args) > 0 {
return fmt.Errorf("invalid command: %q", args[0])
}
config := oracle.NewConfig(ctx)
gpo, err := oracle.NewGasPriceOracle(config)
if err != nil {
return err
}
if err := gpo.Start(); err != nil {
return err
}
if config.MetricsEnabled {
address := fmt.Sprintf("%s:%d", config.MetricsHTTP, config.MetricsPort)
log.Info("Enabling stand-alone metrics HTTP endpoint", "address", address)
ometrics.Setup(address)
}
if config.MetricsEnableInfluxDB {
endpoint := config.MetricsInfluxDBEndpoint
database := config.MetricsInfluxDBDatabase
username := config.MetricsInfluxDBUsername
password := config.MetricsInfluxDBPassword
log.Info("Enabling metrics export to InfluxDB", "endpoint", endpoint, "username", username, "database", database)
go influxdb.InfluxDBWithTags(ometrics.DefaultRegistry, 10*time.Second, endpoint, database, username, password, "geth.", make(map[string]string))
}
gpo.Wait()
return nil
}
err := app.Run(os.Args)
if err != nil {
log.Crit("application failed", "message", err)
}
}
package metrics
// Do not edit this file as it was copied from go-ethereum
import (
"expvar"
"fmt"
"net/http"
"sync"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/metrics/prometheus"
)
var DefaultRegistry = metrics.NewRegistry()
type exp struct {
expvarLock sync.Mutex // expvar panics if you try to register the same var twice, so we must probe it safely
registry metrics.Registry
}
func (exp *exp) expHandler(w http.ResponseWriter, r *http.Request) {
// load our variables into expvar
exp.syncToExpvar()
// now just run the official expvar handler code (which is not publicly callable, so pasted inline)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
fmt.Fprintf(w, "{\n")
first := true
expvar.Do(func(kv expvar.KeyValue) {
if !first {
fmt.Fprintf(w, ",\n")
}
first = false
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
})
fmt.Fprintf(w, "\n}\n")
}
// Exp will register an expvar powered metrics handler with http.DefaultServeMux on "/debug/vars"
func Exp(r metrics.Registry) {
h := ExpHandler(r)
// this would cause a panic:
// panic: http: multiple registrations for /debug/vars
// http.HandleFunc("/debug/vars", e.expHandler)
// haven't found an elegant way, so just use a different endpoint
http.Handle("/debug/metrics", h)
http.Handle("/debug/metrics/prometheus", prometheus.Handler(r))
}
// ExpHandler will return an expvar powered metrics handler.
func ExpHandler(r metrics.Registry) http.Handler {
e := exp{sync.Mutex{}, r}
return http.HandlerFunc(e.expHandler)
}
// Setup starts a dedicated metrics server at the given address.
// This function enables metrics reporting separate from pprof.
func Setup(address string) {
m := http.NewServeMux()
m.Handle("/debug/metrics", ExpHandler(DefaultRegistry))
m.Handle("/debug/metrics/prometheus", prometheus.Handler(DefaultRegistry))
log.Info("Starting metrics server", "addr", fmt.Sprintf("http://%s/debug/metrics", address))
go func() {
if err := http.ListenAndServe(address, m); err != nil {
log.Error("Failure in running metrics server", "err", err)
}
}()
}
func (exp *exp) getInt(name string) *expvar.Int {
var v *expvar.Int
exp.expvarLock.Lock()
p := expvar.Get(name)
if p != nil {
v = p.(*expvar.Int)
} else {
v = new(expvar.Int)
expvar.Publish(name, v)
}
exp.expvarLock.Unlock()
return v
}
func (exp *exp) getFloat(name string) *expvar.Float {
var v *expvar.Float
exp.expvarLock.Lock()
p := expvar.Get(name)
if p != nil {
v = p.(*expvar.Float)
} else {
v = new(expvar.Float)
expvar.Publish(name, v)
}
exp.expvarLock.Unlock()
return v
}
func (exp *exp) publishCounter(name string, metric metrics.Counter) {
v := exp.getInt(name)
v.Set(metric.Count())
}
func (exp *exp) publishGauge(name string, metric metrics.Gauge) {
v := exp.getInt(name)
v.Set(metric.Value())
}
func (exp *exp) publishGaugeFloat64(name string, metric metrics.GaugeFloat64) {
exp.getFloat(name).Set(metric.Value())
}
func (exp *exp) publishHistogram(name string, metric metrics.Histogram) {
h := metric.Snapshot()
ps := h.Percentiles([]float64{0.5, 0.75, 0.95, 0.99, 0.999})
exp.getInt(name + ".count").Set(h.Count())
exp.getFloat(name + ".min").Set(float64(h.Min()))
exp.getFloat(name + ".max").Set(float64(h.Max()))
exp.getFloat(name + ".mean").Set(h.Mean())
exp.getFloat(name + ".std-dev").Set(h.StdDev())
exp.getFloat(name + ".50-percentile").Set(ps[0])
exp.getFloat(name + ".75-percentile").Set(ps[1])
exp.getFloat(name + ".95-percentile").Set(ps[2])
exp.getFloat(name + ".99-percentile").Set(ps[3])
exp.getFloat(name + ".999-percentile").Set(ps[4])
}
func (exp *exp) publishMeter(name string, metric metrics.Meter) {
m := metric.Snapshot()
exp.getInt(name + ".count").Set(m.Count())
exp.getFloat(name + ".one-minute").Set(m.Rate1())
exp.getFloat(name + ".five-minute").Set(m.Rate5())
exp.getFloat(name + ".fifteen-minute").Set(m.Rate15())
exp.getFloat(name + ".mean").Set(m.RateMean())
}
func (exp *exp) publishTimer(name string, metric metrics.Timer) {
t := metric.Snapshot()
ps := t.Percentiles([]float64{0.5, 0.75, 0.95, 0.99, 0.999})
exp.getInt(name + ".count").Set(t.Count())
exp.getFloat(name + ".min").Set(float64(t.Min()))
exp.getFloat(name + ".max").Set(float64(t.Max()))
exp.getFloat(name + ".mean").Set(t.Mean())
exp.getFloat(name + ".std-dev").Set(t.StdDev())
exp.getFloat(name + ".50-percentile").Set(ps[0])
exp.getFloat(name + ".75-percentile").Set(ps[1])
exp.getFloat(name + ".95-percentile").Set(ps[2])
exp.getFloat(name + ".99-percentile").Set(ps[3])
exp.getFloat(name + ".999-percentile").Set(ps[4])
exp.getFloat(name + ".one-minute").Set(t.Rate1())
exp.getFloat(name + ".five-minute").Set(t.Rate5())
exp.getFloat(name + ".fifteen-minute").Set(t.Rate15())
exp.getFloat(name + ".mean-rate").Set(t.RateMean())
}
func (exp *exp) publishResettingTimer(name string, metric metrics.ResettingTimer) {
t := metric.Snapshot()
ps := t.Percentiles([]float64{50, 75, 95, 99})
exp.getInt(name + ".count").Set(int64(len(t.Values())))
exp.getFloat(name + ".mean").Set(t.Mean())
exp.getInt(name + ".50-percentile").Set(ps[0])
exp.getInt(name + ".75-percentile").Set(ps[1])
exp.getInt(name + ".95-percentile").Set(ps[2])
exp.getInt(name + ".99-percentile").Set(ps[3])
}
func (exp *exp) syncToExpvar() {
exp.registry.Each(func(name string, i interface{}) {
switch i := i.(type) {
case metrics.Counter:
exp.publishCounter(name, i)
case metrics.Gauge:
exp.publishGauge(name, i)
case metrics.GaugeFloat64:
exp.publishGaugeFloat64(name, i)
case metrics.Histogram:
exp.publishHistogram(name, i)
case metrics.Meter:
exp.publishMeter(name, i)
case metrics.Timer:
exp.publishTimer(name, i)
case metrics.ResettingTimer:
exp.publishResettingTimer(name, i)
default:
panic(fmt.Sprintf("unsupported type for '%s': %T", name, i))
}
})
}
package oracle
import (
"crypto/ecdsa"
"fmt"
"math/big"
"strings"
"github.com/ethereum-optimism/optimism/go/gas-oracle/flags"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli"
)
// Config represents the configuration options for the gas oracle
type Config struct {
chainID *big.Int
ethereumHttpUrl string
gasPriceOracleAddress common.Address
privateKey *ecdsa.PrivateKey
gasPrice *big.Int
waitForReceipt bool
floorPrice uint64
targetGasPerSecond uint64
maxPercentChangePerEpoch float64
averageBlockGasLimitPerEpoch float64
epochLengthSeconds uint64
significanceFactor float64
// Metrics config
MetricsEnabled bool
MetricsHTTP string
MetricsPort int
MetricsEnableInfluxDB bool
MetricsInfluxDBEndpoint string
MetricsInfluxDBDatabase string
MetricsInfluxDBUsername string
MetricsInfluxDBPassword string
}
// NewConfig creates a new Config
func NewConfig(ctx *cli.Context) *Config {
cfg := Config{}
cfg.ethereumHttpUrl = ctx.GlobalString(flags.EthereumHttpUrlFlag.Name)
addr := ctx.GlobalString(flags.GasPriceOracleAddressFlag.Name)
cfg.gasPriceOracleAddress = common.HexToAddress(addr)
cfg.targetGasPerSecond = ctx.GlobalUint64(flags.TargetGasPerSecondFlag.Name)
cfg.maxPercentChangePerEpoch = ctx.GlobalFloat64(flags.MaxPercentChangePerEpochFlag.Name)
cfg.averageBlockGasLimitPerEpoch = ctx.GlobalFloat64(flags.AverageBlockGasLimitPerEpochFlag.Name)
cfg.epochLengthSeconds = ctx.GlobalUint64(flags.EpochLengthSecondsFlag.Name)
cfg.significanceFactor = ctx.GlobalFloat64(flags.SignificanceFactorFlag.Name)
cfg.floorPrice = ctx.GlobalUint64(flags.FloorPriceFlag.Name)
if ctx.GlobalIsSet(flags.PrivateKeyFlag.Name) {
hex := ctx.GlobalString(flags.PrivateKeyFlag.Name)
hex = strings.TrimPrefix(hex, "0x")
key, err := crypto.HexToECDSA(hex)
if err != nil {
log.Error(fmt.Sprintf("Option %q: %v", flags.PrivateKeyFlag.Name, err))
}
cfg.privateKey = key
} else {
log.Crit("No private key configured")
}
if ctx.GlobalIsSet(flags.ChainIDFlag.Name) {
chainID := ctx.GlobalUint64(flags.ChainIDFlag.Name)
cfg.chainID = new(big.Int).SetUint64(chainID)
}
if ctx.GlobalIsSet(flags.TransactionGasPriceFlag.Name) {
gasPrice := ctx.GlobalUint64(flags.TransactionGasPriceFlag.Name)
cfg.gasPrice = new(big.Int).SetUint64(gasPrice)
}
if ctx.GlobalIsSet(flags.WaitForReceiptFlag.Name) {
cfg.waitForReceipt = true
}
cfg.MetricsEnabled = ctx.GlobalBool(flags.MetricsEnabledFlag.Name)
cfg.MetricsHTTP = ctx.GlobalString(flags.MetricsHTTPFlag.Name)
cfg.MetricsPort = ctx.GlobalInt(flags.MetricsPortFlag.Name)
cfg.MetricsEnableInfluxDB = ctx.GlobalBool(flags.MetricsEnableInfluxDBFlag.Name)
cfg.MetricsInfluxDBEndpoint = ctx.GlobalString(flags.MetricsInfluxDBEndpointFlag.Name)
cfg.MetricsInfluxDBDatabase = ctx.GlobalString(flags.MetricsInfluxDBDatabaseFlag.Name)
cfg.MetricsInfluxDBUsername = ctx.GlobalString(flags.MetricsInfluxDBUsernameFlag.Name)
cfg.MetricsInfluxDBPassword = ctx.GlobalString(flags.MetricsInfluxDBPasswordFlag.Name)
return &cfg
}
package oracle
import (
"context"
"errors"
"fmt"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/go/gas-oracle/bindings"
"github.com/ethereum-optimism/optimism/go/gas-oracle/gasprices"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
// errInvalidSigningKey represents the error when the signing key used
// is not the Owner of the contract and therefore cannot update the gasprice
var errInvalidSigningKey = errors.New("invalid signing key")
// errNoChainID represents the error when the chain id is not provided
// and it cannot be remotely fetched
var errNoChainID = errors.New("no chain id provided")
// errNoPrivateKey represents the error when the private key is not provided to
// the application
var errNoPrivateKey = errors.New("no private key provided")
// errWrongChainID represents the error when the configured chain id is not
// correct
var errWrongChainID = errors.New("wrong chain id provided")
// GasPriceOracle manages a hot key that can update the L2 Gas Price
type GasPriceOracle struct {
chainID *big.Int
ctx context.Context
stop chan struct{}
contract *bindings.GasPriceOracle
backend DeployContractBackend
gasPriceUpdater *gasprices.GasPriceUpdater
config *Config
}
// Start runs the GasPriceOracle
func (g *GasPriceOracle) Start() error {
if g.config.chainID == nil {
return errNoChainID
}
if g.config.privateKey == nil {
return errNoPrivateKey
}
address := crypto.PubkeyToAddress(g.config.privateKey.PublicKey)
log.Info("Starting Gas Price Oracle", "chain-id", g.chainID, "address", address.Hex())
price, err := g.contract.GasPrice(&bind.CallOpts{
Context: context.Background(),
})
if err != nil {
return err
}
gasPriceGauge.Update(int64(price.Uint64()))
go g.Loop()
return nil
}
func (g *GasPriceOracle) Stop() {
close(g.stop)
}
func (g *GasPriceOracle) Wait() {
<-g.stop
}
// ensure makes sure that the configured private key is the owner
// of the `OVM_GasPriceOracle`. If it is not the owner, then it will
// not be able to make updates to the L2 gas price.
func (g *GasPriceOracle) ensure() error {
owner, err := g.contract.Owner(&bind.CallOpts{
Context: g.ctx,
})
if err != nil {
return err
}
address := crypto.PubkeyToAddress(g.config.privateKey.PublicKey)
if address != owner {
log.Error("Signing key does not match contract owner", "signer", address.Hex(), "owner", owner.Hex())
return errInvalidSigningKey
}
return nil
}
// Loop is the main logic of the gas-oracle
func (g *GasPriceOracle) Loop() {
timer := time.NewTicker(time.Duration(g.config.epochLengthSeconds) * time.Second)
for {
select {
case <-timer.C:
log.Trace("polling", "time", time.Now())
if err := g.Update(); err != nil {
log.Error("cannot update gas price", "message", err)
}
case <-g.ctx.Done():
g.Stop()
}
}
}
// Update will update the gas price
func (g *GasPriceOracle) Update() error {
l2GasPrice, err := g.contract.GasPrice(&bind.CallOpts{
Context: g.ctx,
})
if err != nil {
return fmt.Errorf("cannot get gas price: %w", err)
}
if err := g.gasPriceUpdater.UpdateGasPrice(); err != nil {
return fmt.Errorf("cannot update gas price: %w", err)
}
newGasPrice, err := g.contract.GasPrice(&bind.CallOpts{
Context: g.ctx,
})
if err != nil {
return fmt.Errorf("cannot get gas price: %w", err)
}
local := g.gasPriceUpdater.GetGasPrice()
log.Info("Update", "original", l2GasPrice, "current", newGasPrice, "local", local)
return nil
}
// NewGasPriceOracle creates a new GasPriceOracle based on a Config
func NewGasPriceOracle(cfg *Config) (*GasPriceOracle, error) {
client, err := ethclient.Dial(cfg.ethereumHttpUrl)
if err != nil {
return nil, err
}
// Ensure that we can actually connect
t := time.NewTicker(5 * time.Second)
for ; true; <-t.C {
_, err := client.ChainID(context.Background())
if err == nil {
t.Stop()
break
}
log.Error("Unable to connect to remote node", "addr", cfg.ethereumHttpUrl)
}
address := cfg.gasPriceOracleAddress
contract, err := bindings.NewGasPriceOracle(address, client)
if err != nil {
return nil, err
}
// Fetch the current gas price to use as the current price
currentPrice, err := contract.GasPrice(&bind.CallOpts{
Context: context.Background(),
})
if err != nil {
return nil, err
}
// Create a gas pricer for the gas price updater
log.Info("Creating GasPricer", "currentPrice", currentPrice,
"floorPrice", cfg.floorPrice, "targetGasPerSecond", cfg.targetGasPerSecond,
"maxPercentChangePerEpoch", cfg.maxPercentChangePerEpoch)
gasPricer, err := gasprices.NewGasPricer(
currentPrice.Uint64(),
cfg.floorPrice,
func() float64 {
return float64(cfg.targetGasPerSecond)
},
cfg.maxPercentChangePerEpoch,
)
if err != nil {
return nil, err
}
chainID, err := client.ChainID(context.Background())
if err != nil {
return nil, err
}
// If the chainid is passed in, exit if the chain id is
// not correct
if cfg.chainID != nil {
if cfg.chainID.Cmp(chainID) != 0 {
return nil, fmt.Errorf("%w: configured with %d and got %d", errWrongChainID, cfg.chainID, chainID)
}
} else {
cfg.chainID = chainID
}
if cfg.privateKey == nil {
return nil, errNoPrivateKey
}
tip, err := client.HeaderByNumber(context.Background(), nil)
if err != nil {
return nil, err
}
// Start at the tip
epochStartBlockNumber := tip.Number.Uint64()
// getLatestBlockNumberFn is used by the GasPriceUpdater
// to get the latest block number
getLatestBlockNumberFn := wrapGetLatestBlockNumberFn(client)
// updateL2GasPriceFn is used by the GasPriceUpdater to
// update the gas price
updateL2GasPriceFn, err := wrapUpdateL2GasPriceFn(client, cfg)
if err != nil {
return nil, err
}
log.Info("Creating GasPriceUpdater", "epochStartBlockNumber", epochStartBlockNumber,
"averageBlockGasLimitPerEpoch", cfg.averageBlockGasLimitPerEpoch,
"epochLengthSeconds", cfg.epochLengthSeconds)
gasPriceUpdater, err := gasprices.NewGasPriceUpdater(
gasPricer,
epochStartBlockNumber,
cfg.averageBlockGasLimitPerEpoch,
cfg.epochLengthSeconds,
getLatestBlockNumberFn,
updateL2GasPriceFn,
)
if err != nil {
return nil, err
}
gpo := GasPriceOracle{
chainID: chainID,
ctx: context.Background(),
stop: make(chan struct{}),
contract: contract,
gasPriceUpdater: gasPriceUpdater,
config: cfg,
backend: client,
}
if err := gpo.ensure(); err != nil {
return nil, err
}
return &gpo, nil
}
package oracle
import (
"context"
"errors"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/go/gas-oracle/bindings"
ometrics "github.com/ethereum-optimism/optimism/go/gas-oracle/metrics"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
)
var (
txSendCounter = metrics.NewRegisteredCounter("tx/send", ometrics.DefaultRegistry)
txNotSignificantCounter = metrics.NewRegisteredCounter("tx/not-significant", ometrics.DefaultRegistry)
gasPriceGauge = metrics.NewRegisteredGauge("gas-price", ometrics.DefaultRegistry)
txConfTimer = metrics.NewRegisteredTimer("tx/confirmed", ometrics.DefaultRegistry)
txSendTimer = metrics.NewRegisteredTimer("tx/send", ometrics.DefaultRegistry)
)
// getLatestBlockNumberFn is used by the GasPriceUpdater
// to get the latest block number. The outer function binds the
// inner function to a `bind.ContractBackend` which is implemented
// by the `ethclient.Client`
func wrapGetLatestBlockNumberFn(backend bind.ContractBackend) func() (uint64, error) {
return func() (uint64, error) {
tip, err := backend.HeaderByNumber(context.Background(), nil)
if err != nil {
return 0, err
}
return tip.Number.Uint64(), nil
}
}
// DeployContractBackend represents the union of the
// DeployBackend and the ContractBackend
type DeployContractBackend interface {
bind.DeployBackend
bind.ContractBackend
}
// updateL2GasPriceFn is used by the GasPriceUpdater
// to update the L2 gas price
// perhaps this should take an options struct along with the backend?
// how can this continue to be decomposed?
func wrapUpdateL2GasPriceFn(backend DeployContractBackend, cfg *Config) (func(uint64) error, error) {
if cfg.privateKey == nil {
return nil, errNoPrivateKey
}
if cfg.chainID == nil {
return nil, errNoChainID
}
opts, err := bind.NewKeyedTransactorWithChainID(cfg.privateKey, cfg.chainID)
if err != nil {
return nil, err
}
// Once https://github.com/ethereum/go-ethereum/pull/23062 is released
// then we can remove setting the context here
if opts.Context == nil {
opts.Context = context.Background()
}
// Don't send the transaction using the `contract` so that we can inspect
// it beforehand
opts.NoSend = true
// Create a new contract bindings in scope of the updateL2GasPriceFn
// that is returned from this function
contract, err := bindings.NewGasPriceOracle(cfg.gasPriceOracleAddress, backend)
if err != nil {
return nil, err
}
return func(updatedGasPrice uint64) error {
log.Trace("UpdateL2GasPriceFn", "gas-price", updatedGasPrice)
if cfg.gasPrice == nil {
// Set the gas price manually to use legacy transactions
gasPrice, err := backend.SuggestGasPrice(context.Background())
if err != nil {
log.Error("cannot fetch gas price", "message", err)
return err
}
log.Trace("fetched L2 tx.gasPrice", "gas-price", gasPrice)
opts.GasPrice = gasPrice
} else {
// Allow a configurable gas price to be set
opts.GasPrice = cfg.gasPrice
}
// Query the current L2 gas price
currentPrice, err := contract.GasPrice(&bind.CallOpts{
Context: context.Background(),
})
if err != nil {
log.Error("cannot fetch current gas price", "message", err)
return err
}
// no need to update when they are the same
if currentPrice.Uint64() == updatedGasPrice {
log.Info("gas price did not change", "gas-price", updatedGasPrice)
txNotSignificantCounter.Inc(1)
return nil
}
// Only update the gas price when it must be changed by at least
// a paramaterizable amount.
if !isDifferenceSignificant(currentPrice.Uint64(), updatedGasPrice, cfg.significanceFactor) {
log.Info("gas price did not significantly change", "min-factor", cfg.significanceFactor,
"current-price", currentPrice, "next-price", updatedGasPrice)
txNotSignificantCounter.Inc(1)
return nil
}
// Set the gas price by sending a transaction
tx, err := contract.SetGasPrice(opts, new(big.Int).SetUint64(updatedGasPrice))
if err != nil {
return err
}
log.Debug("sending transaction", "tx.gasPrice", tx.GasPrice(), "tx.gasLimit", tx.Gas(),
"tx.data", hexutil.Encode(tx.Data()), "tx.to", tx.To().Hex(), "tx.nonce", tx.Nonce())
pre := time.Now()
if err := backend.SendTransaction(context.Background(), tx); err != nil {
return err
}
txSendTimer.Update(time.Since(pre))
log.Info("transaction sent", "hash", tx.Hash().Hex())
gasPriceGauge.Update(int64(updatedGasPrice))
txSendCounter.Inc(1)
if cfg.waitForReceipt {
// Keep track of the time it takes to confirm the transaction
pre := time.Now()
// Wait for the receipt
receipt, err := waitForReceipt(backend, tx)
if err != nil {
return err
}
txConfTimer.Update(time.Since(pre))
log.Info("transaction confirmed", "hash", tx.Hash().Hex(),
"gas-used", receipt.GasUsed, "blocknumber", receipt.BlockNumber)
}
return nil
}, nil
}
// Only update the gas price when it must be changed by at least
// a paramaterizable amount. If the param is greater than the result
// of 1 - (min/max) where min and max are the gas prices then do not
// update the gas price
func isDifferenceSignificant(a, b uint64, c float64) bool {
max := max(a, b)
min := min(a, b)
factor := 1 - (float64(min) / float64(max))
return c <= factor
}
// Wait for the receipt by polling the backend
func waitForReceipt(backend DeployContractBackend, tx *types.Transaction) (*types.Receipt, error) {
t := time.NewTicker(300 * time.Millisecond)
receipt := new(types.Receipt)
var err error
for range t.C {
receipt, err = backend.TransactionReceipt(context.Background(), tx.Hash())
if errors.Is(err, ethereum.NotFound) {
continue
}
if err != nil {
return nil, err
}
if receipt != nil {
t.Stop()
break
}
}
return receipt, nil
}
func max(a, b uint64) uint64 {
if a >= b {
return a
}
return b
}
func min(a, b uint64) uint64 {
if a >= b {
return b
}
return a
}
package oracle
import (
"context"
"crypto/ecdsa"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/go/gas-oracle/bindings"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
)
func TestWrapGetLatestBlockNumberFn(t *testing.T) {
key, _ := crypto.GenerateKey()
sim, db := newSimulatedBackend(key)
chain := sim.Blockchain()
getLatest := wrapGetLatestBlockNumberFn(sim)
// Generate a valid chain of 10 blocks
blocks, _ := core.GenerateChain(chain.Config(), chain.CurrentBlock(), chain.Engine(), db, 10, nil)
// Check that the latest is 0 to start
latest, err := getLatest()
if err != nil {
t.Fatal(err)
}
if latest != 0 {
t.Fatal("not zero")
}
// Insert the blocks one by one and assert that they are incrementing
for i, block := range blocks {
if _, err := chain.InsertChain([]*types.Block{block}); err != nil {
t.Fatal(err)
}
latest, err := getLatest()
if err != nil {
t.Fatal(err)
}
// Handle zero index by adding 1
if latest != uint64(i+1) {
t.Fatal("mismatch")
}
}
}
func TestWrapUpdateL2GasPriceFn(t *testing.T) {
key, _ := crypto.GenerateKey()
sim, _ := newSimulatedBackend(key)
opts, _ := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337))
addr, _, gpo, err := bindings.DeployGasPriceOracle(opts, sim, opts.From, big.NewInt(0))
if err != nil {
t.Fatal(err)
}
sim.Commit()
cfg := &Config{
privateKey: key,
chainID: big.NewInt(1337),
gasPriceOracleAddress: addr,
gasPrice: big.NewInt(676167759),
}
updateL2GasPriceFn, err := wrapUpdateL2GasPriceFn(sim, cfg)
if err != nil {
t.Fatal(err)
}
for i := uint64(0); i < 10; i++ {
err := updateL2GasPriceFn(i)
if err != nil {
t.Fatal(err)
}
sim.Commit()
gasPrice, err := gpo.GasPrice(&bind.CallOpts{Context: context.Background()})
if err != nil {
t.Fatal(err)
}
if gasPrice.Uint64() != i {
t.Fatal("mismatched gas price")
}
}
}
func TestWrapUpdateL2GasPriceFnNoUpdates(t *testing.T) {
key, _ := crypto.GenerateKey()
sim, _ := newSimulatedBackend(key)
opts, _ := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337))
// Deploy the contract
addr, _, gpo, err := bindings.DeployGasPriceOracle(opts, sim, opts.From, big.NewInt(0))
if err != nil {
t.Fatal(err)
}
sim.Commit()
cfg := &Config{
privateKey: key,
chainID: big.NewInt(1337),
gasPriceOracleAddress: addr,
gasPrice: big.NewInt(772763153),
// the new gas price must change be 50% for it to actually update
significanceFactor: 0.5,
}
updateL2GasPriceFn, err := wrapUpdateL2GasPriceFn(sim, cfg)
if err != nil {
t.Fatal(err)
}
// Create a function to do the assertions
tryUpdate := func(price uint64, shouldUpdate bool) {
// Get a reference to the original gas price
original, err := gpo.GasPrice(&bind.CallOpts{Context: context.Background()})
if err != nil {
t.Fatal(err)
}
// Call the updateL2GasPriceFn and commit the state
if err := updateL2GasPriceFn(price); err != nil {
t.Fatal(err)
}
sim.Commit()
// Get a reference to the potentially updated state
updated, err := gpo.GasPrice(&bind.CallOpts{Context: context.Background()})
if err != nil {
t.Fatal(err)
}
// the assertion differs depending on if it is expected that the
// update occurs or not
switch shouldUpdate {
case true:
// the price passed in should equal updated
if updated.Uint64() != price {
t.Fatalf("mismatched gas price, expect %d - got %d - should update (%t)", updated, price, shouldUpdate)
}
case false:
// the original should match the updated
if original.Uint64() != updated.Uint64() {
t.Fatalf("mismatched gas price, expect %d - got %d - should update (%t)", original, updated, shouldUpdate)
}
}
}
// tryUpdate(newGasPrice, shouldUpdate)
// The gas price starts out at 0
// try to update it to 0 and it should not update
tryUpdate(0, false)
// update it to 2 and it should update
tryUpdate(2, true)
// it should not update to 3
tryUpdate(3, false)
// it should update to 4
tryUpdate(4, true)
// it should not update back down to 3
tryUpdate(3, false)
// it should update to 1
tryUpdate(1, true)
}
func TestIsDifferenceSignificant(t *testing.T) {
tests := []struct {
name string
a uint64
b uint64
sig float64
expect bool
}{
{name: "test 1", a: 1, b: 1, sig: 0.05, expect: false},
{name: "test 2", a: 4, b: 1, sig: 0.25, expect: true},
{name: "test 3", a: 3, b: 1, sig: 0.1, expect: true},
{name: "test 4", a: 4, b: 1, sig: 0.9, expect: false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := isDifferenceSignificant(tc.a, tc.b, tc.sig)
if result != tc.expect {
t.Fatalf("mismatch %s", tc.name)
}
})
}
}
func newSimulatedBackend(key *ecdsa.PrivateKey) (*backends.SimulatedBackend, ethdb.Database) {
var gasLimit uint64 = 9_000_000
auth, _ := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337))
genAlloc := make(core.GenesisAlloc)
genAlloc[auth.From] = core.GenesisAccount{Balance: big.NewInt(9223372036854775807)}
db := rawdb.NewMemoryDatabase()
sim := backends.NewSimulatedBackendWithDatabase(db, genAlloc, gasLimit)
return sim, db
}
{
"name": "@eth-optimism/gas-oracle",
"version": "0.0.1",
"private": true,
"devDependencies": {}
}
...@@ -157,7 +157,7 @@ services: ...@@ -157,7 +157,7 @@ services:
ports: ports:
- ${VERIFIER_HTTP_PORT:-8547}:8545 - ${VERIFIER_HTTP_PORT:-8547}:8545
- ${VERIFIER_WS_PORT:-8548}:8546 - ${VERIFIER_WS_PORT:-8548}:8546
replica: replica:
depends_on: depends_on:
- dtl - dtl
...@@ -198,3 +198,14 @@ services: ...@@ -198,3 +198,14 @@ services:
ENABLE_GAS_REPORT: 1 ENABLE_GAS_REPORT: 1
NO_NETWORK: 1 NO_NETWORK: 1
gas_oracle:
image: ethereumoptimism/gas-oracle
deploy:
replicas: 0
build:
context: ..
dockerfile: ./ops/docker/Dockerfile.gas-oracle
entrypoint: ./gas-oracle.sh
environment:
GAS_PRICE_ORACLE_ETHEREUM_HTTP_URL: http://l2geth:8545
GAS_PRICE_ORACLE_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
FROM golang:1.15-alpine3.13 as builder
RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash
ADD ./go/gas-oracle /gas-oracle
RUN cd /gas-oracle && make gas-oracle
FROM alpine:3.13
RUN apk add --no-cache ca-certificates jq curl
COPY --from=builder /gas-oracle/gas-oracle /usr/local/bin/
COPY ./ops/scripts/gas-oracle.sh .
ENTRYPOINT ["gas-oracle"]
...@@ -7,6 +7,7 @@ ROLLUP_CLIENT_HTTP= ...@@ -7,6 +7,7 @@ ROLLUP_CLIENT_HTTP=
ROLLUP_STATE_DUMP_PATH= ROLLUP_STATE_DUMP_PATH=
ROLLUP_POLL_INTERVAL_FLAG=500ms ROLLUP_POLL_INTERVAL_FLAG=500ms
ROLLUP_ENABLE_L2_GAS_POLLING=true ROLLUP_ENABLE_L2_GAS_POLLING=true
ROLLUP_GAS_PRICE_ORACLE_OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
# ROLLUP_ENFORCE_FEES= # ROLLUP_ENFORCE_FEES=
ETHERBASE=0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf ETHERBASE=0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
......
#!/bin/sh
RETRIES=${RETRIES:-40}
if [[ -z $GAS_PRICE_ORACLE_ETHEREUM_HTTP_URL ]]; then
echo "Must set env GAS_PRICE_ORACLE_ETHEREUM_HTTP_URL"
exit 1
fi
# waits for l2geth to be up
curl --fail \
--show-error \
--silent \
--retry-connrefused \
--retry $RETRIES \
--retry-delay 1 \
--output /dev/null \
$GAS_PRICE_ORACLE_ETHEREUM_HTTP_URL
exec gas-oracle "$@"
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
"packages/*", "packages/*",
"l2geth", "l2geth",
"integration-tests", "integration-tests",
"specs" "specs",
"go/gas-oracle"
], ],
"nohoist": [ "nohoist": [
"examples/*" "examples/*"
......
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