Commit 35c035fa authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

Merge pull request #5960 from ethereum-optimism/cleanup/delete-gas-oracle

cleanup: delete gas-oracle
parents 142448c5 b12471c9
......@@ -1245,10 +1245,6 @@ workflows:
name: proxyd-tests
binary_name: proxyd
working_directory: proxyd
- go-lint-test-build:
name: gas-oracle-tests
binary_name: gas-oracle
working_directory: gas-oracle
- go-lint-test-build:
name: indexer-tests
binary_name: indexer
......
# Legacy codebases
/gas-oracle @ethereum-optimism/legacy-reviewers
/l2geth @ethereum-optimism/legacy-reviewers
/packages/actor-tests @ethereum-optimism/legacy-reviewers
/packages/common-ts @ethereum-optimism/typescript-reviewers
......
......@@ -24,7 +24,6 @@ jobs:
data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }}
contracts: ${{ steps.packages.outputs.contracts }}
contracts-bedrock: ${{ steps.packages.outputs.contracts-bedrock }}
gas-oracle: ${{ steps.packages.outputs.gas-oracle }}
replica-healthcheck: ${{ steps.packages.outputs.replica-healthcheck }}
hardhat-node: ${{ steps.packages.outputs.hardhat-node }}
canary-docker-tag: ${{ steps.docker-image-name.outputs.canary-docker-tag }}
......@@ -124,32 +123,6 @@ jobs:
push: true
tags: ethereumoptimism/l2geth:${{ needs.canary-publish.outputs.canary-docker-tag }}
gas-oracle:
name: Publish Gas Oracle ${{ needs.canary-publish.outputs.canary-docker-tag }}
needs: canary-publish
if: needs.canary-publish.outputs.gas-oracle != ''
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_USERNAME }}
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_SECRET }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./gas-oracle/Dockerfile
push: true
tags: ethereumoptimism/gas-oracle:${{ needs.canary-publish.outputs.canary-docker-tag }}
hardhat-node:
name: Publish Hardhat Node ${{ needs.canary-publish.outputs.canary-docker-tag }}
needs: canary-publish
......
......@@ -24,7 +24,6 @@ jobs:
contracts: ${{ steps.packages.outputs.contracts }}
contracts-bedrock: ${{ steps.packages.outputs.contracts-bedrock }}
balance-monitor: ${{ steps.packages.outputs.balance-monitor }}
gas-oracle: ${{ steps.packages.outputs.gas-oracle }}
replica-healthcheck: ${{ steps.packages.outputs.replica-healthcheck }}
hardhat-node: ${{ steps.packages.outputs.hardhat-node }}
op-exporter: ${{ steps.packages.outputs.op-exporter }}
......@@ -110,32 +109,6 @@ jobs:
push: true
tags: ethereumoptimism/l2geth:${{ needs.release.outputs.l2geth }},ethereumoptimism/l2geth:latest
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: ./gas-oracle/Dockerfile
push: true
tags: ethereumoptimism/gas-oracle:${{ needs.release.outputs.gas-oracle }},ethereumoptimism/gas-oracle:latest
hardhat-node:
name: Publish Hardhat Node ${{ needs.release.outputs.hardhat-node }}
needs: release
......
......@@ -89,7 +89,6 @@ Refer to the Directory Structure section below to understand which packages are
│ ├── <a href="./packages/message-relayer">message-relayer</a>: Tool for automatically relaying L1<>L2 messages in development
│ ├── <a href="./packages/replica-healthcheck">replica-healthcheck</a>: Service for monitoring the health of a replica node
│ └── <a href="./packages/sdk">sdk</a>: provides a set of tools for interacting with Optimism
├── <a href="./gas-oracle">gas-oracle</a>: Service for updating L1 gas prices on L2
├── <a href="./indexer">indexer</a>: indexes and syncs transactions
├── <a href="./infra/op-replica">infra/op-replica</a>: Deployment examples and resources for running an Optimism replica
├── <a href="./l2geth">l2geth</a>: Optimism client software, a fork of <a href="https://github.com/ethereum/go-ethereum/tree/v1.9.10">geth v1.9.10</a>
......
# @eth-optimism/gas-oracle
## 0.1.13
### Patch Changes
- 9b61c84c9: build(deps): bump golang.org/x/net from 0.0.0-20211112202133-69e39bad7dc2 to 0.7.0 in /gas-oracle
- f13b31e04: build(deps): bump golang.org/x/sys from 0.0.0-20220310020820-b874c991c1a5 to 0.1.0 in /gas-oracle
## 0.1.12
### Patch Changes
- 6f458607: Bump go-ethereum to 1.10.17
## 0.1.11
### Patch Changes
- 160f4c3d: Update docker image to use golang 1.18.0
## 0.1.10
### Patch Changes
- 162ff89c: Fixes a bug that would cause the service to crash on startup if the RPC URLs were not immediately available
## 0.1.9
### Patch Changes
- c535b3a5: Allow configurable base fee update poll time with `GAS_PRICE_ORACLE_L1_BASE_FEE_EPOCH_LENGTH_SECONDS`
## 0.1.8
### Patch Changes
- 88601cb7: Refactored Dockerfiles
## 0.1.7
### Patch Changes
- fed748e0: Update to go-ethereum v1.10.16
## 0.1.6
### Patch Changes
- b3efb8b7: String update to change the system name from OE to Optimism
## 0.1.5
### Patch Changes
- 40b6c5bd: Update the flag parsing of the average block gas limit
## 0.1.4
### Patch Changes
- 9eed33c4: fix rounding error in average gas/epoch calculation
## 0.1.3
### Patch Changes
- 3af7ce3f: Meter gas usage based on gas used in block instead of assuming max gas usage per block
## 0.1.2
### Patch Changes
- 5a3996ec: Fixed gas-oacle tx/not_significant metric name
## 0.1.1
### Patch Changes
- e4067d4c: Fix the gas oracle gas price prometheus metric
## 0.1.0
### Minor Changes
- d89b5005: Add L1 base fee, add breaking config options
- 81ccd6e4: `regenesis/0.5.0` release
### Patch Changes
- d7fa6809: Bumps the go-ethereum dependency version to v1.10.9
- b70ee70c: upgraded to solidity 0.8.9
- 4f805355: Bump go-ethereum dep to v1.10.10
- 1527cf6f: Use the configured gas price when updating the L1 base fee in L2 state
## 0.0.3
### Patch Changes
- 8c4f479c: Add additional logging in the `gas-oracle`
## 0.0.2
### Patch Changes
- ce3c353b: Initial implementation of the `gas-oracle`
FROM golang:1.18.0-alpine3.15 as builder
RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash
COPY ./gas-oracle /gas-oracle
RUN cd /gas-oracle && make gas-oracle
FROM alpine:3.15
RUN apk add --no-cache ca-certificates jq curl
COPY --from=builder /gas-oracle/gas-oracle /usr/local/bin/
WORKDIR /usr/local/bin/
ENTRYPOINT ["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)"
CONTRACTS_PATH := "../../packages/contracts/artifacts/contracts"
gas-oracle:
env GO111MODULE=on go build $(LDFLAGS)
.PHONY: gas-oracle
clean:
rm gas-oracle
test:
go test -v ./...
lint:
golangci-lint run ./...
abi:
cat $(CONTRACTS_PATH)/L2/predeploys/OVM_GasPriceOracle.sol/OVM_GasPriceOracle.json \
| jq '{abi,bytecode}' \
> abis/OVM_GasPriceOracle.json
binding: abi
$(eval temp := $(shell mktemp))
cat abis/OVM_GasPriceOracle.json \
| jq -r .bytecode > $(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 Optimism 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 Optimism 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
```
This diff is collapsed.
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: "L1 HTTP Endpoint",
EnvVar: "GAS_PRICE_ORACLE_ETHEREUM_HTTP_URL",
}
LayerTwoHttpUrlFlag = cli.StringFlag{
Name: "layer-two-http-url",
Value: "http://127.0.0.1:9545",
Usage: "Sequencer HTTP Endpoint",
EnvVar: "GAS_PRICE_ORACLE_LAYER_TWO_HTTP_URL",
}
L1ChainIDFlag = cli.Uint64Flag{
Name: "l1-chain-id",
Usage: "L1 Chain ID",
EnvVar: "GAS_PRICE_ORACLE_L1_CHAIN_ID",
}
L2ChainIDFlag = cli.Uint64Flag{
Name: "l2-chain-id",
Usage: "L2 Chain ID",
EnvVar: "GAS_PRICE_ORACLE_L2_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",
}
EnableL1BaseFeeFlag = cli.BoolFlag{
Name: "enable-l1-base-fee",
Usage: "Enable updating the L1 base fee",
EnvVar: "GAS_PRICE_ORACLE_ENABLE_L1_BASE_FEE",
}
EnableL2GasPriceFlag = cli.BoolFlag{
Name: "enable-l2-gas-price",
Usage: "Enable updating the L2 gas price",
EnvVar: "GAS_PRICE_ORACLE_ENABLE_L2_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.Uint64Flag{
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",
}
L1BaseFeeEpochLengthSecondsFlag = cli.Uint64Flag{
Name: "l1-base-fee-epoch-length-seconds",
Value: 15,
Usage: "polling time for updating the L1 base fee",
EnvVar: "GAS_PRICE_ORACLE_L1_BASE_FEE_EPOCH_LENGTH_SECONDS",
}
L1BaseFeeSignificanceFactorFlag = cli.Float64Flag{
Name: "l1-base-fee-significant-factor",
Value: 0.10,
Usage: "only update when the L1 base fee changes by more than this factor",
EnvVar: "GAS_PRICE_ORACLE_L1_BASE_FEE_SIGNIFICANT_FACTOR",
}
L2GasPriceSignificanceFactorFlag = 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,
LayerTwoHttpUrlFlag,
L1ChainIDFlag,
L2ChainIDFlag,
L1BaseFeeSignificanceFactorFlag,
GasPriceOracleAddressFlag,
PrivateKeyFlag,
TransactionGasPriceFlag,
LogLevelFlag,
FloorPriceFlag,
TargetGasPerSecondFlag,
MaxPercentChangePerEpochFlag,
AverageBlockGasLimitPerEpochFlag,
EpochLengthSecondsFlag,
L1BaseFeeEpochLengthSecondsFlag,
L2GasPriceSignificanceFactorFlag,
WaitForReceiptFlag,
EnableL1BaseFeeFlag,
EnableL2GasPriceFlag,
MetricsEnabledFlag,
MetricsHTTPFlag,
MetricsPortFlag,
MetricsEnableInfluxDBFlag,
MetricsInfluxDBEndpointFlag,
MetricsInfluxDBDatabaseFlag,
MetricsInfluxDBUsernameFlag,
MetricsInfluxDBPasswordFlag,
}
package gasprices
import (
"errors"
"math/big"
"sync"
"github.com/ethereum/go-ethereum/log"
)
type GetLatestBlockNumberFn func() (uint64, error)
type UpdateL2GasPriceFn func(uint64) error
type GetGasUsedByBlockFn func(*big.Int) (uint64, error)
type GasPriceUpdater struct {
mu *sync.RWMutex
gasPricer *GasPricer
epochStartBlockNumber uint64
averageBlockGasLimit uint64
epochLengthSeconds uint64
getLatestBlockNumberFn GetLatestBlockNumberFn
getGasUsedByBlockFn GetGasUsedByBlockFn
updateL2GasPriceFn UpdateL2GasPriceFn
}
func NewGasPriceUpdater(
gasPricer *GasPricer,
epochStartBlockNumber uint64,
averageBlockGasLimit uint64,
epochLengthSeconds uint64,
getLatestBlockNumberFn GetLatestBlockNumberFn,
getGasUsedByBlockFn GetGasUsedByBlockFn,
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,
getGasUsedByBlockFn: getGasUsedByBlockFn,
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 < g.epochStartBlockNumber {
return errors.New("Latest block number less than the last epoch's block number")
}
if latestBlockNumber == g.epochStartBlockNumber {
log.Debug("latest block number is equal to epoch start block number", "number", latestBlockNumber)
return nil
}
// Accumulate the amount of gas that has been used in the epoch
totalGasUsed := uint64(0)
for i := g.epochStartBlockNumber + 1; i <= latestBlockNumber; i++ {
gasUsed, err := g.getGasUsedByBlockFn(new(big.Int).SetUint64(i))
log.Trace("fetching gas used", "height", i, "gas-used", gasUsed, "total-gas", totalGasUsed)
if err != nil {
return err
}
totalGasUsed += gasUsed
}
averageGasPerSecond := float64(totalGasUsed) / float64(g.epochLengthSeconds)
log.Debug("UpdateGasPrice", "average-gas-per-second", 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 (
"math/big"
"testing"
)
type MockEpoch struct {
numBlocks uint64
repeatCount uint64
postHook func(prevGasPrice uint64, gasPriceUpdater *GasPriceUpdater)
}
// 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 := 990000.3
getGasTarget := func() float64 { return gpsTarget }
epochLengthSeconds := uint64(10)
averageBlockGasLimit := uint64(11000000)
// Based on our 10 second epoch, we are targeting 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
}
// This is paramaterized based on 3 blocks per epoch, where each uses
// the average block gas limit plus an additional bit of gas added
getGasUsedByBlockFn := func(number *big.Int) (uint64, error) {
return averageBlockGasLimit*3/epochLengthSeconds + 1, nil
}
startBlock, _ := getLatestBlockNumber()
gasUpdater, err := NewGasPriceUpdater(
gasPricer,
startBlock,
averageBlockGasLimit,
epochLengthSeconds,
getLatestBlockNumber,
getGasUsedByBlockFn,
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. Got %d, was %d", curPrice, prevGasPrice)
}
},
},
// 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. Got %d, was %d", curPrice, prevGasPrice)
}
targetGps := gasPriceUpdater.gasPricer.getTargetGasPerSecond()
averageGps := gasPriceUpdater.gasPricer.avgGasPerSecondLastEpoch
if targetGps != averageGps {
t.Fatalf("Average gas/second (%f) did not converge to target (%f)",
averageGps, targetGps)
}
},
},
// 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
avgGasPerSecondLastEpoch float64
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
log.Trace("Calculating next epoch gas price", "proportionOfTarget", proportionOfTarget,
"avgGasPerSecondLastEpoch", avgGasPerSecondLastEpoch, "targetGasPerSecond", 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)
}
updated := float64(max(1, p.curPrice)) * proportionToChangeBy
result := max(p.floorPrice, uint64(math.Ceil(updated)))
log.Debug("Calculated next epoch gas price", "proportionToChangeBy", proportionToChangeBy,
"proportionOfTarget", proportionOfTarget, "result", result)
return result, 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
p.avgGasPerSecondLastEpoch = avgGasPerSecondLastEpoch
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/gas-oracle
go 1.18
require (
github.com/ethereum/go-ethereum v1.10.17
github.com/urfave/cli v1.22.5
)
require (
github.com/VictoriaMetrics/fastcache v1.9.0 // indirect
github.com/allegro/bigcache v1.2.1 // indirect
github.com/btcsuite/btcd v0.22.0-beta // indirect
github.com/btcsuite/btcd/btcec/v2 v2.1.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/deepmap/oapi-codegen v1.8.2 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/fjl/memsize v0.0.1 // indirect
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/go-bexpr v0.1.11 // indirect
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/holiman/uint256 v1.2.0 // indirect
github.com/huin/goupnp v1.0.3 // indirect
github.com/influxdata/influxdb v1.8.3 // indirect
github.com/influxdata/influxdb-client-go/v2 v2.4.0 // indirect
github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.16.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/tsdb v0.10.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rjeczalik/notify v0.9.2 // indirect
github.com/rs/cors v1.8.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/status-im/keycard-go v0.0.0-20211109104530-b0e0482ba91d // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
This diff is collapsed.
package main
import (
"fmt"
"os"
"time"
"github.com/ethereum-optimism/optimism/gas-oracle/flags"
ometrics "github.com/ethereum-optimism/optimism/gas-oracle/metrics"
"github.com/ethereum-optimism/optimism/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 Optimism Gas Price"
app.Description = "Configure with a private key and an Optimism 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 (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/gas-oracle/bindings"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
)
func wrapUpdateBaseFee(l1Backend bind.ContractTransactor, l2Backend DeployContractBackend, cfg *Config) (func() error, error) {
if cfg.privateKey == nil {
return nil, errNoPrivateKey
}
if cfg.l2ChainID == nil {
return nil, errNoChainID
}
opts, err := bind.NewKeyedTransactorWithChainID(cfg.privateKey, cfg.l2ChainID)
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, l2Backend)
if err != nil {
return nil, err
}
return func() error {
baseFee, err := contract.L1BaseFee(&bind.CallOpts{
Context: context.Background(),
})
if err != nil {
return err
}
tip, err := l1Backend.HeaderByNumber(context.Background(), nil)
if err != nil {
return err
}
if tip.BaseFee == nil {
return errNoBaseFee
}
if !isDifferenceSignificant(baseFee.Uint64(), tip.BaseFee.Uint64(), cfg.l1BaseFeeSignificanceFactor) {
log.Debug("non significant base fee update", "tip", tip.BaseFee, "current", baseFee)
return nil
}
// Use the configured gas price if it is set,
// otherwise use gas estimation
if cfg.gasPrice != nil {
opts.GasPrice = cfg.gasPrice
} else {
gasPrice, err := l2Backend.SuggestGasPrice(opts.Context)
if err != nil {
return err
}
opts.GasPrice = gasPrice
}
tx, err := contract.SetL1BaseFee(opts, tip.BaseFee)
if err != nil {
return err
}
log.Debug("updating L1 base fee", "tx.gasPrice", tx.GasPrice(), "tx.gasLimit", tx.Gas(),
"tx.data", hexutil.Encode(tx.Data()), "tx.to", tx.To().Hex(), "tx.nonce", tx.Nonce())
if err := l2Backend.SendTransaction(context.Background(), tx); err != nil {
return fmt.Errorf("cannot update base fee: %w", err)
}
log.Info("L1 base fee transaction sent", "hash", tx.Hash().Hex())
if cfg.waitForReceipt {
// Wait for the receipt
receipt, err := waitForReceipt(l2Backend, tx)
if err != nil {
return err
}
log.Info("base-fee transaction confirmed", "hash", tx.Hash().Hex(),
"gas-used", receipt.GasUsed, "blocknumber", receipt.BlockNumber)
}
return nil
}, nil
}
package oracle
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/gas-oracle/bindings"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
func TestBaseFeeUpdate(t *testing.T) {
key, _ := crypto.GenerateKey()
sim, _ := newSimulatedBackend(key)
chain := sim.Blockchain()
opts, _ := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337))
addr, _, gpo, err := bindings.DeployGasPriceOracle(opts, sim, opts.From)
if err != nil {
t.Fatal(err)
}
sim.Commit()
cfg := &Config{
privateKey: key,
l2ChainID: big.NewInt(1337),
gasPriceOracleAddress: addr,
gasPrice: big.NewInt(784637584),
}
update, err := wrapUpdateBaseFee(sim, sim, cfg)
if err != nil {
t.Fatal(err)
}
// Get the initial base fee
l1BaseFee, err := gpo.L1BaseFee(&bind.CallOpts{})
if err != nil {
t.Fatal(err)
}
// base fee should start at 0
if l1BaseFee.Cmp(common.Big0) != 0 {
t.Fatal("does not start at 0")
}
// get the header to know what the base fee
// should be updated to
tip := chain.CurrentHeader()
if tip.BaseFee == nil {
t.Fatal("no base fee found")
}
// Ensure that there is no false negative by
// checking that the values don't start out the same
if l1BaseFee.Cmp(tip.BaseFee) == 0 {
t.Fatal("values are already the same")
}
// Call the update function to do the update
if err := update(); err != nil {
t.Fatalf("cannot update base fee: %s", err)
}
sim.Commit()
// Check the updated base fee
l1BaseFee, err = gpo.L1BaseFee(&bind.CallOpts{})
if err != nil {
t.Fatal(err)
}
// the base fee should be equal to the value
// on the header
if tip.BaseFee.Cmp(l1BaseFee) != 0 {
t.Fatal("base fee not updated")
}
}
package oracle
import (
"crypto/ecdsa"
"fmt"
"math/big"
"strings"
"github.com/ethereum-optimism/optimism/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 {
l1ChainID *big.Int
l2ChainID *big.Int
ethereumHttpUrl string
layerTwoHttpUrl string
gasPriceOracleAddress common.Address
privateKey *ecdsa.PrivateKey
gasPrice *big.Int
waitForReceipt bool
floorPrice uint64
targetGasPerSecond uint64
maxPercentChangePerEpoch float64
averageBlockGasLimitPerEpoch uint64
epochLengthSeconds uint64
l1BaseFeeEpochLengthSeconds uint64
l2GasPriceSignificanceFactor float64
l1BaseFeeSignificanceFactor float64
enableL1BaseFee bool
enableL2GasPrice bool
// 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)
cfg.layerTwoHttpUrl = ctx.GlobalString(flags.LayerTwoHttpUrlFlag.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.GlobalUint64(flags.AverageBlockGasLimitPerEpochFlag.Name)
cfg.epochLengthSeconds = ctx.GlobalUint64(flags.EpochLengthSecondsFlag.Name)
cfg.l1BaseFeeEpochLengthSeconds = ctx.GlobalUint64(flags.L1BaseFeeEpochLengthSecondsFlag.Name)
cfg.l2GasPriceSignificanceFactor = ctx.GlobalFloat64(flags.L2GasPriceSignificanceFactorFlag.Name)
cfg.floorPrice = ctx.GlobalUint64(flags.FloorPriceFlag.Name)
cfg.l1BaseFeeSignificanceFactor = ctx.GlobalFloat64(flags.L1BaseFeeSignificanceFactorFlag.Name)
cfg.enableL1BaseFee = ctx.GlobalBool(flags.EnableL1BaseFeeFlag.Name)
cfg.enableL2GasPrice = ctx.GlobalBool(flags.EnableL2GasPriceFlag.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.L1ChainIDFlag.Name) {
chainID := ctx.GlobalUint64(flags.L1ChainIDFlag.Name)
cfg.l1ChainID = new(big.Int).SetUint64(chainID)
}
if ctx.GlobalIsSet(flags.L2ChainIDFlag.Name) {
chainID := ctx.GlobalUint64(flags.L2ChainIDFlag.Name)
cfg.l2ChainID = 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/gas-oracle/bindings"
"github.com/ethereum-optimism/optimism/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"
)
var (
// errInvalidSigningKey represents the error when the signing key used
// is not the Owner of the contract and therefore cannot update the gasprice
errInvalidSigningKey = errors.New("invalid signing key")
// errNoChainID represents the error when the chain id is not provided
// and it cannot be remotely fetched
errNoChainID = errors.New("no chain id provided")
// errNoPrivateKey represents the error when the private key is not provided to
// the application
errNoPrivateKey = errors.New("no private key provided")
// errWrongChainID represents the error when the configured chain id is not
// correct
errWrongChainID = errors.New("wrong chain id provided")
// errNoBaseFee represents the error when the base fee is not found on the
// block. This means that the block being queried is pre eip1559
errNoBaseFee = errors.New("base fee not found on block")
)
// GasPriceOracle manages a hot key that can update the L2 Gas Price
type GasPriceOracle struct {
l1ChainID *big.Int
l2ChainID *big.Int
ctx context.Context
stop chan struct{}
contract *bindings.GasPriceOracle
l2Backend DeployContractBackend
l1Backend bind.ContractTransactor
gasPriceUpdater *gasprices.GasPriceUpdater
config *Config
}
// Start runs the GasPriceOracle
func (g *GasPriceOracle) Start() error {
if g.config.l1ChainID == nil {
return fmt.Errorf("layer-one: %w", errNoChainID)
}
if g.config.l2ChainID == nil {
return fmt.Errorf("layer-two: %w", errNoChainID)
}
if g.config.privateKey == nil {
return errNoPrivateKey
}
address := crypto.PubkeyToAddress(g.config.privateKey.PublicKey)
log.Info("Starting Gas Price Oracle", "l1-chain-id", g.l1ChainID,
"l2-chain-id", g.l2ChainID, "address", address.Hex())
price, err := g.contract.GasPrice(&bind.CallOpts{
Context: context.Background(),
})
if err != nil {
return err
}
gasPriceGauge.Update(int64(price.Uint64()))
if g.config.enableL1BaseFee {
go g.BaseFeeLoop()
}
if g.config.enableL2GasPrice {
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)
defer timer.Stop()
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()
}
}
}
func (g *GasPriceOracle) BaseFeeLoop() {
timer := time.NewTicker(time.Duration(g.config.l1BaseFeeEpochLengthSeconds) * time.Second)
defer timer.Stop()
updateBaseFee, err := wrapUpdateBaseFee(g.l1Backend, g.l2Backend, g.config)
if err != nil {
panic(err)
}
for {
select {
case <-timer.C:
if err := updateBaseFee(); err != nil {
log.Error("cannot update l1 base fee", "messgae", 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) {
// Create the L2 client
l2Client, err := ethclient.Dial(cfg.layerTwoHttpUrl)
if err != nil {
return nil, err
}
l1Client, err := ethclient.Dial(cfg.ethereumHttpUrl)
if err != nil {
return nil, err
}
// Ensure that we can actually connect to both backends
log.Info("Connecting to layer two")
if err := ensureConnection(l2Client); err != nil {
log.Error("Unable to connect to layer two")
return nil, err
}
log.Info("Connecting to layer one")
if err := ensureConnection(l1Client); err != nil {
log.Error("Unable to connect to layer one")
return nil, err
}
address := cfg.gasPriceOracleAddress
contract, err := bindings.NewGasPriceOracle(address, l2Client)
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
}
l2ChainID, err := l2Client.ChainID(context.Background())
if err != nil {
return nil, err
}
l1ChainID, err := l1Client.ChainID(context.Background())
if err != nil {
return nil, err
}
if cfg.l2ChainID != nil {
if cfg.l2ChainID.Cmp(l2ChainID) != 0 {
return nil, fmt.Errorf("%w: L2: configured with %d and got %d",
errWrongChainID, cfg.l2ChainID, l2ChainID)
}
} else {
cfg.l2ChainID = l2ChainID
}
if cfg.l1ChainID != nil {
if cfg.l1ChainID.Cmp(l1ChainID) != 0 {
return nil, fmt.Errorf("%w: L1: configured with %d and got %d",
errWrongChainID, cfg.l1ChainID, l1ChainID)
}
} else {
cfg.l1ChainID = l1ChainID
}
if cfg.privateKey == nil {
return nil, errNoPrivateKey
}
tip, err := l2Client.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(l2Client)
// updateL2GasPriceFn is used by the GasPriceUpdater to
// update the gas price
updateL2GasPriceFn, err := wrapUpdateL2GasPriceFn(l2Client, cfg)
if err != nil {
return nil, err
}
// getGasUsedByBlockFn is used by the GasPriceUpdater
// to fetch the amount of gas that a block has used
getGasUsedByBlockFn := wrapGetGasUsedByBlock(l2Client)
log.Info("Creating GasPriceUpdater", "epochStartBlockNumber", epochStartBlockNumber,
"averageBlockGasLimitPerEpoch", cfg.averageBlockGasLimitPerEpoch,
"epochLengthSeconds", cfg.epochLengthSeconds)
gasPriceUpdater, err := gasprices.NewGasPriceUpdater(
gasPricer,
epochStartBlockNumber,
cfg.averageBlockGasLimitPerEpoch,
cfg.epochLengthSeconds,
getLatestBlockNumberFn,
getGasUsedByBlockFn,
updateL2GasPriceFn,
)
if err != nil {
return nil, err
}
gpo := GasPriceOracle{
l2ChainID: l2ChainID,
l1ChainID: l1ChainID,
ctx: context.Background(),
stop: make(chan struct{}),
contract: contract,
gasPriceUpdater: gasPriceUpdater,
config: cfg,
l2Backend: l2Client,
l1Backend: l1Client,
}
if err := gpo.ensure(); err != nil {
return nil, err
}
return &gpo, nil
}
// Ensure that we can actually connect
func ensureConnection(client *ethclient.Client) error {
t := time.NewTicker(1 * time.Second)
retries := 0
defer t.Stop()
for ; true; <-t.C {
_, err := client.ChainID(context.Background())
if err == nil {
break
} else {
retries += 1
if retries > 90 {
return err
}
}
}
return nil
}
package oracle
import (
"context"
"errors"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/gas-oracle/bindings"
ometrics "github.com/ethereum-optimism/optimism/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
}
}
// wrapGetGasUsedByBlock is used by the GasPriceUpdater to get
// the amount of gas used by a particular block. This is used to
// track gas usage over time
func wrapGetGasUsedByBlock(backend bind.ContractBackend) func(*big.Int) (uint64, error) {
return func(number *big.Int) (uint64, error) {
block, err := backend.HeaderByNumber(context.Background(), number)
if err != nil {
return 0, err
}
return block.GasUsed, 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.l2ChainID == nil {
return nil, errNoChainID
}
opts, err := bind.NewKeyedTransactorWithChainID(cfg.privateKey, cfg.l2ChainID)
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.l2GasPriceSignificanceFactor) {
log.Info("gas price did not significantly change", "min-factor", cfg.l2GasPriceSignificanceFactor,
"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("updating L2 gas price", "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("L2 gas price 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("L2 gas price 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
}
This diff is collapsed.
{
"name": "@eth-optimism/gas-oracle",
"version": "0.1.13",
"private": true,
"devDependencies": {}
}
This diff is collapsed.
......@@ -6,7 +6,6 @@
"workspaces": {
"packages": [
"packages/*",
"gas-oracle",
"l2geth",
"ops/docker/rpc-proxy",
"ops/docker/hardhat",
......
......@@ -27,7 +27,6 @@ Go modules which are not yet versioned:
```text
./batch-submitter (changesets)
./gas-oracle (changesets)
./indexer (changesets)
./l2geth (changesets)
./op-exporter (changesets)
......
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