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 and special thanks to Mark Tyneway for the review
OVM_GasPriceOracle.sol
OVM_GasPriceOracle
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:
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:
Directly from the service README:
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, you'll notice there's some code to initialize the Gas Price Oracle and some code to start it.
The Start
method has the option of running two loops, one to update L1 data fee price and a second to update the L2 gas price.
The core loop code is the following:
Nothing too unusual but worth noting that the updateBaseFee
function is called every 15 seconds which is slighly longer than the average block time 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.
If there is a difference, a contract call is made 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.
The main loop
The default value for config.epochLengthSeconds
is 10 seconds as per the README.
So what does the Update
method do?
We need to dig a layer deeper into the GasPriceUpdater and the UpdateGasPrice
method.
latestBlockNumber
and checks to see that it is greater than previously stored epochStartBlockNumber
.epochStartBlockNumber
and the latestBlockNumber
is calculated by getting the gas used in each block in the range and summing it together.averageGasPerSecond
is calculated by dividing the totalGasUsed by the epochLengthSeconds
which is also the L2 update gas price loop length.CompleteEpoch
method is called the calculated averageGasPerSecond
. More on this below.epochStartBlockNumber
is updated to be latestBlockNumber
.To go a bit deeper on CompleteEpoch
:
Looks like we should take a peek at CalcNextEpochGasPrice
:
targetGasPerSecond
. This is currently set via a config variable whose default value is 11000000.averageGasPerSecond
.averageGasPerSecond
and targetGasPerSecond
.proportionToChangeBy
(how much to change L2 gas price). This amount of possible percent change per epooch is bounded by a config variable whose default is set to .1.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 whose default is 1.Moving back up toCompleteEpoch
(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 is done (probably?) for debugging, but that's it!
The price of gas on L1 is often 50K-100K 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.
A non-exhaustive list of efforts that could bring meaningful reductions to gas costs on Optimism: