diff --git a/.changeset/dull-nails-sin.md b/.changeset/dull-nails-sin.md new file mode 100644 index 0000000000000..76325cb3ad095 --- /dev/null +++ b/.changeset/dull-nails-sin.md @@ -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 diff --git a/go/gas-oracle/gasprices/gas_price_updater.go b/go/gas-oracle/gasprices/gas_price_updater.go index 558e6202474e1..bba3e4e2a5ce6 100644 --- a/go/gas-oracle/gasprices/gas_price_updater.go +++ b/go/gas-oracle/gasprices/gas_price_updater.go @@ -2,6 +2,7 @@ package gasprices import ( "errors" + "math/big" "sync" "github.com/ethereum/go-ethereum/log" @@ -9,33 +10,26 @@ import ( 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 { @@ -51,6 +45,7 @@ func NewGasPriceUpdater( epochLengthSeconds: epochLengthSeconds, averageBlockGasLimit: averageBlockGasLimit, getLatestBlockNumberFn: getLatestBlockNumberFn, + getGasUsedByBlockFn: getGasUsedByBlockFn, updateL2GasPriceFn: updateL2GasPriceFn, }, nil } @@ -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 diff --git a/go/gas-oracle/gasprices/gas_price_updater_test.go b/go/gas-oracle/gasprices/gas_price_updater_test.go index 4ba73150e604e..a15ab9879f77e 100644 --- a/go/gas-oracle/gasprices/gas_price_updater_test.go +++ b/go/gas-oracle/gasprices/gas_price_updater_test.go @@ -1,6 +1,7 @@ package gasprices import ( + "math/big" "testing" ) @@ -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 { @@ -46,6 +30,12 @@ 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, @@ -53,6 +43,7 @@ func makeTestGasPricerAndUpdater(curPrice uint64) (*GasPricer, *GasPriceUpdater, averageBlockGasLimit, epochLengthSeconds, getLatestBlockNumber, + getGasUsedByBlockFn, updateL2GasPrice, ) if err != nil { @@ -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) } }, }, @@ -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) } }, }, diff --git a/go/gas-oracle/gasprices/l2_gas_pricer.go b/go/gas-oracle/gasprices/l2_gas_pricer.go index 71babe1eb13db..67ebb426a63ac 100644 --- a/go/gas-oracle/gasprices/l2_gas_pricer.go +++ b/go/gas-oracle/gasprices/l2_gas_pricer.go @@ -52,8 +52,10 @@ 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 @@ -61,10 +63,13 @@ func (p *GasPricer) CalcNextEpochGasPrice(avgGasPerSecondLastEpoch float64) (uin } 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 } diff --git a/go/gas-oracle/oracle/config.go b/go/gas-oracle/oracle/config.go index abf11bc356aca..10ddad361c49c 100644 --- a/go/gas-oracle/oracle/config.go +++ b/go/gas-oracle/oracle/config.go @@ -26,7 +26,7 @@ type Config struct { floorPrice uint64 targetGasPerSecond uint64 maxPercentChangePerEpoch float64 - averageBlockGasLimitPerEpoch float64 + averageBlockGasLimitPerEpoch uint64 epochLengthSeconds uint64 l2GasPriceSignificanceFactor float64 l1BaseFeeSignificanceFactor float64 @@ -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) diff --git a/go/gas-oracle/oracle/gas_price_oracle.go b/go/gas-oracle/oracle/gas_price_oracle.go index 5b5cc0e4d2e4a..b25f13a9479a1 100644 --- a/go/gas-oracle/oracle/gas_price_oracle.go +++ b/go/gas-oracle/oracle/gas_price_oracle.go @@ -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, @@ -282,6 +285,7 @@ func NewGasPriceOracle(cfg *Config) (*GasPriceOracle, error) { cfg.averageBlockGasLimitPerEpoch, cfg.epochLengthSeconds, getLatestBlockNumberFn, + getGasUsedByBlockFn, updateL2GasPriceFn, ) diff --git a/go/gas-oracle/oracle/updater_interface.go b/go/gas-oracle/oracle/updater_interface.go index ca18600f85d26..6f81fc74c7539 100644 --- a/go/gas-oracle/oracle/updater_interface.go +++ b/go/gas-oracle/oracle/updater_interface.go @@ -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 {