# Converging to the Target
How gas markets and block fullness have been impacted by the merge.
An analysis of the gas market pre v.s. post-merge for the [merge data challenge](https://esp.ethereum.foundation/merge-data-challenge). Focus in this post is on the execution layer, with all data obtained by querying execution APIs from Web3 providers.
## TL;DR
Post-merge there are a few noticeable changes in the gas market:
- The shape of the `gas_filled = block.gas_used / block.gas_limit` density function (PDF) seems to have changed. It is more Gaussian-like around the gas filled target of `1 / ELASTICITY_MULTIPLIER = 0.5` from the [EIP-1559 spec](https://eips.ethereum.org/EIPS/eip-1559), whereas pre-merge shape appears near uniform (ignoring completely empty and full blocks) with a slight skew toward empty blocks.
- Gas limit variability has decreased dramatically -- standard deviation has reduced by ~93%.
- Base fee prices appear to be exhibiting similar distributional properties to those of traditional financial assets (i.e. log price changes between blocks potentially trending toward a typical [Wiener process](https://en.wikipedia.org/wiki/Wiener_process)).
But more data is needed to verify these trends.
## Intro
Ethereum transitioned from proof-of-work (PoW) to proof-of-stake (PoS) on September 15, 2022. [The merge](https://ethereum.org/en/upgrades/merge/) resulted in the joining of the original execution layer of the protocol with a new consensus layer, the [beacon chain](https://ethereum.org/en/upgrades/beacon-chain/).
With the transition to PoS came a few changes: particularly relevant for this analysis, the network changed from random block times (~13 second Poisson process) to [fixed block times occurring every 12 seconds](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/#validators) (in consensus layer lingo, "slots").
I examined how those changes may have affected the gas market. This post aims to answer the following questions:
- How has the block gas limit changed through the merge?
- What does ["block fullness"](https://www.reddit.com/r/ethereum/comments/p4nloh/why_has_the_chain_capacity_increased_by_9_after/?utm_source=share&utm_medium=web2x&context=3) look like pre v.s. post-merge?
- Did the merge have any effect on fee prices?
## Setup
I used the [`ape-notebook`](https://github.com/ApeWorX/ape-notebook) plugin from [ApeWorX](https://github.com/ApeWorX/ape) to gather and analyze the data.
I cannot express just how awesome `ape` + `ape-notebook` is from a data science perspective. The combo allows you to gather and analyze on-chain data from a single Jupyter notebook setup.
Clone the [repo](https://github.com/fmrmf/merge-challenge)
```bash
git clone https://github.com/fmrmf/merge-challenge.git
cd merge-challenge
```
Install the plugins needed for the project:
```bash
pipx install eth-ape'[recommended-plugins]'
pipx runpip eth-ape install notebook matplotlib scipy
ape plugins install .
```
And launch Jupyter notebook
```bash
ape notebook
```
`ape`'s provider functionality should work out of the box, funneling Web3 queries into `pandas.DataFrame`.
You'll need an [Alchemy](https://www.alchemy.com) API key if using the provider I chose as default for the repo:
```bash
export WEB3_ALCHEMY_PROJECT_ID=MY_ALCHEMY_PROJECT_ID
```
or simply choose a different Ape provider for the execution layer (e.g. [Infura](https://github.com/ApeWorX/ape-infura#quick-usage) or Geth).
## Methods
There are two sections to the analysis:
1. Block data
2. Transaction data within blocks
I looked at execution layer block data over a two month timespan from August 14, 2022 to October 15, 2022 (from block `15338009` to `15754058`) for the first part of the analysis. It's rather trivial to query for this data using `ape`'s chain manager:
```python
from ape import chain
qb = chain.blocks.query('*', start_block=15338009, stop_block=15754058)
```
The returned value is a `pandas.DataFrame`
```
>>> qb
num_transactions hash ... difficulty total_difficulty
0 97 b'\xf0\x82\x95\x1e@\xe4by\xfc,\x15\xd6\xb6xFw\... ... 12045723921070914 56321843715293942409414
1 305 b'\xdb\x91:\xfc8Qk\xc1c\x82\xe2\xdbA\xe9b#R\xe... ... 12051743061157721 56321855767037003567135
2 51 b'\xb0\x0c2\x7f$\xc0\xec$i\xbf\xd9\xcb\x13\x0c... ... 12045995859944613 56321867813032863511748
3 24 b"0\xbc\xf0\xa6\x89\xf1w\x1d\x9d\x05\x1c\xc8\x... ... 12057896966730061 56321879870929830241809
4 63 b'`/\xd0\xe0\x7fvc>\x94\xf2\xd1\xca\xbf\x11\x9... ... 12063922050686819 56321891934851880928628
... ... ... ... ... ...
416045 189 b'\xecP\xf7g\x08\xc2\x19\x12_\x0c\xd8X\x92\x9b... ... 0 58750003716598352816469
416046 253 b'n\xf2R{ \xa9t\xabUD\xbc\x8e\x04B\x87\xb3\t\x... ... 0 58750003716598352816469
416047 137 b'\xb7\x06\xba9s\x7f>p\xd1\xa3\x16\xb0)Y\n\xd4... ... 0 58750003716598352816469
416048 117 b'\x86\xd9n\xfb+\x9e\n\xbc\xb3\x99\x155@\xbb\x... ... 0 58750003716598352816469
416049 132 b'\xb4G\x9e\xa6\xa0\x18\x8d\xee\xb0\x15\x9faK\... ... 0 58750003716598352816469
[416050 rows x 11 columns]
```
which I used in the [`blocks.ipynb`](https://github.com/fmrmf/merge-challenge/blob/main/notebook/blocks.ipynb) Jupyter notebook for the bulk of the block analysis.
For transactions, I reduced the timespan I focused on to two weeks from September 9, 2022 to September 21, 2022 (from block `15500283` to `15580183`), given the amount of data involved.
To query for transaction data within each block, I had to get a bit more creative (probably not the best way, but needed something), as I continued using `ape`'s chain manager functionality. I implemented a transaction container query function
```python
import pandas as pd
from functools import partial
from hexbytes import HexBytes
from typing import List
from ape.api.providers import BlockAPI, TransactionAPI
from ape.api.query import BlockTransactionQuery, extract_fields, validate_and_expand_columns
# for each block in blocks query, query for transactions
def transaction_container_query(block, *columns: List[str]) -> pd.DataFrame:
"""
Implements what could be a transaction "container" query analogous
to https://github.com/ApeWorX/ape/blob/main/src/ape/managers/chain.py#L94
but for transactions.
"""
# perform BlockTransactionQuery
# SEE: https://github.com/ApeWorX/ape/blob/main/src/ape/api/providers.py#L92
query = BlockTransactionQuery(columns=columns, block_id=int(block.number))
transactions = chain.query_manager.query(query) # use chain here so block can be row in pd.DataFrame
# put into a dataframe and return
columns = validate_and_expand_columns(columns, TransactionAPI)
transactions = map(partial(extract_fields, columns=columns), transactions)
df = pd.DataFrame(columns=columns, data=transactions)
# add in columns for block number and block hash then return
df['block_hash'] = [ block.hash for i in range(len(df)) ]
df['block_number'] = [ block.number for i in range(len(df)) ]
return df
```
and iterated through all blocks used in the block analysis appending each new transaction query `pandas.DataFrame` result to a csv file:
```python
for _, b in qb.iterrows():
qt = transaction_container_query(b, '*')
# append to local csv
# NOTE: qt is of type pd.DataFrame
qt.to_csv('data/transactions.csv', mode='a', index=False, header=False)
```
I used this csv for the bulk of the analysis in [`transactions.ipynb`](https://github.com/fmrmf/merge-challenge/blob/main/notebook/transactions.ipynb). Block data was also kept in a csv just in case I messed up somewhere along the way.
In plots where I contrasted pre v.s. post-merge results, I used different colors: orange for pre-merge data and green for post-merge data.
## Transition to Fixed Block Times
The block at which the merge happened can be found from the first occurence in the block dataframe of [`difficulty = 0`](https://eips.ethereum.org/EIPS/eip-3675#replacing-difficulty-with-0) (block `15537394`). To start the analysis, I sanity checked a few known details about the merge: particularly, the transition from variable to fixed block times of 12 seconds and an associated increase in blocks per day of [about 8%](https://notes.ethereum.org/@djrtwo/merge-data-comp-wish).
To obtain the time between successive blocks (i.e. block time), I took the difference in timestamps
```
qb['dtimestamp'] = qb['timestamp'].diff()
```
Plotting these time deltas clearly showed the transition to fixed block times after the merge
| ![Scatterplot of time between blocks pre v.s. post-merge](https://i.imgur.com/ngNo8ou.png) |
|:--:|
| Scatter plot of time between successive blocks pre v.s. post-merge. Pre in orange, post in green. |
Those horizontal lines after block `15537394` should occur at integer multiples of 12 for a transition to fixed slot times of 12 seconds to have occurred. When I focused on post-merge block times (in green below), it's very clear this is the case:
```python
merge_block_number = qb[qb['difficulty'] == 0]['number'].iloc[0]
qb[qb['number'] >= merge_block_number].plot(x='number', y='dtimestamp', kind='scatter', s=0.1, color='C2', label='block time (post-merge)')
```
| ![Scatterplot of time between blocks post-merge](https://i.imgur.com/HbDNsIt.png) |
|:--:|
| Scatter plot of time between successive blocks post-merge. |
where a multiple > 1 results from a missed slot.
This was also easy to confirm with a bit of manipulation of the block dataframe:
```python
>>> qb[(qb['number'] > merge_block_number) & (qb['dtimestamp'] % 12 != 0)]['dtimestamp'].count()
0
```
So the network has definitely transitioned to fixed block times post-merge. Note that I've excluded the block time for the merge block itself since the time delta would be in reference to the last PoW block just prior to merge (happens to be 17 sec).
When examining the fraction of blocks with block times equal to `12, 24, 36 sec` over the course of one month post-merge,
- ~99.27% of blocks were produced at the next slot (12 sec time difference)
- ~0.72% of blocks missed one slot (24 sec time difference)
- ~0.01% of blocks missed two slots (36 sec time difference)
There were no occurrences of block times > 36 secs (more than 2 slots missed).
To check for an associated increase in blocks per day due to the decrease in average block time, I resampled the data to 1 day intervals aggregating on number of blocks produced
```python
qb['date'] = pd.to_datetime(qb['timestamp'], unit='s')
qb_block_count = qb.set_index('date').resample('1D').aggregate({'number': 'count'})[1:-1]
```
Plotting this for pre v.s. post-merge data shows the large jump in daily block production through the merge:
| ![Plot of blocks produced per day pre v.s. post-merge](https://i.imgur.com/KTWlgWp.png) |
|:--:|
| Plot of blocks produced per day pre v.s. post-merge. Pre in orange, post in green. |
There were 6241 blocks per day on average over the month prior to the merge and 7148 blocks per day on average over the month following the merge. This represents a ~14.5% increase in block production, which at first glance seems significantly higher than the expected increase of ~8% from ~13 sec random block times to 12 sec fixed block times.
However, an average of 6241 blocks produced per day pre-merge represents an average block time of 13.84 sec. If compared instead with a hypothetical 13 sec average block time pre-merge (or 6646 blocks per day), an increase of ~7.5% relative to this hypothetical block production average has occurred. This is in line with expectations given the inferred average block time observed post-merge of 12.09 sec is slightly above 12 sec due to missed slots.
## Gas Limit Variability
For the rest of this post, I'll focus on the original intention of the analysis: how gas markets have been impacted by the merge.
To dive deeper into the topic, it helps to reference the [EIP 1559 specification](https://eips.ethereum.org/EIPS/eip-1559), as on average ~83% (median 84-85%) of txns included in a block near the merge are of `type = 0x02`, following 1559:
| ![Histogram of fraction of 1559 txns in a block (pre v.s. post-merge)](https://i.imgur.com/Y0n3jqW.png) |
|:--:|
| Histograms of fraction of 1559 txns in a block pre v.s. post-merge (September 9, 2022 to September 21, 2022). Pre in orange, post in green. |
For EIP 1559, fees are segmented into two components:
- `block.base_fee`
- `tx.max_priority_fee`
The base fee per gas is the variable fee amount in each block that all transactions in the block must pay at a minimum. It moves up or down each block as a function of the gas used in the parent block and a "gas target" which the protocol targets for `block.gas_used`: the gas target is simply the `block.gas_limit` (absolute limit on the amount of gas that can be used in the block) divided by an elasticity multiplier set to `ELASTICITY_MULTIPLIER = 2`. The base fee increases when the gas used within a block is more than the intended target and decreases when less to counter changes in demand.
The max priority fee is the maximum per transaction tip to miners/validators users are willing to pay to incentivize inclusion of their transaction in a block. It is an additional fee each transaction pays on top of the block's base fee.
I took a look at the behavior of the block gas limit first. The EIP 1559 specification provides some flexibility on the actual value of the `block.gas_limit` that can be used, so that it's not a constant cap in practice but rather bound stringently. Taken from the spec (you can see the implementation in [`geth`](https://github.com/ethereum/go-ethereum/blob/master/core/block_validator.go#L108) as well):
```python
# check if the block changed the gas limit too much
assert block.gas_limit < parent_gas_limit + parent_gas_limit // 1024, 'invalid block: gas limit increased too much'
assert block.gas_limit > parent_gas_limit - parent_gas_limit // 1024, 'invalid block: gas limit decreased too much'
```
Basically, the deviation in the gas limit from the previous parent block should not exceed `1/1024 = 0.0009765625`.
Through the merge, the realized values for this deviation actually decreased dramatically. I plotted `block.gas_limit` over one month prior to one month post-merge to see the difference alongside histograms of the gas limit over the same timespan:
| ![Plot of `block.gas_limit` pre v.s. post-merge](https://i.imgur.com/jkrDVMt.png) |
|:--:|
| Scatter plot of `block.gas_limit` pre v.s. post-merge (August 14, 2022 to October 15, 2022). Pre in orange, post in green. |
| ![Histograms of `block.gas_limit` pre v.s. post-merge](https://i.imgur.com/RqlQNOC.png) |
|:--:|
| Histograms of `block.gas_limit` pre v.s. post-merge (August 14, 2022 to October 15, 2022). Pre in orange, post in green. |
Post-merge `gas_limit` values have been far more concentrated around `30_000_000` gas (post-merge looks almost like a dirac delta in comparison). Performing some statistics showed the standard deviation in the gas limit had dropped by ~93%
``` python
>>> qb[qb['number'] < merge_block_number]['gas_limit'].describe()
count 1.993850e+05
mean 3.000275e+07
std 2.627404e+04
min 2.973731e+07
25% 2.999997e+07
50% 3.000000e+07
75% 3.000000e+07
max 3.020567e+07
Name: gas_limit, dtype: float64
>>> qb[qb['number'] >= merge_block_number]['gas_limit'].describe()
count 2.166650e+05
mean 2.999989e+07
std 1.881200e+03
min 2.991220e+07
25% 3.000000e+07
50% 3.000000e+07
75% 3.000000e+07
max 3.002930e+07
Name: gas_limit, dtype: float64
```
from 26274.04 gas to 1881.2 gas, a dramatic decrease from pre-merge.
It's not entirely clear to me whether there's a known reason for this substantial drop (I'm likely missing something others are aware of), but it is rather interesting when coupled with the transition from random to fixed block times.
<!-- TODO: why might variability in gas limit have happened. (It's not really clear to me why this might happen but possibly fixed block times? same reason as block fullness below) what does gas limit variability signal? -->
## Shape of Block Fullness
Even more interesting was the impact of the merge on block fullness and the amount of gas used in each block. To measure block fullness, I looked at a per block `gas_filled` quantity
```python
qb['gas_filled'] = qb['gas_used'] / qb['gas_limit']
```
that represents the amount of gas used in a block relative to the gas limit. EIP 1559 specifies that the gas target be exactly half the block gas limit
```python
parent_gas_target = self.parent(block).gas_limit // ELASTICITY_MULTIPLIER
parent_gas_limit = self.parent(block).gas_limit
```
so that blocks should be [close to 50% full on average](https://www.reddit.com/r/ethereum/comments/p4nloh/why_has_the_chain_capacity_increased_by_9_after/?utm_source=share&utm_medium=web2x&context=3) (i.e. `qb['gas_filled'] ~ 0.5`) if 1559 txns are effectively tracking the gas target. The scatter plot of block fullness provided a hint for what changes may have happened post-merge:
| ![Scatterplot of gas filled (pre v.s. post-merge)](https://i.imgur.com/x6OQpEN.png) |
|:--:|
| Scatter plot of `gas_filled = block.gas_used / block.gas_limit` pre v.s. post-merge (August 14, 2022 to October 15, 2022). Pre in orange, post in green. |
It's a bit subtle, but notice the darker shades of green near the target fullness value of 50% post-merge, resembling a lack of uniformity (possibly Gaussian in nature). Contrast this with a lack of concentration in realized values anywhere between (but excluding) 0 and 1 on the orange pre-merge side of the plot. Lack of concentration suggests uniformity of the pre-merge distribution for block fullness, in line with [prior analyses](https://mipasa.unbounded.network/featured/London-Hard-Fork-Analysis).
Histograms including 0% and 100% for block fullness hinted again at a significant change in the fullness distributions near the target level. Pre-merge still looked similar to [this tweet](https://twitter.com/VitalikButerin/status/1423568180572684288?s=19) from one year ago. However, post-merge appeared to have shifted with far more realized values near the target fullness of 50%:
| ![Histogram of gas filled (pre-merge)](https://i.imgur.com/ludrrDl.png) |
|:--:|
| Histogram of `gas_filled = block.gas_used / block.gas_limit` pre-merge (August 14, 2022 to September 15, 2022). |
| ![Histogram of gas filled (post-merge)](https://i.imgur.com/Gm9zMyV.png) |
|:--:|
| Histogram of `gas_filled = block.gas_used / block.gas_limit` post-merge (September 15, 2022 to October 15, 2022). |
Ignoring the extremes of completely full and empty blocks (i.e. filter out `gas_filled = 0, 1`) to view the the bulk of the randomness in block fullness provided confirmation of a likely significant change post-merge. I filtered out block fullness less than 1% and greater than 99% from the dataset
```python
qb[(qb['gas_filled'] > 0.01) & (qb['gas_filled'] < 0.99)]
```
Then I plotted the results for the estimated density functions pre v.s. post-merge:
| ![Density function estimates of gas limit filled (pre v.s. post-merge)](https://i.imgur.com/bYXFYgK.png) |
|:--:|
| Density function estimates of `gas_filled = block.gas_used / block.gas_limit` pre v.s. post-merge (August 14, 2022 to October 15, 2022), ignoring completely full and empty blocks. Pre in orange, post in green. |
The difference is really interesting. Post-merge the randomness in block fullness appears far more bell-shaped in nature, concentrating close to the target level. The density function plots supported the prior intuition from the scatter plots. Empirical CDFs pre v.s. post-merge also seem to suggest a trend toward normality around the target level, but it does appear too early to tell whether this trend will continue.
| ![Empirical CDFs of gas limit filled (pre v.s. post-merge)](https://i.imgur.com/4v2kqpB.png) |
|:--:|
| Empirical CDFs of `gas_filled = block.gas_used / block.gas_limit` pre v.s. post-merge (August 14, 2022 to October 15, 2022), ignoring completely full and empty blocks. Pre in orange, post in green. |
I used the mean and standard deviation from the observed sample statistics to superimpose a Gaussian CDF for comparison in the empiricial CDF plots. Observed sample statistics for block fullness were:
```python
>>> qb[(qb['number'] < merge_block_number) & (qb['gas_filled'] < 0.99) & (qb['gas_filled'] > 0.01)]['gas_filled'].describe()
count 151423.000000
mean 0.416293
std 0.269158
min 0.010004
25% 0.187437
50% 0.376640
75% 0.620047
max 0.989989
Name: gas_filled, dtype: float64
>>> qb[(qb['number'] >= merge_block_number) & (qb['gas_filled'] < 0.99) & (qb['gas_filled'] > 0.01)]['gas_filled'].describe()
count 196322.000000
mean 0.466560
std 0.250048
min 0.010018
25% 0.286456
50% 0.454101
75% 0.643992
max 0.989999
Name: gas_filled, dtype: float64
```
My best guess as to why this behavior could be occurring at this point was some relation between block fullness and the transition from random to fixed block times through the merge. But I was simply speculating here.
## Are Base Fees Less Variable?
Given a greater concentration in block fullness around the gas target from EIP 1559, I expected the answer to the title of this section to be yes as the smaller the `block.gas_used` deviation from the gas target, the smaller the deviation in base fees from the parent block. Detailed out in the 1559 spec:
```python
# check if the base fee is correct
if INITIAL_FORK_BLOCK_NUMBER == block.number:
expected_base_fee_per_gas = INITIAL_BASE_FEE
elif parent_gas_used == parent_gas_target:
expected_base_fee_per_gas = parent_base_fee_per_gas
elif parent_gas_used > parent_gas_target:
gas_used_delta = parent_gas_used - parent_gas_target
base_fee_per_gas_delta = max(parent_base_fee_per_gas * gas_used_delta // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR, 1)
expected_base_fee_per_gas = parent_base_fee_per_gas + base_fee_per_gas_delta
else:
gas_used_delta = parent_gas_target - parent_gas_used
base_fee_per_gas_delta = parent_base_fee_per_gas * gas_used_delta // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR
expected_base_fee_per_gas = parent_base_fee_per_gas - base_fee_per_gas_delta
assert expected_base_fee_per_gas == block.base_fee_per_gas, 'invalid block: base fee not correct'
```
Note that this does not necessarily mean lower base fees in general. Simply an expectation of less volatile base fees due to more blocks with gas usage near the gas target.
However, I was wrong in this expectation. Variability in base fees remained approximately the same through the merge. Actually, both the mean and variance of base fees increased slightly over the month prior vs month post-merge:
```python
>>> qb['base_fee_gwei'] = qb['base_fee'] / 1e9
>>> qb[qb['number'] < merge_block_number]['base_fee_gwei'].describe()
count 199385.000000
mean 12.959468
std 9.788894
min 1.130058
25% 6.477647
50% 10.156201
75% 16.247512
max 159.188203
Name: base_fee_gwei, dtype: float64
>>> qb[qb['number'] >= merge_block_number]['base_fee_gwei'].describe()
count 216665.000000
mean 13.207611
std 11.397038
min 1.706518
25% 5.353451
50% 9.719042
75% 17.075325
max 214.716125
Name: base_fee_gwei, dtype: float64
```
Histograms of base fee pre v.s. post-merge provided further clues. The base fee distribution exhibited a far more concentrated peak around smaller base fee values post-merge.
| ![Scatter plot of `block.base_fee` (pre v.s. post-merge)](https://i.imgur.com/A3WGdgt.png) |
|:--:|
| Scatter plot of `block.base_fee` pre v.s. post-merge (August 14, 2022 to October 15, 2022). Pre in orange, post in green. |
| ![Histograms of `block.base_fee` (pre v.s. post-merge)](https://i.imgur.com/iFRrO0L.png) |
|:--:|
| Histograms of `block.base_fee` pre v.s. post-merge (August 14, 2022 to October 15, 2022). Pre in orange, post in green. |
Higher order sample moments confirmed the difference:
```python
>>> print('skew (base fee; pre-merge):', qb[(qb['number'] < merge_block_number)]['base_fee_gwei'].skew())
skew (base fee; pre-merge): 2.5207615007709063
>>> print('skew (base fee; post-merge):', qb[(qb['number'] >= merge_block_number)]['base_fee_gwei'].skew())
skew (base fee; post-merge): 3.2231864373233523
>>> print('excess kurtosis (base fee; pre-merge):', qb[(qb['number'] < merge_block_number)]['base_fee_gwei'].kurt())
excess kurtosis (base fee; pre-merge): 11.82242179950007
>>> print('excess kurtosis (base fee; post-merge):', qb[(qb['number'] >= merge_block_number)]['base_fee_gwei'].kurt())
excess kurtosis (base fee; post-merge): 22.842471541385883
```
Excess kurtosis of the base fee has doubled post-merge. But intuitively it wasn't clear to me whether this was due to the merge per se, or simply an excess amount of on-chain activity post-merge (ETH did drop substantially in price).
## Log Changes in Base Fee
I altered course a bit and began to look at the *changes* in base fee price over time, as base fee gas price deltas between parent and child blocks provide the closest link to the gas target mechanism detailed in EIP 1559. I differenced the natural logarithms of successive base fee prices
```python
qb['dlog_base_fee_gwei'] = np.log(qb['base_fee_gwei']).diff()
```
equivalent to `log(block[i].base_fee / block[i-1].base_fee)` for all blocks in the query. Plotting these log base fee deltas made the shape results from the prior section click for me. Here's the scatter plot of the log change in base fees through the merge:
| ![Scatter plot of `log(block[i].base_fee/block[i-1].base_fee)` (pre v.s. post-merge)](https://i.imgur.com/Nt2A7SH.png) |
|:--:|
| Scatter plot of `log(block[i].base_fee/block[i-1].base_fee)` (August 14, 2022 to October 15, 2022). Pre in orange, post in green. |
Looks familiar right? Almost identical to the `gas_filled` scatter plots. Same for the histograms
| ![Histograms of `log(block[i].base_fee/block[i-1].base_fee)` (pre v.s. post-merge)](https://i.imgur.com/pNumFEu.png) |
|:--:|
| Histograms of `log(block[i].base_fee/block[i-1].base_fee)` (August 14, 2022 to October 15, 2022). Pre in orange, post in green. |
and estimated density functions when ignoring the extremes
| ![Histograms of `log(block[i].base_fee/block[i-1].base_fee)` (pre v.s. post-merge)](https://i.imgur.com/IS0Osqs.png) |
|:--:|
| Density function estimates of `log(block[i].base_fee/block[i-1].base_fee)` (August 14, 2022 to October 15, 2022), ignoring completely full and empty blocks. Pre in orange, post in green. |
This makes sense, of course, given the EIP 1559 base fee pricing mechanism is linearly dependent on deviations away from target block fullness. In the context of base fee prices, however, the log price change in base fees trending toward normality suggests that the price process for block space (i.e. `block.base_fee`) looks explicitly like [Geometric Brownian motion](https://en.wikipedia.org/wiki/Geometric_Brownian_motion) post-merge:
$$
\log\bigg[\frac{S(t+\tau)}{S(t)}\bigg] \sim \mu'\tau + \sigma W_\tau
$$
where $\tau = 1$ block for each data point analyzed. Though, this does not account for the extreme behavior (i.e. completely empty and full blocks) filtered out of the data set.
It seems plausible that a reason for why block fullness may be trending toward normality is that randomness in the execution layer block production process post-merge has been dedicated solely to gas pricing in the block (i.e. block times are now fixed). On the other hand, the gas price process may exhibit behavior similar to that of many financial assets. This price behavior could then be manifesting in the shape of the block fullness distribution given prices are determined directly from these realized fullness values according to EIP 1559.
## Conclusions
Post-merge, the gas market is rather intriguing to say the least. There are a few unanswered questions from this analysis that hopefully more data will help with.
1. Is the shape of the block fullness distribution (ignoring completely empty and full blocks) actually trending towards a more Gaussian-like distribution?
2. Does this potential change in shape have to do with the gas pricing mechanism from EIP 1559?
3. Is the base fee price process driving this shape change closer toward normality? And is it manifesting post-merge due to the transition to fixed block times?
## References
- [EIP-3675: Upgrade consensus to Proof-of-Stake (Kalinin, Ryan, and Buterin)](https://eips.ethereum.org/EIPS/eip-3675)
- [EIP-1559: Fee market change for ETH 1.0 chain (Buterin et al)](https://eips.ethereum.org/EIPS/eip-1559)
- [EIP 1559 FAQ (Buterin)](https://notes.ethereum.org/@vbuterin/eip-1559-faq)
- [EIP 1559 Simulations (Monnot)](https://ethresear.ch/t/eip-1559-simulations/7280)
- [Has EIP-1559 Fulfilled Its Objectives? (Pintail)](https://pintail.xyz/posts/gas-market-analysis/)
- [Why has the chain capacity increased by ~9% after London? Three answers... (Buterin r/ethereum)](https://www.reddit.com/r/ethereum/comments/p4nloh/why_has_the_chain_capacity_increased_by_9_after/?utm_source=share&utm_medium=web2x&context=3)
- [London Hard Fork Analysis (MiPasa)](https://mipasa.unbounded.network/featured/London-Hard-Fork-Analysis)
## Acknowledgements
Many thanks to [fubuloubu](https://github.com/fubuloubu) and the ApeWorX team for building something incredible and for kindly answering many of my uninformed questions on Discord.