From cfce44e37235cc54d76c734bcf365dda618880a0 Mon Sep 17 00:00:00 2001 From: Roberto Bayardo Date: Tue, 8 Oct 2024 21:16:49 -0700 Subject: [PATCH] handle holocene EIP-1559 params --- beacon/engine/gen_blockparams.go | 6 +++ beacon/engine/types.go | 9 ++++- consensus/misc/eip1559/eip1559.go | 27 ++++++++++--- consensus/misc/eip1559/eip1559_test.go | 52 ++++++++++++++++++++++++++ eth/catalyst/api.go | 9 ++++- miner/payload_building.go | 52 ++++++++++++++------------ miner/payload_building_test.go | 49 +++++++++++++++++++----- miner/worker.go | 25 +++++++++---- 8 files changed, 180 insertions(+), 49 deletions(-) diff --git a/beacon/engine/gen_blockparams.go b/beacon/engine/gen_blockparams.go index c343e58906..1a9a45fba5 100644 --- a/beacon/engine/gen_blockparams.go +++ b/beacon/engine/gen_blockparams.go @@ -24,6 +24,7 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { Transactions []hexutil.Bytes `json:"transactions,omitempty" gencodec:"optional"` NoTxPool bool `json:"noTxPool,omitempty" gencodec:"optional"` GasLimit *hexutil.Uint64 `json:"gasLimit,omitempty" gencodec:"optional"` + EIP1559Params hexutil.Bytes `json:"eip1559Params,omitempty" gencodec:"optional"` } var enc PayloadAttributes enc.Timestamp = hexutil.Uint64(p.Timestamp) @@ -39,6 +40,7 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { } enc.NoTxPool = p.NoTxPool enc.GasLimit = (*hexutil.Uint64)(p.GasLimit) + enc.EIP1559Params = p.EIP1559Params return json.Marshal(&enc) } @@ -53,6 +55,7 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { Transactions []hexutil.Bytes `json:"transactions,omitempty" gencodec:"optional"` NoTxPool *bool `json:"noTxPool,omitempty" gencodec:"optional"` GasLimit *hexutil.Uint64 `json:"gasLimit,omitempty" gencodec:"optional"` + EIP1559Params *hexutil.Bytes `json:"eip1559Params,omitempty" gencodec:"optional"` } var dec PayloadAttributes if err := json.Unmarshal(input, &dec); err != nil { @@ -88,5 +91,8 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { if dec.GasLimit != nil { p.GasLimit = (*uint64)(dec.GasLimit) } + if dec.EIP1559Params != nil { + p.EIP1559Params = *dec.EIP1559Params + } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 9b06b1e1bd..9c5db3489d 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -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 diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index a66298af69..8460563b7d 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -17,6 +17,7 @@ package eip1559 import ( + "encoding/binary" "errors" "fmt" "math/big" @@ -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) @@ -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) @@ -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) diff --git a/consensus/misc/eip1559/eip1559_test.go b/consensus/misc/eip1559/eip1559_test.go index 39766b57cc..5a34f99d55 100644 --- a/consensus/misc/eip1559/eip1559_test.go +++ b/consensus/misc/eip1559/eip1559_test.go @@ -17,6 +17,7 @@ package eip1559 import ( + "encoding/binary" "math/big" "testing" @@ -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 = ¶ms.OptimismConfig{ EIP1559Elasticity: 6, EIP1559Denominator: 50, @@ -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) + + tests := []struct { + parentGasLimit uint64 + parentGasUsed uint64 + expectedBaseFee int64 + nonce types.BlockNonce + }{ + {30_000_000, 15_000_000, parentBaseFee, elasticity2Denom10Nonce}, // target + {30_000_000, 10_000_000, 9666667, elasticity2Denom10Nonce}, // below + {30_000_000, 20_000_000, 10333333, elasticity2Denom10Nonce}, // above + {30_000_000, 3_000_000, parentBaseFee, elasticity10Denom2Nonce}, // target + {30_000_000, 1_000_000, 6_666_667, elasticity10Denom2Nonce}, // below + {30_000_000, 30_000_000, 55_000_000, elasticity10Denom2Nonce}, // above + } + for i, test := range tests { + parent := &types.Header{ + Number: common.Big32, + GasLimit: test.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) + } } } diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index e8c98d1e1d..7ebe29b505 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -435,8 +435,13 @@ 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")) + } } transactions := make(types.Transactions, 0, len(payloadAttributes.Transactions)) for i, otx := range payloadAttributes.Transactions { diff --git a/miner/payload_building.go b/miner/payload_building.go index 8cbab3042e..eef8e62021 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -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. @@ -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]) @@ -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 { @@ -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 diff --git a/miner/payload_building_test.go b/miner/payload_building_test.go index eea55e734a..fb95c31e8b 100644 --- a/miner/payload_building_test.go +++ b/miner/payload_building_test.go @@ -147,20 +147,41 @@ 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) }) } -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 = ¶ms.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 { @@ -172,11 +193,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 @@ -222,6 +244,13 @@ func testBuildPayload(t *testing.T, noTxPool, interrupt bool) { payload.WaitFull() full := payload.ResolveFull() verify(full, len(pendingTxs)) + var nonce2 types.BlockNonce + if nonce != nil { + nonce2 = *nonce + } + if payload.full.Header().Nonce != nonce2 { + t.Fatalf("Nonces don't match. want: %x, got %x", nonce2, payload.full.Header().Nonce) + } } // Ensure resolve can be called multiple times and the diff --git a/miner/worker.go b/miner/worker.go index 0146700283..e1b69c77b2 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -107,10 +107,11 @@ type generateParams struct { beaconRoot *common.Hash // The beacon root (cancun field). noTxs bool // Flag whether an empty block without any transaction is expected - txs types.Transactions // Deposit transactions to include at the start of the block - gasLimit *uint64 // Optional gas limit override - interrupt *atomic.Int32 // Optional interruption signal to pass down to worker.generateWork - isUpdate bool // Optional flag indicating that this is building a discardable update + txs types.Transactions // Deposit transactions to include at the start of the block + gasLimit *uint64 // Optional gas limit override + eip1559Params *types.BlockNonce // Optional EIP-1559 parameters + interrupt *atomic.Int32 // Optional interruption signal to pass down to worker.generateWork + isUpdate bool // Optional flag indicating that this is building a discardable update } // generateWork generates a sealing block based on the given parameters. @@ -245,9 +246,19 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir } if genParams.gasLimit != nil { // override gas limit if specified header.GasLimit = *genParams.gasLimit - } else if miner.chain.Config().Optimism != nil && miner.config.GasCeil != 0 { - // configure the gas limit of pending blocks with the miner gas limit config when using optimism - header.GasLimit = miner.config.GasCeil + } else if miner.chain.Config().Optimism != nil { + if miner.config.GasCeil != 0 { + // configure the gas limit of pending blocks with the miner gas limit config when using optimism + header.GasLimit = miner.config.GasCeil + } + if miner.chainConfig.IsHolocene(header.Time) { + if genParams.eip1559Params == nil { + return nil, errors.New("Expected eip1559 params, got none") + } + header.Nonce = *genParams.eip1559Params + } else if genParams.eip1559Params != nil { + return nil, errors.New("Got eip1559 params, expected none") + } } // Run the consensus preparation with the default or customized consensus engine. // Note that the `header.Time` may be changed.