(thanks @jochem-brouwer for inputs and feedback to get things a bit right here)
EIP 4396 (cc: @adietrichs) provides for a way to compensate missed blocks by having an parent_adjusted_gas_target
to factor into the base fee calculations. While it accommodates the unused capacity by missed slots and delivers the benefit to N+2 for the block missed at N (by expanding the gas target at N+1) upto a limit, we can go further and track unused target gas and pay it forward to the next blocks.
note: its possible to modify the calculations to pay the benefit to N+1, but then it increases incentive for N+1 to not build on N even if it has seen it
New proposal
(Old proposal tracks and accumulates even the unused target in a block but thanks to feedback and discussions with @MicahZoltu realized that unused target of a block is already benefiting and lowering baseFee for the next block. however unused target of a block can still be used to raise the gasLimit of the next block but there is a problem that builders could try to under-utilize space to provide them the ability to pack more at the next block on lower fees. So the new proposal doesn’t attempt accumulating the unused target of a block in any way)
So new proposal just tracks and accumulates the unused targets because of the missed blocks.
BLOCK_TIME_TARGET = 12
# no change in previous constants
ELASTICITY_MULTIPLIER = ...
BASE_FEE_MAX_CHANGE_DENOMINATOR =
def buildBlock(...)
# make the extra target available to the block upto gasLimit
blockGasTarget = block.gasLimit // ELASTICITY_MULTIPLIER
blockAvailableGasTarget = min (blockGasTarget + parent.unusedGasTarget, block.gasLimit)
...
# update unusedGasTarget by accumulating if there are any misses and removing extra target available to this block
block.unusedGasTarget =
max(0,
parent.unusedGasTarget
# if there are no missed block, the following segment evals to 0
+ (block.timestamp - parent.timestamp - BLOCK_TIME_TARGET) // BLOCK_TIME_TARGET * blockGasTarget
- (blockAvailableGasTarget - blockGasTarget))
return block
base fee calculation changes as below
def calcBaseFee (...)
...
parentBlockGasTarget = parent.gasLimit // ELASTICITY_MULTIPLIER
parentAvailableGasTarget = min (parentBlockGasTarget + parent.parent.unusedGasTarget, parent.gasLimit)
gasUsedDelta = parent.gasUsed - parentAvailableGasTarget
gasUsedDeltaDenominator = parent.gasLimit // ELASTICITY_MULTIPLIER
feeDelta = parent.baseFee * gasUsedDelta / gasUsedDeltaDenominator / BASE_FEE_MAX_CHANGE_DENOMINATOR
# min feeDelta is enforced as before is gas used > available gas target
if (parent.gasUsed > parentAvailableGasTarget)
feeDelta = max(feeDelta, 1)
# min base fee is enforced as before
baseFee = max (parent.baseFee + feeDelta, MIN_BASE_FEE)
return baseFee
Old proposal
So a block builder not fully utilizing the blockspace wouldn’t mean lost capacity to the network. We can do this by tracking unusedGasTarget
on the lines of excessBlobGas
, i.e. a cumulative tracker of the previous unused blockspace capacity (limited by some max limit since blocks in a slot need to be build under a strict window)
so essentially:
BLOCK_TIME_TARGET = 12
# no change in previous constants
ELASTICITY_MULTIPLIER = ...
BASE_FEE_MAX_CHANGE_DENOMINATOR =
# we allow the block to expand to some `hardGasLimit` than the provided `block.gasLimit` i.e. bigger blocks than usual to compensate for past missed slots or less throughput i.e. only when there is some unused available gas
HARD_GAS_LIMIT_MULTIPLIER = 3
def buildBlock(...)
# block's normal gas limit tracking remains unchanged (as provided by fcU)
block.gasLimit = ...
hardGasLimit = block.gasLimit * HARD_GAS_LIMIT_MULTIPLIER
# however modified gas limit is available to build the block
availableGasTarget = min (parent.unusedGasTarget + (block.timestamp - parent.timestamp) // BLOCK_TIME_TARGET * (block.gasLimit // ELASTICITY_MULTIPLIER), hardGasLimit)
# if ELASTICITY_MULTIPLIER=2 this can be collapsed into a smaller calc
availableGasLimit = min ( availableGasTarget + block.gasLimit - block.gasLimit // ELASTICITY_MULTIPLIER, hardGasLimit)
...
#
block.unusedGasTarget = max (availableGasTarget - block.gasUsed, 0)
return block
block validations are now
def validateBlock(...)
...
# calculate the available gas limit as above
availableGasLimit = ...
assert ( block.gasUsed <= availableGasLimit )
and base fee calculation is now
def calcBaseFee (...)
...
# calc what gas target was available to parent
parentAvailableGasTarget = ....
gasUsedDelta = parent.gasUsed - parentAvailableGasTarget
gasUsedDeltaDenominator = parent.gasLimit // ELASTICITY_MULTIPLIER
feeDelta = parent.baseFee * gasUsedDelta / gasUsedDeltaDenominator / BASE_FEE_MAX_CHANGE_DENOMINATOR
# min feeDelta is enforced as before is gas used > available gas target
if (parent.gasUsed > parentAvailableGasTarget)
feeDelta = max(feeDelta, 1)
# min base fee is enforced as before
baseFee = max (parent.baseFee + feeDelta, MIN_BASE_FEE)
return baseFee
Open to improving the calculations and make them more precise/better to reflect the intend