Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dull-nails-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@eth-optimism/gas-oracle': patch
---

Meter gas usage based on gas used in block instead of assuming max gas usage per block
48 changes: 28 additions & 20 deletions go/gas-oracle/gasprices/gas_price_updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,34 @@ 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 float64
averageBlockGasLimit uint64
epochLengthSeconds uint64
getLatestBlockNumberFn GetLatestBlockNumberFn
getGasUsedByBlockFn GetGasUsedByBlockFn
updateL2GasPriceFn UpdateL2GasPriceFn
}

func GetAverageGasPerSecond(
epochStartBlockNumber uint64,
latestBlockNumber uint64,
epochLengthSeconds uint64,
averageBlockGasLimit uint64,
) float64 {
blocksPassed := latestBlockNumber - epochStartBlockNumber
return float64(blocksPassed * averageBlockGasLimit / epochLengthSeconds)
}

func NewGasPriceUpdater(
gasPricer *GasPricer,
epochStartBlockNumber uint64,
averageBlockGasLimit float64,
averageBlockGasLimit uint64,
epochLengthSeconds uint64,
getLatestBlockNumberFn GetLatestBlockNumberFn,
getGasUsedByBlockFn GetGasUsedByBlockFn,
updateL2GasPriceFn UpdateL2GasPriceFn,
) (*GasPriceUpdater, error) {
if averageBlockGasLimit < 1 {
Expand All @@ -51,6 +45,7 @@ func NewGasPriceUpdater(
epochLengthSeconds: epochLengthSeconds,
averageBlockGasLimit: averageBlockGasLimit,
getLatestBlockNumberFn: getLatestBlockNumberFn,
getGasUsedByBlockFn: getGasUsedByBlockFn,
updateL2GasPriceFn: updateL2GasPriceFn,
}, nil
}
Expand All @@ -63,16 +58,29 @@ func (g *GasPriceUpdater) UpdateGasPrice() error {
if err != nil {
return err
}
if latestBlockNumber < uint64(g.epochStartBlockNumber) {
if latestBlockNumber < g.epochStartBlockNumber {
return errors.New("Latest block number less than the last epoch's block number")
}
averageGasPerSecond := GetAverageGasPerSecond(
g.epochStartBlockNumber,
latestBlockNumber,
uint64(g.epochLengthSeconds),
uint64(g.averageBlockGasLimit),
)
log.Debug("UpdateGasPrice", "averageGasPerSecond", averageGasPerSecond, "current-price", g.gasPricer.curPrice)

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 / 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
Expand Down
31 changes: 11 additions & 20 deletions go/gas-oracle/gasprices/gas_price_updater_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gasprices

import (
"math/big"
"testing"
)

Expand All @@ -10,29 +11,12 @@ type MockEpoch struct {
postHook func(prevGasPrice uint64, gasPriceUpdater *GasPriceUpdater)
}

func TestGetAverageGasPerSecond(t *testing.T) {
// Let's sanity check this function with some simple inputs.
// A 10 block epoch
epochStartBlockNumber := 10
latestBlockNumber := 20
// That lasts 10 seconds (1 block per second)
epochLengthSeconds := 10
// And each block has a gas limit of 1
averageBlockGasLimit := 1
// We expect a gas per second to be 1!
expectedGps := 1.0
gps := GetAverageGasPerSecond(uint64(epochStartBlockNumber), uint64(latestBlockNumber), uint64(epochLengthSeconds), uint64(averageBlockGasLimit))
if gps != expectedGps {
t.Fatalf("Gas per second not calculated correctly. Got: %v expected: %v", gps, expectedGps)
}
}

// Return a gas pricer that targets 3 blocks per epoch & 10% max change per epoch.
func makeTestGasPricerAndUpdater(curPrice uint64) (*GasPricer, *GasPriceUpdater, func(uint64), error) {
gpsTarget := 3300000.0
getGasTarget := func() float64 { return gpsTarget }
epochLengthSeconds := uint64(10)
averageBlockGasLimit := 11000000.0
averageBlockGasLimit := uint64(11000000)
// Based on our 10 second epoch, we are targetting 3 blocks per epoch.
gasPricer, err := NewGasPricer(curPrice, 1, getGasTarget, 10)
if err != nil {
Expand All @@ -46,13 +30,20 @@ func makeTestGasPricerAndUpdater(curPrice uint64) (*GasPricer, *GasPriceUpdater,
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 {
Expand Down Expand Up @@ -127,7 +118,7 @@ func TestUsageOfGasPriceUpdater(t *testing.T) {
postHook: func(prevGasPrice uint64, gasPriceUpdater *GasPriceUpdater) {
curPrice := gasPriceUpdater.gasPricer.curPrice
if prevGasPrice >= curPrice {
t.Fatalf("Expected gas price to increase.")
t.Fatalf("Expected gas price to increase. Got %d, was %d", curPrice, prevGasPrice)
}
},
},
Expand All @@ -143,7 +134,7 @@ func TestUsageOfGasPriceUpdater(t *testing.T) {
postHook: func(prevGasPrice uint64, gasPriceUpdater *GasPriceUpdater) {
curPrice := gasPriceUpdater.gasPricer.curPrice
if prevGasPrice != curPrice {
t.Fatalf("Expected gas price to stablize.")
t.Fatalf("Expected gas price to stablize. Got %d, was %d", curPrice, prevGasPrice)
}
},
},
Expand Down
5 changes: 5 additions & 0 deletions go/gas-oracle/gasprices/l2_gas_pricer.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,24 @@ func (p *GasPricer) CalcNextEpochGasPrice(avgGasPerSecondLastEpoch float64) (uin
}
// 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
}

Expand Down
4 changes: 2 additions & 2 deletions go/gas-oracle/oracle/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Config struct {
floorPrice uint64
targetGasPerSecond uint64
maxPercentChangePerEpoch float64
averageBlockGasLimitPerEpoch float64
averageBlockGasLimitPerEpoch uint64
epochLengthSeconds uint64
l2GasPriceSignificanceFactor float64
l1BaseFeeSignificanceFactor float64
Expand All @@ -52,7 +52,7 @@ func NewConfig(ctx *cli.Context) *Config {
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.averageBlockGasLimitPerEpoch = ctx.GlobalUint64(flags.AverageBlockGasLimitPerEpochFlag.Name)
cfg.epochLengthSeconds = ctx.GlobalUint64(flags.EpochLengthSecondsFlag.Name)
cfg.l2GasPriceSignificanceFactor = ctx.GlobalFloat64(flags.L2GasPriceSignificanceFactorFlag.Name)
cfg.floorPrice = ctx.GlobalUint64(flags.FloorPriceFlag.Name)
Expand Down
4 changes: 4 additions & 0 deletions go/gas-oracle/oracle/gas_price_oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ func NewGasPriceOracle(cfg *Config) (*GasPriceOracle, error) {
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,
Expand All @@ -282,6 +285,7 @@ func NewGasPriceOracle(cfg *Config) (*GasPriceOracle, error) {
cfg.averageBlockGasLimitPerEpoch,
cfg.epochLengthSeconds,
getLatestBlockNumberFn,
getGasUsedByBlockFn,
updateL2GasPriceFn,
)

Expand Down
13 changes: 13 additions & 0 deletions go/gas-oracle/oracle/updater_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ func wrapGetLatestBlockNumberFn(backend bind.ContractBackend) func() (uint64, er
}
}

// 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 {
Expand Down