Commit ce3c353b authored by Mark Tyneway's avatar Mark Tyneway

gas-oracle: implement and test

This commit adds the `gas-oracle` which is an offchain entity
that sends transactions to L2 to update the gas price. It must
be configured with a private key as the `OVM_GasPriceOracle`
is owned.

The `gas-oracle` is added to the changesets setup.

Tests are included as well as CI. Dockerizing will happen
in a follow up PR.
parent 93a3d5a5
---
'@eth-optimism/gas-oracle': patch
---
Initial implementation of the `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
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,
}
...@@ -36,7 +36,7 @@ func NewGasPricer(curPrice, floorPrice uint64, getTargetGasPerSecond GetTargetGa ...@@ -36,7 +36,7 @@ func NewGasPricer(curPrice, floorPrice uint64, getTargetGasPerSecond GetTargetGa
curPrice: max(curPrice, floorPrice), curPrice: max(curPrice, floorPrice),
floorPrice: floorPrice, floorPrice: floorPrice,
getTargetGasPerSecond: getTargetGasPerSecond, getTargetGasPerSecond: getTargetGasPerSecond,
maxChangePerEpoch: maxPercentChangePerEpoch / 100, maxChangePerEpoch: maxPercentChangePerEpoch,
}, nil }, nil
} }
......
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": {}
}
...@@ -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