gas_price_oracle.go 8.41 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
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"
)

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
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")
)
36 37 38

// GasPriceOracle manages a hot key that can update the L2 Gas Price
type GasPriceOracle struct {
39 40
	l1ChainID       *big.Int
	l2ChainID       *big.Int
41 42 43
	ctx             context.Context
	stop            chan struct{}
	contract        *bindings.GasPriceOracle
44 45
	l2Backend       DeployContractBackend
	l1Backend       bind.ContractTransactor
46 47 48 49 50 51
	gasPriceUpdater *gasprices.GasPriceUpdater
	config          *Config
}

// Start runs the GasPriceOracle
func (g *GasPriceOracle) Start() error {
52 53 54 55 56
	if g.config.l1ChainID == nil {
		return fmt.Errorf("layer-one: %w", errNoChainID)
	}
	if g.config.l2ChainID == nil {
		return fmt.Errorf("layer-two: %w", errNoChainID)
57 58 59 60 61 62
	}
	if g.config.privateKey == nil {
		return errNoPrivateKey
	}

	address := crypto.PubkeyToAddress(g.config.privateKey.PublicKey)
63 64
	log.Info("Starting Gas Price Oracle", "l1-chain-id", g.l1ChainID,
		"l2-chain-id", g.l2ChainID, "address", address.Hex())
65 66 67 68 69 70 71 72 73

	price, err := g.contract.GasPrice(&bind.CallOpts{
		Context: context.Background(),
	})
	if err != nil {
		return err
	}
	gasPriceGauge.Update(int64(price.Uint64()))

74 75 76 77 78 79
	if g.config.enableL1BaseFee {
		go g.BaseFeeLoop()
	}
	if g.config.enableL2GasPrice {
		go g.Loop()
	}
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112

	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)
113
	defer timer.Stop()
114 115 116 117 118 119 120 121 122 123 124 125 126 127
	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()
		}
	}
}

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
func (g *GasPriceOracle) BaseFeeLoop() {
	timer := time.NewTicker(15 * 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()
		}
	}
}

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
// 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) {
177 178
	// Create the L2 client
	l2Client, err := ethclient.Dial(cfg.layerTwoHttpUrl)
179 180 181 182
	if err != nil {
		return nil, err
	}

183 184 185 186 187 188 189 190 191 192 193
	l1Client, err := ethclient.Dial(cfg.ethereumHttpUrl)
	if err != nil {
		return nil, err
	}

	// Ensure that we can actually connect to both backends
	if err := ensureConnection(l2Client); err != nil {
		log.Error("Unable to connect to layer two", "addr", cfg.layerTwoHttpUrl)
	}
	if err := ensureConnection(l1Client); err != nil {
		log.Error("Unable to connect to layer one", "addr", cfg.ethereumHttpUrl)
194 195 196
	}

	address := cfg.gasPriceOracleAddress
197
	contract, err := bindings.NewGasPriceOracle(address, l2Client)
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
	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
	}

227
	l2ChainID, err := l2Client.ChainID(context.Background())
228 229 230
	if err != nil {
		return nil, err
	}
231 232 233 234 235 236 237 238 239 240 241 242 243
	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
	}
244

245 246 247 248
	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)
249 250
		}
	} else {
251
		cfg.l1ChainID = l1ChainID
252 253 254 255 256 257
	}

	if cfg.privateKey == nil {
		return nil, errNoPrivateKey
	}

258
	tip, err := l2Client.HeaderByNumber(context.Background(), nil)
259 260 261 262 263 264 265 266
	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
267
	getLatestBlockNumberFn := wrapGetLatestBlockNumberFn(l2Client)
268 269
	// updateL2GasPriceFn is used by the GasPriceUpdater to
	// update the gas price
270
	updateL2GasPriceFn, err := wrapUpdateL2GasPriceFn(l2Client, cfg)
271 272 273
	if err != nil {
		return nil, err
	}
274 275 276
	// getGasUsedByBlockFn is used by the GasPriceUpdater
	// to fetch the amount of gas that a block has used
	getGasUsedByBlockFn := wrapGetGasUsedByBlock(l2Client)
277 278 279 280 281 282 283 284 285 286 287

	log.Info("Creating GasPriceUpdater", "epochStartBlockNumber", epochStartBlockNumber,
		"averageBlockGasLimitPerEpoch", cfg.averageBlockGasLimitPerEpoch,
		"epochLengthSeconds", cfg.epochLengthSeconds)

	gasPriceUpdater, err := gasprices.NewGasPriceUpdater(
		gasPricer,
		epochStartBlockNumber,
		cfg.averageBlockGasLimitPerEpoch,
		cfg.epochLengthSeconds,
		getLatestBlockNumberFn,
288
		getGasUsedByBlockFn,
289 290 291 292 293 294 295 296
		updateL2GasPriceFn,
	)

	if err != nil {
		return nil, err
	}

	gpo := GasPriceOracle{
297 298
		l2ChainID:       l2ChainID,
		l1ChainID:       l1ChainID,
299 300 301 302 303
		ctx:             context.Background(),
		stop:            make(chan struct{}),
		contract:        contract,
		gasPriceUpdater: gasPriceUpdater,
		config:          cfg,
304 305
		l2Backend:       l2Client,
		l1Backend:       l1Client,
306 307 308 309 310 311 312 313
	}

	if err := gpo.ensure(); err != nil {
		return nil, err
	}

	return &gpo, nil
}
314 315 316 317 318 319 320 321 322

// Ensure that we can actually connect
func ensureConnection(client *ethclient.Client) error {
	t := time.NewTicker(5 * time.Second)
	defer t.Stop()
	for ; true; <-t.C {
		_, err := client.ChainID(context.Background())
		if err == nil {
			break
323 324
		} else {
			return err
325 326 327 328
		}
	}
	return nil
}