Skip to content

Commit

Permalink
handle holocene EIP-1559 params
Browse files Browse the repository at this point in the history
  • Loading branch information
roberto-bayardo committed Oct 9, 2024
1 parent 9eb6114 commit 45bf7f7
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 49 deletions.
6 changes: 6 additions & 0 deletions beacon/engine/gen_blockparams.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions beacon/engine/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@ type PayloadAttributes struct {
NoTxPool bool `json:"noTxPool,omitempty" gencodec:"optional"`
// GasLimit is a field for rollups: if set, this sets the exact gas limit the block produced with.
GasLimit *uint64 `json:"gasLimit,omitempty" gencodec:"optional"`
// EIP1559Params is a field for rollups implementing the Holocene upgrade,
// and contains encoded EIP-1559 parameters. See:
// https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md#eip1559params-encodingq
EIP1559Params []byte `json:"eip1559Params,omitempty" gencodec:"optional"`
}

// JSON type overrides for PayloadAttributes.
type payloadAttributesMarshaling struct {
Timestamp hexutil.Uint64

Transactions []hexutil.Bytes
GasLimit *hexutil.Uint64
Transactions []hexutil.Bytes
GasLimit *hexutil.Uint64
EIP1559Params hexutil.Bytes
}

//go:generate go run github.com/fjl/gencodec -type ExecutableData -field-override executableDataMarshaling -out gen_ed.go
Expand Down
27 changes: 22 additions & 5 deletions consensus/misc/eip1559/eip1559.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package eip1559

import (
"encoding/binary"
"errors"
"fmt"
"math/big"
Expand Down Expand Up @@ -55,15 +56,31 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade
return nil
}

// DecodeHolocene1599Params extracts the Holcene 1599 parameters from the encoded form:
// https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md#eip1559params-encoding
func DecodeHolocene1559Params(nonce types.BlockNonce) (uint64, uint64) {
elasticity := binary.BigEndian.Uint32(nonce[:4])
denominator := binary.BigEndian.Uint32(nonce[4:])
return uint64(elasticity), uint64(denominator)
}

// CalcBaseFee calculates the basefee of the header.
// The time belongs to the new block to check if Canyon is activted or not
// The time belongs to the new block to check which upgrades are active.
func CalcBaseFee(config *params.ChainConfig, parent *types.Header, time uint64) *big.Int {
// If the current block is the first EIP-1559 block, return the InitialBaseFee.
if !config.IsLondon(parent.Number) {
return new(big.Int).SetUint64(params.InitialBaseFee)
}

parentGasTarget := parent.GasLimit / config.ElasticityMultiplier()
elasticity := config.ElasticityMultiplier()
denominator := config.BaseFeeChangeDenominator(time)
if config.IsHolocene(time) {
// Holocene requires we get the 1559 parameters from the nonce field of parent header,
// unless the field is zero, in which case we use the Canyon values.
if parent.Nonce != types.BlockNonce([8]byte{}) {
elasticity, denominator = DecodeHolocene1559Params(parent.Nonce)
}
}
parentGasTarget := parent.GasLimit / elasticity
// If the parent gasUsed is the same as the target, the baseFee remains unchanged.
if parent.GasUsed == parentGasTarget {
return new(big.Int).Set(parent.BaseFee)
Expand All @@ -80,7 +97,7 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header, time uint64)
num.SetUint64(parent.GasUsed - parentGasTarget)
num.Mul(num, parent.BaseFee)
num.Div(num, denom.SetUint64(parentGasTarget))
num.Div(num, denom.SetUint64(config.BaseFeeChangeDenominator(time)))
num.Div(num, denom.SetUint64(denominator))
baseFeeDelta := math.BigMax(num, common.Big1)

return num.Add(parent.BaseFee, baseFeeDelta)
Expand All @@ -90,7 +107,7 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header, time uint64)
num.SetUint64(parentGasTarget - parent.GasUsed)
num.Mul(num, parent.BaseFee)
num.Div(num, denom.SetUint64(parentGasTarget))
num.Div(num, denom.SetUint64(config.BaseFeeChangeDenominator(time)))
num.Div(num, denom.SetUint64(denominator))
baseFee := num.Sub(parent.BaseFee, num)

return math.BigMax(baseFee, common.Big0)
Expand Down
52 changes: 52 additions & 0 deletions consensus/misc/eip1559/eip1559_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package eip1559

import (
"encoding/binary"
"math/big"
"testing"

Expand Down Expand Up @@ -61,6 +62,8 @@ func opConfig() *params.ChainConfig {
ct := uint64(10)
eip1559DenominatorCanyon := uint64(250)
config.CanyonTime = &ct
ht := uint64(12)
config.HoloceneTime = &ht
config.Optimism = &params.OptimismConfig{
EIP1559Elasticity: 6,
EIP1559Denominator: 50,
Expand Down Expand Up @@ -174,5 +177,54 @@ func TestCalcBaseFeeOptimism(t *testing.T) {
if have, want := CalcBaseFee(opConfig(), parent, parent.Time+2), big.NewInt(test.expectedBaseFee); have.Cmp(want) != 0 {
t.Errorf("test %d: have %d want %d, ", i, have, want)
}
if test.postCanyon {
// make sure Holocene activation doesn't change the outcome; since these tests have a
// zero nonce, they should be handled using the Canyon config.
parent.Time = 10
if have, want := CalcBaseFee(opConfig(), parent, parent.Time+2), big.NewInt(test.expectedBaseFee); have.Cmp(want) != 0 {
t.Errorf("test %d: have %d want %d, ", i, have, want)
}
}
}
}

func encodeHolocene1559Params(elasticity, denom uint32) types.BlockNonce {
var nonce types.BlockNonce
binary.BigEndian.PutUint32(nonce[:4], elasticity)
binary.BigEndian.PutUint32(nonce[4:], denom)
return nonce
}

// TestCalcBaseFeeHolocene assumes all blocks are Optimism blocks post-Holocene upgrade
func TestCalcBaseFeeOptimismHolocene(t *testing.T) {
elasticity2Denom10Nonce := encodeHolocene1559Params(2, 10)
elasticity10Denom2Nonce := encodeHolocene1559Params(10, 2)
parentBaseFee := int64(10_000_000)
parentGasLimit := uint64(30_000_000)

tests := []struct {
parentGasUsed uint64
expectedBaseFee int64
nonce types.BlockNonce
}{
{parentGasLimit / 2, parentBaseFee, elasticity2Denom10Nonce}, // target
{10_000_000, 9_666_667, elasticity2Denom10Nonce}, // below
{20_000_000, 10_333_333, elasticity2Denom10Nonce}, // above
{parentGasLimit / 10, parentBaseFee, elasticity10Denom2Nonce}, // target
{1_000_000, 6_666_667, elasticity10Denom2Nonce}, // below
{30_000_000, 55_000_000, elasticity10Denom2Nonce}, // above
}
for i, test := range tests {
parent := &types.Header{
Number: common.Big32,
GasLimit: parentGasLimit,
GasUsed: test.parentGasUsed,
BaseFee: big.NewInt(parentBaseFee),
Time: 10,
Nonce: test.nonce,
}
if have, want := CalcBaseFee(opConfig(), parent, parent.Time+2), big.NewInt(test.expectedBaseFee); have.Cmp(want) != 0 {
t.Errorf("test %d: have %d want %d, ", i, have, want)
}
}
}
11 changes: 9 additions & 2 deletions eth/catalyst/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,15 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl
// sealed by the beacon client. The payload will be requested later, and we
// will replace it arbitrarily many times in between.
if payloadAttributes != nil {
if api.eth.BlockChain().Config().Optimism != nil && payloadAttributes.GasLimit == nil {
return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("gasLimit parameter is required"))
if api.eth.BlockChain().Config().Optimism != nil {
if payloadAttributes.GasLimit == nil {
return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("gasLimit parameter is required"))
}
if api.eth.BlockChain().Config().IsHolocene(payloadAttributes.Timestamp) && payloadAttributes.EIP1559Params == nil {
return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("eip1559Params parameter is required"))
} else if payloadAttributes.EIP1559Params != nil {
return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("eip155Params not supported prior to Holocene upgrade"))
}
}
transactions := make(types.Transactions, 0, len(payloadAttributes.Transactions))
for i, otx := range payloadAttributes.Transactions {
Expand Down
52 changes: 29 additions & 23 deletions miner/payload_building.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ type BuildPayloadArgs struct {
BeaconRoot *common.Hash // The provided beaconRoot (Cancun)
Version engine.PayloadVersion // Versioning byte for payload id calculation.

NoTxPool bool // Optimism addition: option to disable tx pool contents from being included
Transactions []*types.Transaction // Optimism addition: txs forced into the block via engine API
GasLimit *uint64 // Optimism addition: override gas limit of the block to build
NoTxPool bool // Optimism addition: option to disable tx pool contents from being included
Transactions []*types.Transaction // Optimism addition: txs forced into the block via engine API
GasLimit *uint64 // Optimism addition: override gas limit of the block to build
EIP1559Params *types.BlockNonce // Optimism addition: encodes Holocene EIP-1559 params
}

// Id computes an 8-byte identifier by hashing the components of the payload arguments.
Expand All @@ -75,6 +76,9 @@ func (args *BuildPayloadArgs) Id() engine.PayloadID {
if args.GasLimit != nil {
binary.Write(hasher, binary.BigEndian, *args.GasLimit)
}
if args.EIP1559Params != nil {
hasher.Write(args.EIP1559Params[:])
}

var out engine.PayloadID
copy(out[:], hasher.Sum(nil)[:8])
Expand Down Expand Up @@ -280,16 +284,17 @@ func (miner *Miner) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload
// to deliver for not missing slot.
// In OP-Stack, the "empty" block is constructed from provided txs only, i.e. no tx-pool usage.
emptyParams := &generateParams{
timestamp: args.Timestamp,
forceTime: true,
parentHash: args.Parent,
coinbase: args.FeeRecipient,
random: args.Random,
withdrawals: args.Withdrawals,
beaconRoot: args.BeaconRoot,
noTxs: true,
txs: args.Transactions,
gasLimit: args.GasLimit,
timestamp: args.Timestamp,
forceTime: true,
parentHash: args.Parent,
coinbase: args.FeeRecipient,
random: args.Random,
withdrawals: args.Withdrawals,
beaconRoot: args.BeaconRoot,
noTxs: true,
txs: args.Transactions,
gasLimit: args.GasLimit,
eip1559Params: args.EIP1559Params,
}
empty := miner.generateWork(emptyParams, witness)
if empty.err != nil {
Expand All @@ -304,16 +309,17 @@ func (miner *Miner) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload
}

fullParams := &generateParams{
timestamp: args.Timestamp,
forceTime: true,
parentHash: args.Parent,
coinbase: args.FeeRecipient,
random: args.Random,
withdrawals: args.Withdrawals,
beaconRoot: args.BeaconRoot,
noTxs: false,
txs: args.Transactions,
gasLimit: args.GasLimit,
timestamp: args.Timestamp,
forceTime: true,
parentHash: args.Parent,
coinbase: args.FeeRecipient,
random: args.Random,
withdrawals: args.Withdrawals,
beaconRoot: args.BeaconRoot,
noTxs: false,
txs: args.Transactions,
gasLimit: args.GasLimit,
eip1559Params: args.EIP1559Params,
}

// Since we skip building the empty block when using the tx pool, we need to explicitly
Expand Down
56 changes: 46 additions & 10 deletions miner/payload_building_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,43 @@ func newTestWorker(t *testing.T, chainConfig *params.ChainConfig, engine consens
}

func TestBuildPayload(t *testing.T) {
t.Run("no-tx-pool", func(t *testing.T) { testBuildPayload(t, true, false) })
t.Run("no-tx-pool", func(t *testing.T) { testBuildPayload(t, true, false, nil) })
// no-tx-pool case with interrupt not interesting because no-tx-pool doesn't run
// the builder routine
t.Run("with-tx-pool", func(t *testing.T) { testBuildPayload(t, false, false) })
t.Run("with-tx-pool-interrupt", func(t *testing.T) { testBuildPayload(t, false, true) })
t.Run("with-tx-pool", func(t *testing.T) { testBuildPayload(t, false, false, nil) })
t.Run("with-tx-pool-interrupt", func(t *testing.T) { testBuildPayload(t, false, true, nil) })
nonce := types.BlockNonce([8]byte{0, 1, 2, 3, 4, 5, 6, 7})
t.Run("with-nonce", func(t *testing.T) { testBuildPayload(t, false, false, &nonce) })
t.Run("with-nonce-no-tx-pool", func(t *testing.T) { testBuildPayload(t, true, false, &nonce) })
t.Run("with-nonce-interrupt", func(t *testing.T) { testBuildPayload(t, false, true, &nonce) })
}

func testBuildPayload(t *testing.T, noTxPool, interrupt bool) {
func opConfig() *params.ChainConfig {
config := *params.TestChainConfig
config.LondonBlock = big.NewInt(0)
t := uint64(0)
config.CanyonTime = &t
config.HoloceneTime = &t
canyonDenom := uint64(250)
config.Optimism = &params.OptimismConfig{
EIP1559Elasticity: 6,
EIP1559Denominator: 50,
EIP1559DenominatorCanyon: &canyonDenom,
}
return &config
}

func testBuildPayload(t *testing.T, noTxPool, interrupt bool, nonce *types.BlockNonce) {
t.Parallel()
var (
db = rawdb.NewMemoryDatabase()
recipient = common.HexToAddress("0xdeadbeef")
)
w, b := newTestWorker(t, params.TestChainConfig, ethash.NewFaker(), db, 0)
config := params.TestChainConfig
if nonce != nil {
config = opConfig()
}
w, b := newTestWorker(t, config, ethash.NewFaker(), db, 0)

const numInterruptTxs = 256
if interrupt {
Expand All @@ -172,11 +195,12 @@ func testBuildPayload(t *testing.T, noTxPool, interrupt bool) {

timestamp := uint64(time.Now().Unix())
args := &BuildPayloadArgs{
Parent: b.chain.CurrentBlock().Hash(),
Timestamp: timestamp,
Random: common.Hash{},
FeeRecipient: recipient,
NoTxPool: noTxPool,
Parent: b.chain.CurrentBlock().Hash(),
Timestamp: timestamp,
Random: common.Hash{},
FeeRecipient: recipient,
NoTxPool: noTxPool,
EIP1559Params: nonce,
}
// payload resolution now interrupts block building, so we have to
// wait for the payloading building process to build its first block
Expand Down Expand Up @@ -209,6 +233,18 @@ func testBuildPayload(t *testing.T, noTxPool, interrupt bool) {
}
}

// make sure the nonce we've specied (if any) ends up in both the full and empty block headers
var nonce2 types.BlockNonce
if nonce != nil {
nonce2 = *nonce
}
if payload.full != nil && payload.full.Header().Nonce != nonce2 {
t.Fatalf("Nonces don't match. want: %x, got %x", nonce2, payload.full.Header().Nonce)
}
if payload.empty != nil && payload.empty.Header().Nonce != nonce2 {
t.Fatalf("Nonces don't match on empty block. want: %x, got %x", nonce2, payload.empty.Header().Nonce)
}

if noTxPool {
// we only build the empty block when ignoring the tx pool
empty := payload.ResolveEmpty()
Expand Down
Loading

0 comments on commit 45bf7f7

Please sign in to comment.