--- title: Gas on Optimism description: A brief technical overview of how transactions are priced on Optimism --- # Gas on Optimism This document seeks to provide an technical overview of the components that impact transaction prices on Optimism as of February 22, 2022. *Authored by [Rajiv Patel-O'Connor](https://twitter.com/rajivpoc) and special thanks to [Mark Tyneway](https://twitter.com/tyneslol) for the review* ## `OVM_GasPriceOracle.sol` [`OVM_GasPriceOracle`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/predeploys/OVM_GasPriceOracle.sol) is a predeploy (a pre-compile written in Solidity as opposed to native langauge) that exposes the L2 execution gas price as well as the associated L1 data fee. The reason there is an L1 data fee is because each transaction on Optimism is posted to mainnet for data availability purposes which naturally incurs a cost as calldata is not free. As such, the total cost of a transaction can be given as the sum of the L2 execution fee and associated L1 data fee. The L2 execution fee is `gasPrice * l2_tx_gas_used` and the gasPrice can be updated by the `owner` of the GasPriceOracle. The L1 data fee calculation is a bit more complicated but it's roughly `l1BaseFee * (l1_gas_used + overhead) * scalar / decimals` where `scalar / decimals` represents an dynamic overhead and `overhead` is a fixed overhead for the amortized cost of batch submission per tx. `l1_gas_used` is calculated as 16 gas per non-zero byte and 4 gas per zero byte of the RLP-encoded transaction. A helper for calculating L1 gas of unsigned txs is provided as part of the predeploy: ```solidity= function getL1GasUsed(bytes memory _data) public view returns (uint256) { uint256 total = 0; for (uint256 i = 0; i < _data.length; i++) { if (_data[i] == 0) { total += 4; } else { total += 16; } } uint256 unsigned = total + overhead; return unsigned + (68 * 16); } ``` The 68 * 16 at the end is to account for the 68 bytes for r, s, v and their respective RLP prefixes all of which are assumed to be non-zero. The predeploy includes a functions for updating the variables used in above equations: ```solidity= /** * Allows the owner to modify the l2 gas price. * @param _gasPrice New l2 gas price. */ // slither-disable-next-line external-function function setGasPrice(uint256 _gasPrice) public onlyOwner { gasPrice = _gasPrice; emit GasPriceUpdated(_gasPrice); } /** * Allows the owner to modify the l1 base fee. * @param _baseFee New l1 base fee */ // slither-disable-next-line external-function function setL1BaseFee(uint256 _baseFee) public onlyOwner { l1BaseFee = _baseFee; emit L1BaseFeeUpdated(_baseFee); } /** * Allows the owner to modify the overhead. * @param _overhead New overhead */ // slither-disable-next-line external-function function setOverhead(uint256 _overhead) public onlyOwner { overhead = _overhead; emit OverheadUpdated(_overhead); } /** * Allows the owner to modify the scalar. * @param _scalar New scalar */ // slither-disable-next-line external-function function setScalar(uint256 _scalar) public onlyOwner { scalar = _scalar; emit ScalarUpdated(_scalar); } /** * Allows the owner to modify the decimals. * @param _decimals New decimals */ // slither-disable-next-line external-function function setDecimals(uint256 _decimals) public onlyOwner { decimals = _decimals; emit DecimalsUpdated(_decimals); } ``` ## Gas Oracle Service Directly from the service [README](https://github.com/ethereum-optimism/optimism/tree/develop/go/gas-oracle): > This service is responsible for sending transactions to the Sequencer to update the L2 gas price over time. It holds the private key associated with the owner of `OVM_GasPriceOracle`, so it alone can make updates as per the `onlyOwner` modifier. So how exactly does this work? If you peek into [main.go](https://github.com/ethereum-optimism/optimism/blob/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle/main.go#L46-L54), you'll notice there's some code to initialize the Gas Price Oracle and some code to start it. ```go= config := oracle.NewConfig(ctx) gpo, err := oracle.NewGasPriceOracle(config) if err != nil { return err } if err := gpo.Start(); err != nil { return err } ``` The `Start` method has the option of running [two loops](https://github.com/ethereum-optimism/optimism/blob/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle/oracle/gas_price_oracle.go#L74-L78), one to update L1 data fee price and a second to update the L2 gas price. ### Updating the L1 data fee price The [core loop code](https://github.com/ethereum-optimism/optimism/blob/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle/oracle/gas_price_oracle.go#L128) is the following: ```go= 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() } } } ``` Nothing too unusual but worth noting that the `updateBaseFee` function is called every 15 seconds which is slighly longer than the [average block time](https://etherscan.io/chart/blocktime) of ~13s. `updateBaseFee` first gets the gas price of the last L1 block and checks to see if there is a "significant" difference between it and the value for the L1 data fee price in the `OVM_GasPriceOracle` contract. Significance is determined by having a percentage difference greater than a config variable whose default is set to .05 as per the [README](https://github.com/ethereum-optimism/optimism/tree/develop/go/gas-oracle#readme). ```go= 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 } ``` If there is a difference, a [contract call is made](https://github.com/ethereum-optimism/optimism/blob/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle/oracle/base_fee.go#L72) and the gas price is updated. In summary, the l1 gas price updates a little less frequently than every block if there's more than a 5% change from the previous update. ### Updating the L2 gas price The main loop ```go= 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() } } } ``` The default value for `config.epochLengthSeconds` is 10 seconds as per the [README](https://github.com/ethereum-optimism/optimism/tree/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle). So what does the `Update` method do? ```go= 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 } ``` We need to dig a layer deeper into the GasPriceUpdater and the `UpdateGasPrice` method. ```go= 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 } ``` 1. It gets the `latestBlockNumber` and checks to see that it is greater than previously stored `epochStartBlockNumber`. 2. The total amount of gas used betweeen `epochStartBlockNumber` and the `latestBlockNumber` is calculated by getting the gas used in each block in the range and summing it together. 3. `averageGasPerSecond` is calculated by dividing the totalGasUsed by the `epochLengthSeconds` which is also the L2 update gas price loop length. 4. The gas pricer `CompleteEpoch` method is called the calculated `averageGasPerSecond`. More on this below. 5. `epochStartBlockNumber` is updated to be `latestBlockNumber`. 6. An update function is called which actually updates the L2 gas price. To go a bit deeper on [`CompleteEpoch`](https://github.com/ethereum-optimism/optimism/blob/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle/gasprices/l2_gas_pricer.go#L78-L86): ```go= 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 } ``` Looks like we should take a peek at [`CalcNextEpochGasPrice`](https://github.com/ethereum-optimism/optimism/blob/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle/gasprices/l2_gas_pricer.go#L46): ```go= 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 } ``` 1. Get the `targetGasPerSecond`. This is currently set via a [config variable](https://github.com/ethereum-optimism/optimism/blob/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle/oracle/gas_price_oracle.go#L219) whose default value is [11000000](https://github.com/ethereum-optimism/optimism/tree/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle). 2. Do some basic santiy checks around the previously calculated `averageGasPerSecond`. 3. Get the proportion difference between `averageGasPerSecond` and `targetGasPerSecond`. 4. Calculate the `proportionToChangeBy` (how much to change L2 gas price). This amount of possible percent change per epooch is bounded by a [config variable](https://github.com/ethereum-optimism/optimism/blob/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle/gasprices/l2_gas_pricer.go#L63) whose default is set to [.1](https://github.com/ethereum-optimism/optimism/tree/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle). 5. Based on `proportionToChangeBy`, calculate the new L2 gas price and ensure that it's not smaller than the `floorPrice` which is also set via a [config variable](https://github.com/ethereum-optimism/optimism/blob/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle/oracle/gas_price_oracle.go#L217) whose default is [1](https://github.com/ethereum-optimism/optimism/tree/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle). 6. Return the new L2 gas price. Moving back up to`CompleteEpoch` (see above), the returned gas price is stored as is the `averageGasPerSecond` from what will have been the previous epoch. Zooming way back to update, [logging](https://github.com/ethereum-optimism/optimism/blob/e0f7e5f43388d489de8882c316c24b17d489a24d/go/gas-oracle/oracle/gas_price_oracle.go#L171) is done (probably?) for debugging, but that's it! ## Optimizing Gas Today The price of gas on L1 is often [50K-100K](https://public-grafana.optimism.io/d/9hkhMxn7z/public-dashboard?orgId=1&refresh=5m) times more expensive than it is on Optimism. Given that, it could make sense to minimize calldata as each byte adds extra cost to the L1 data fee. While it might not make sense for project to modify its core contracts across chains due to increased maintenance burden, there might be an interesting opportunity to write different periphery contracts that minimize calldata. ## Looking forward A non-exhaustive list of efforts that could bring meaningful reductions to gas costs on Optimism: - [EIP-4488](https://eips.ethereum.org/EIPS/eip-4488) seeks to reduce the cost of calldata to 3 gas per byte which is much lower than the 4 gas per zero-byte and 16 gas per non-zero byte today. - [Stateless compression of tx data](https://github.com/ethereum-optimism/optimistic-specs/issues/161)