diff --git a/consensus/dummy/consensus.go b/consensus/dummy/consensus.go index ac750e4b67..ffbd8b0abe 100644 --- a/consensus/dummy/consensus.go +++ b/consensus/dummy/consensus.go @@ -159,12 +159,13 @@ func (eng *DummyEngine) verifyHeaderGasFields(config *params.ChainConfig, header } // Verify BlockGasCost, ExtDataGasUsed not present before AP4 + headerExtra := types.GetHeaderExtra(header) if !configExtra.IsApricotPhase4(header.Time) { - if header.BlockGasCost != nil { - return fmt.Errorf("invalid blockGasCost before fork: have %d, want ", header.BlockGasCost) + if headerExtra.BlockGasCost != nil { + return fmt.Errorf("invalid blockGasCost before fork: have %d, want ", headerExtra.BlockGasCost) } - if header.ExtDataGasUsed != nil { - return fmt.Errorf("invalid extDataGasUsed before fork: have %d, want ", header.ExtDataGasUsed) + if headerExtra.ExtDataGasUsed != nil { + return fmt.Errorf("invalid extDataGasUsed before fork: have %d, want ", headerExtra.ExtDataGasUsed) } return nil } @@ -175,16 +176,16 @@ func (eng *DummyEngine) verifyHeaderGasFields(config *params.ChainConfig, header parent, header.Time, ) - if !utils.BigEqualUint64(header.BlockGasCost, expectedBlockGasCost) { - return fmt.Errorf("invalid block gas cost: have %d, want %d", header.BlockGasCost, expectedBlockGasCost) + if !utils.BigEqualUint64(headerExtra.BlockGasCost, expectedBlockGasCost) { + return fmt.Errorf("invalid block gas cost: have %d, want %d", headerExtra.BlockGasCost, expectedBlockGasCost) } // ExtDataGasUsed correctness is checked during block validation // (when the validator has access to the block contents) - if header.ExtDataGasUsed == nil { + if headerExtra.ExtDataGasUsed == nil { return errExtDataGasUsedNil } - if !header.ExtDataGasUsed.IsUint64() { + if !headerExtra.ExtDataGasUsed.IsUint64() { return errExtDataGasUsedTooLarge } return nil @@ -422,10 +423,11 @@ func (eng *DummyEngine) FinalizeAndAssemble(chain consensus.ChainHeaderReader, h } configExtra := params.GetExtra(chain.Config()) + headerExtra := types.GetHeaderExtra(header) if configExtra.IsApricotPhase4(header.Time) { - header.ExtDataGasUsed = extDataGasUsed - if header.ExtDataGasUsed == nil { - header.ExtDataGasUsed = new(big.Int).Set(common.Big0) + headerExtra.ExtDataGasUsed = extDataGasUsed + if headerExtra.ExtDataGasUsed == nil { + headerExtra.ExtDataGasUsed = new(big.Int).Set(common.Big0) } // Calculate the required block gas cost for this block. blockGasCost := customheader.BlockGasCost( @@ -433,12 +435,12 @@ func (eng *DummyEngine) FinalizeAndAssemble(chain consensus.ChainHeaderReader, h parent, header.Time, ) - header.BlockGasCost = new(big.Int).SetUint64(blockGasCost) + headerExtra.BlockGasCost = new(big.Int).SetUint64(blockGasCost) // Verify that this block covers the block fee. if err := eng.verifyBlockFee( header.BaseFee, - header.BlockGasCost, + headerExtra.BlockGasCost, txs, receipts, contribution, diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 86038130af..aa5b96d502 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -383,8 +383,9 @@ func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Tr BaseFee: baseFee, } if configExtra.IsApricotPhase4(header.Time) { - header.BlockGasCost = big.NewInt(0) - header.ExtDataGasUsed = big.NewInt(0) + headerExtra := types.GetHeaderExtra(header) + headerExtra.BlockGasCost = big.NewInt(0) + headerExtra.ExtDataGasUsed = big.NewInt(0) } var receipts []*types.Receipt // The post-state result doesn't need to be correct (this is a bad block), but we do need something there diff --git a/core/types/block.go b/core/types/block.go index b47048fa62..404edca818 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -31,132 +31,12 @@ import ( "encoding/binary" "io" "math/big" - "reflect" "sync/atomic" "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/common/hexutil" "github.com/ava-labs/libevm/rlp" ) -// A BlockNonce is a 64-bit hash which proves (combined with the -// mix-hash) that a sufficient amount of computation has been carried -// out on a block. -type BlockNonce [8]byte - -// EncodeNonce converts the given integer to a block nonce. -func EncodeNonce(i uint64) BlockNonce { - var n BlockNonce - binary.BigEndian.PutUint64(n[:], i) - return n -} - -// Uint64 returns the integer value of a block nonce. -func (n BlockNonce) Uint64() uint64 { - return binary.BigEndian.Uint64(n[:]) -} - -// MarshalText encodes n as a hex string with 0x prefix. -func (n BlockNonce) MarshalText() ([]byte, error) { - return hexutil.Bytes(n[:]).MarshalText() -} - -// UnmarshalText implements encoding.TextUnmarshaler. -func (n *BlockNonce) UnmarshalText(input []byte) error { - return hexutil.UnmarshalFixedText("BlockNonce", input, n[:]) -} - -//go:generate go run github.com/fjl/gencodec -type Header -field-override headerMarshaling -out gen_header_json.go -//go:generate go run github.com/ava-labs/libevm/rlp/rlpgen -type Header -out gen_header_rlp.go - -// Header represents a block header in the Ethereum blockchain. -type Header struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` - Coinbase common.Address `json:"miner" gencodec:"required"` - Root common.Hash `json:"stateRoot" gencodec:"required"` - TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` - ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` - Bloom Bloom `json:"logsBloom" gencodec:"required"` - Difficulty *big.Int `json:"difficulty" gencodec:"required"` - Number *big.Int `json:"number" gencodec:"required"` - GasLimit uint64 `json:"gasLimit" gencodec:"required"` - GasUsed uint64 `json:"gasUsed" gencodec:"required"` - Time uint64 `json:"timestamp" gencodec:"required"` - Extra []byte `json:"extraData" gencodec:"required"` - MixDigest common.Hash `json:"mixHash"` - Nonce BlockNonce `json:"nonce"` - ExtDataHash common.Hash `json:"extDataHash" gencodec:"required"` - - // BaseFee was added by EIP-1559 and is ignored in legacy headers. - BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` - - // ExtDataGasUsed was added by Apricot Phase 4 and is ignored in legacy - // headers. - // - // It is not a uint64 like GasLimit or GasUsed because it is not possible to - // correctly encode this field optionally with uint64. - ExtDataGasUsed *big.Int `json:"extDataGasUsed" rlp:"optional"` - - // BlockGasCost was added by Apricot Phase 4 and is ignored in legacy - // headers. - BlockGasCost *big.Int `json:"blockGasCost" rlp:"optional"` - - // BlobGasUsed was added by EIP-4844 and is ignored in legacy headers. - BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"` - - // ExcessBlobGas was added by EIP-4844 and is ignored in legacy headers. - ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"` - - // ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers. - ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` -} - -// field type overrides for gencodec -type headerMarshaling struct { - Difficulty *hexutil.Big - Number *hexutil.Big - GasLimit hexutil.Uint64 - GasUsed hexutil.Uint64 - Time hexutil.Uint64 - Extra hexutil.Bytes - BaseFee *hexutil.Big - ExtDataGasUsed *hexutil.Big - BlockGasCost *hexutil.Big - Hash common.Hash `json:"hash"` // adds call to Hash() in MarshalJSON - BlobGasUsed *hexutil.Uint64 - ExcessBlobGas *hexutil.Uint64 -} - -// Hash returns the block hash of the header, which is simply the keccak256 hash of its -// RLP encoding. -func (h *Header) Hash() common.Hash { - return rlpHash(h) -} - -var headerSize = common.StorageSize(reflect.TypeOf(Header{}).Size()) - -// Size returns the approximate memory used by all internal contents. It is used -// to approximate and limit the memory consumption of various caches. -func (h *Header) Size() common.StorageSize { - var baseFeeBits int - if h.BaseFee != nil { - baseFeeBits = h.BaseFee.BitLen() - } - return headerSize + common.StorageSize(len(h.Extra)+(h.Difficulty.BitLen()+h.Number.BitLen()+baseFeeBits)/8) -} - -// EmptyBody returns true if there is no additional 'body' to complete the header -// that is: no transactions and no uncles. -func (h *Header) EmptyBody() bool { - return h.TxHash == EmptyTxsHash && h.UncleHash == EmptyUncleHash -} - -// EmptyReceipts returns true if there are no receipts for this header/block. -func (h *Header) EmptyReceipts() bool { - return h.ReceiptHash == EmptyReceiptsHash -} - // Body is a simple (mutable, non-safe) data container for storing and moving // a block's data contents (transactions and uncles) together. type Body struct { @@ -249,6 +129,12 @@ func NewBlock( // CopyHeader creates a deep copy of a block header. func CopyHeader(h *Header) *Header { cpy := *h + hExtra := GetHeaderExtra(h) + cpyExtra := &HeaderExtra{ + ExtDataHash: hExtra.ExtDataHash, + } + SetHeaderExtra(&cpy, cpyExtra) + if cpy.Difficulty = new(big.Int); h.Difficulty != nil { cpy.Difficulty.Set(h.Difficulty) } @@ -258,16 +144,20 @@ func CopyHeader(h *Header) *Header { if h.BaseFee != nil { cpy.BaseFee = new(big.Int).Set(h.BaseFee) } - if h.ExtDataGasUsed != nil { - cpy.ExtDataGasUsed = new(big.Int).Set(h.ExtDataGasUsed) + if hExtra.ExtDataGasUsed != nil { + cpyExtra.ExtDataGasUsed = new(big.Int).Set(hExtra.ExtDataGasUsed) } - if h.BlockGasCost != nil { - cpy.BlockGasCost = new(big.Int).Set(h.BlockGasCost) + if hExtra.BlockGasCost != nil { + cpyExtra.BlockGasCost = new(big.Int).Set(hExtra.BlockGasCost) } if len(h.Extra) > 0 { cpy.Extra = make([]byte, len(h.Extra)) copy(cpy.Extra, h.Extra) } + if h.WithdrawalsHash != nil { + cpy.WithdrawalsHash = new(common.Hash) + *cpy.WithdrawalsHash = *h.WithdrawalsHash + } if h.ExcessBlobGas != nil { cpy.ExcessBlobGas = new(uint64) *cpy.ExcessBlobGas = *h.ExcessBlobGas @@ -380,10 +270,11 @@ func (b *Block) BlobGasUsed() *uint64 { } func (b *Block) BlockGasCost() *big.Int { - if b.header.BlockGasCost == nil { + cost := GetHeaderExtra(b.header).BlockGasCost + if cost == nil { return nil } - return new(big.Int).Set(b.header.BlockGasCost) + return new(big.Int).Set(cost) } // Size returns the true RLP encoded storage size of the block, either by encoding diff --git a/core/types/block_ext.go b/core/types/block_ext.go index c186e5d241..2cfb4b2888 100644 --- a/core/types/block_ext.go +++ b/core/types/block_ext.go @@ -28,7 +28,7 @@ func (b *Block) setExtData(data []byte, recalc bool) { b.extdata = &_data copy(*b.extdata, data) if recalc { - b.header.ExtDataHash = CalcExtDataHash(*b.extdata) + GetHeaderExtra(b.header).ExtDataHash = CalcExtDataHash(*b.extdata) } } @@ -44,10 +44,11 @@ func (b *Block) Version() uint32 { } func (b *Block) ExtDataGasUsed() *big.Int { - if b.header.ExtDataGasUsed == nil { + used := GetHeaderExtra(b.header).ExtDataGasUsed + if used == nil { return nil } - return new(big.Int).Set(b.header.ExtDataGasUsed) + return new(big.Int).Set(used) } func CalcExtDataHash(extdata []byte) common.Hash { diff --git a/core/types/block_ext_test.go b/core/types/block_ext_test.go new file mode 100644 index 0000000000..df7d988e0c --- /dev/null +++ b/core/types/block_ext_test.go @@ -0,0 +1,108 @@ +// (c) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package types + +import ( + "math/big" + "reflect" + "testing" + "unsafe" + + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/assert" +) + +func TestCopyHeader(t *testing.T) { + t.Parallel() + + t.Run("empty_header", func(t *testing.T) { + t.Parallel() + + empty := &Header{} + + headerExtra := &HeaderExtra{} + extras.Header.Set(empty, headerExtra) + + cpy := CopyHeader(empty) + + want := &Header{ + Difficulty: new(big.Int), + Number: new(big.Int), + } + + headerExtra = &HeaderExtra{} + extras.Header.Set(want, headerExtra) + + assert.Equal(t, want, cpy) + }) + + t.Run("filled_header", func(t *testing.T) { + t.Parallel() + + header, _ := headerWithNonZeroFields() // the header carries the [HeaderExtra] so we can ignore it + + gotHeader := CopyHeader(header) + gotExtra := GetHeaderExtra(gotHeader) + + wantHeader, wantExtra := headerWithNonZeroFields() + assert.Equal(t, wantHeader, gotHeader) + assert.Equal(t, wantExtra, gotExtra) + + exportedFieldsPointToDifferentMemory(t, header, gotHeader) + exportedFieldsPointToDifferentMemory(t, GetHeaderExtra(header), gotExtra) + }) +} + +func exportedFieldsPointToDifferentMemory[T interface { + Header | HeaderExtra +}](t *testing.T, original, cpy *T) { + t.Helper() + + v := reflect.ValueOf(*original) + typ := v.Type() + cp := reflect.ValueOf(*cpy) + for i := range v.NumField() { + field := typ.Field(i) + if !field.IsExported() { + continue + } + switch field.Type.Kind() { + case reflect.Array, reflect.Uint64: + // Not pointers, but using explicit Kinds for safety + continue + } + + t.Run(field.Name, func(t *testing.T) { + fieldCp := cp.Field(i).Interface() + switch f := v.Field(i).Interface().(type) { + case *big.Int: + assertDifferentPointers(t, f, fieldCp) + case *common.Hash: + assertDifferentPointers(t, f, fieldCp) + case *uint64: + assertDifferentPointers(t, f, fieldCp) + case []uint8: + assertDifferentPointers(t, unsafe.SliceData(f), unsafe.SliceData(fieldCp.([]uint8))) + default: + t.Errorf("field %q type %T needs to be added to switch cases of exportedFieldsDeepCopied", field.Name, f) + } + }) + } +} + +// assertDifferentPointers asserts that `a` and `b` are both non-nil +// pointers pointing to different memory locations. +func assertDifferentPointers[T any](t *testing.T, a *T, b any) { + t.Helper() + switch { + case a == nil: + t.Errorf("a (%T) cannot be nil", a) + case b == nil: + t.Errorf("b (%T) cannot be nil", b) + case a == b: + t.Errorf("pointers to same memory") + } + // Note: no need to check `b` is of the same type as `a`, otherwise + // the memory address would be different as well. +} diff --git a/core/types/block_test.go b/core/types/block_test.go index 75bb428853..95546c83c3 100644 --- a/core/types/block_test.go +++ b/core/types/block_test.go @@ -69,7 +69,7 @@ func TestBlockEncoding(t *testing.T) { check("Extra", block.Extra(), common.FromHex("")) check("MixDigest", block.MixDigest(), common.HexToHash("0000000000000000000000000000000000000000000000000000000000000000")) check("Nonce", block.Nonce(), uint64(0)) - check("ExtDataHash", block.header.ExtDataHash, common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")) + check("ExtDataHash", GetHeaderExtra(block.header).ExtDataHash, common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")) check("BaseFee", block.BaseFee(), (*big.Int)(nil)) check("ExtDataGasUsed", block.ExtDataGasUsed(), (*big.Int)(nil)) check("BlockGasCost", block.BlockGasCost(), (*big.Int)(nil)) @@ -177,7 +177,7 @@ func TestEIP2718BlockEncoding(t *testing.T) { check("Nonce", block.Nonce(), uint64(0xa13a5a8c8f2bb1c4)) check("Time", block.Time(), uint64(1426516743)) check("Size", block.Size(), uint64(len(blockEnc))) - check("ExtDataHash", block.header.ExtDataHash, common.HexToHash("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")) + check("ExtDataHash", GetHeaderExtra(block.header).ExtDataHash, common.HexToHash("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")) check("BaseFee", block.BaseFee(), (*big.Int)(nil)) check("ExtDataGasUsed", block.ExtDataGasUsed(), (*big.Int)(nil)) check("BlockGasCost", block.BlockGasCost(), (*big.Int)(nil)) @@ -252,7 +252,7 @@ func TestBlockEncodingWithExtraData(t *testing.T) { check("Extra", block.Extra(), common.FromHex("")) check("MixDigest", block.MixDigest(), common.HexToHash("0000000000000000000000000000000000000000000000000000000000000000")) check("Nonce", block.Nonce(), uint64(0)) - check("ExtDataHash", block.header.ExtDataHash, common.HexToHash("296ff3bfdebf7c4b1fb71f589d69ed03b1c59b278d1780d54dc86ea7cb87cf17")) + check("ExtDataHash", GetHeaderExtra(block.header).ExtDataHash, common.HexToHash("296ff3bfdebf7c4b1fb71f589d69ed03b1c59b278d1780d54dc86ea7cb87cf17")) check("BaseFee", block.BaseFee(), (*big.Int)(nil)) check("ExtDataGasUsed", block.ExtDataGasUsed(), (*big.Int)(nil)) check("BlockGasCost", block.BlockGasCost(), (*big.Int)(nil)) diff --git a/core/types/gen_header_json.go b/core/types/gen_header_serializable_json.go similarity index 91% rename from core/types/gen_header_json.go rename to core/types/gen_header_serializable_json.go index 0c1f588944..eb4e7957a7 100644 --- a/core/types/gen_header_json.go +++ b/core/types/gen_header_serializable_json.go @@ -14,8 +14,8 @@ import ( var _ = (*headerMarshaling)(nil) // MarshalJSON marshals as JSON. -func (h Header) MarshalJSON() ([]byte, error) { - type Header struct { +func (h HeaderSerializable) MarshalJSON() ([]byte, error) { + type HeaderSerializable struct { ParentHash common.Hash `json:"parentHash" gencodec:"required"` UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` Coinbase common.Address `json:"miner" gencodec:"required"` @@ -40,7 +40,7 @@ func (h Header) MarshalJSON() ([]byte, error) { ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` Hash common.Hash `json:"hash"` } - var enc Header + var enc HeaderSerializable enc.ParentHash = h.ParentHash enc.UncleHash = h.UncleHash enc.Coinbase = h.Coinbase @@ -68,8 +68,8 @@ func (h Header) MarshalJSON() ([]byte, error) { } // UnmarshalJSON unmarshals from JSON. -func (h *Header) UnmarshalJSON(input []byte) error { - type Header struct { +func (h *HeaderSerializable) UnmarshalJSON(input []byte) error { + type HeaderSerializable struct { ParentHash *common.Hash `json:"parentHash" gencodec:"required"` UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` Coinbase *common.Address `json:"miner" gencodec:"required"` @@ -93,60 +93,60 @@ func (h *Header) UnmarshalJSON(input []byte) error { ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` } - var dec Header + var dec HeaderSerializable if err := json.Unmarshal(input, &dec); err != nil { return err } if dec.ParentHash == nil { - return errors.New("missing required field 'parentHash' for Header") + return errors.New("missing required field 'parentHash' for HeaderSerializable") } h.ParentHash = *dec.ParentHash if dec.UncleHash == nil { - return errors.New("missing required field 'sha3Uncles' for Header") + return errors.New("missing required field 'sha3Uncles' for HeaderSerializable") } h.UncleHash = *dec.UncleHash if dec.Coinbase == nil { - return errors.New("missing required field 'miner' for Header") + return errors.New("missing required field 'miner' for HeaderSerializable") } h.Coinbase = *dec.Coinbase if dec.Root == nil { - return errors.New("missing required field 'stateRoot' for Header") + return errors.New("missing required field 'stateRoot' for HeaderSerializable") } h.Root = *dec.Root if dec.TxHash == nil { - return errors.New("missing required field 'transactionsRoot' for Header") + return errors.New("missing required field 'transactionsRoot' for HeaderSerializable") } h.TxHash = *dec.TxHash if dec.ReceiptHash == nil { - return errors.New("missing required field 'receiptsRoot' for Header") + return errors.New("missing required field 'receiptsRoot' for HeaderSerializable") } h.ReceiptHash = *dec.ReceiptHash if dec.Bloom == nil { - return errors.New("missing required field 'logsBloom' for Header") + return errors.New("missing required field 'logsBloom' for HeaderSerializable") } h.Bloom = *dec.Bloom if dec.Difficulty == nil { - return errors.New("missing required field 'difficulty' for Header") + return errors.New("missing required field 'difficulty' for HeaderSerializable") } h.Difficulty = (*big.Int)(dec.Difficulty) if dec.Number == nil { - return errors.New("missing required field 'number' for Header") + return errors.New("missing required field 'number' for HeaderSerializable") } h.Number = (*big.Int)(dec.Number) if dec.GasLimit == nil { - return errors.New("missing required field 'gasLimit' for Header") + return errors.New("missing required field 'gasLimit' for HeaderSerializable") } h.GasLimit = uint64(*dec.GasLimit) if dec.GasUsed == nil { - return errors.New("missing required field 'gasUsed' for Header") + return errors.New("missing required field 'gasUsed' for HeaderSerializable") } h.GasUsed = uint64(*dec.GasUsed) if dec.Time == nil { - return errors.New("missing required field 'timestamp' for Header") + return errors.New("missing required field 'timestamp' for HeaderSerializable") } h.Time = uint64(*dec.Time) if dec.Extra == nil { - return errors.New("missing required field 'extraData' for Header") + return errors.New("missing required field 'extraData' for HeaderSerializable") } h.Extra = *dec.Extra if dec.MixDigest != nil { @@ -156,7 +156,7 @@ func (h *Header) UnmarshalJSON(input []byte) error { h.Nonce = *dec.Nonce } if dec.ExtDataHash == nil { - return errors.New("missing required field 'extDataHash' for Header") + return errors.New("missing required field 'extDataHash' for HeaderSerializable") } h.ExtDataHash = *dec.ExtDataHash if dec.BaseFee != nil { diff --git a/core/types/gen_header_rlp.go b/core/types/gen_header_serializable_rlp.go similarity index 97% rename from core/types/gen_header_rlp.go rename to core/types/gen_header_serializable_rlp.go index e7c98e851f..a2641fb4b3 100644 --- a/core/types/gen_header_rlp.go +++ b/core/types/gen_header_serializable_rlp.go @@ -5,7 +5,7 @@ package types import "github.com/ava-labs/libevm/rlp" import "io" -func (obj *Header) EncodeRLP(_w io.Writer) error { +func (obj *HeaderSerializable) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) _tmp0 := w.List() w.WriteBytes(obj.ParentHash[:]) diff --git a/core/types/header_ext.go b/core/types/header_ext.go index e680ee7798..1e6ebd5bf8 100644 --- a/core/types/header_ext.go +++ b/core/types/header_ext.go @@ -5,32 +5,215 @@ package types import ( "io" + "math/big" + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/common/hexutil" ethtypes "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/rlp" ) +// GetHeaderExtra returns the [HeaderExtra] from the given [Header]. +func GetHeaderExtra(h *Header) *HeaderExtra { + return extras.Header.Get(h) +} + +// SetHeaderExtra sets the given [HeaderExtra] on the [Header]. +func SetHeaderExtra(h *Header, extra *HeaderExtra) { + extras.Header.Set(h, extra) +} + // HeaderExtra is a struct that contains extra fields used by Avalanche // in the block header. +// This type uses [HeaderSerializable] to encode and decode the extra fields +// along with the upstream type for compatibility with existing network blocks. type HeaderExtra struct { + ExtDataHash common.Hash + ExtDataGasUsed *big.Int + BlockGasCost *big.Int } +// EncodeRLP RLP encodes the given [ethtypes.Header] and [HeaderExtra] together +// to the `writer`. It does merge both structs into a single [HeaderSerializable]. func (h *HeaderExtra) EncodeRLP(eth *ethtypes.Header, writer io.Writer) error { - panic("not implemented") + temp := new(HeaderSerializable) + + temp.updateFromEth(eth) + temp.updateFromExtras(h) + + return rlp.Encode(writer, temp) } +// DecodeRLP RLP decodes from the [*rlp.Stream] and writes the output to both the +// [ethtypes.Header] passed as argument and to the receiver [HeaderExtra]. func (h *HeaderExtra) DecodeRLP(eth *ethtypes.Header, stream *rlp.Stream) error { - panic("not implemented") + temp := new(HeaderSerializable) + if err := stream.Decode(temp); err != nil { + return err + } + + temp.updateToEth(eth) + temp.updateToExtras(h) + + return nil } +// EncodeJSON JSON encodes the given [ethtypes.Header] and [HeaderExtra] together +// to the `writer`. It does merge both structs into a single [HeaderSerializable]. func (h *HeaderExtra) EncodeJSON(eth *ethtypes.Header) ([]byte, error) { - panic("not implemented") + temp := new(HeaderSerializable) + + temp.updateFromEth(eth) + temp.updateFromExtras(h) + + return temp.MarshalJSON() } +// DecodeJSON JSON decodes from the `input` bytes and writes the output to both the +// [ethtypes.Header] passed as argument and to the receiver [HeaderExtra]. func (h *HeaderExtra) DecodeJSON(eth *ethtypes.Header, input []byte) error { - panic("not implemented") + temp := new(HeaderSerializable) + if err := temp.UnmarshalJSON(input); err != nil { + return err + } + + temp.updateToEth(eth) + temp.updateToExtras(h) + + return nil } func (h *HeaderExtra) PostCopy(dst *ethtypes.Header) { panic("not implemented") } + +func (h *HeaderSerializable) updateFromEth(eth *ethtypes.Header) { + h.ParentHash = eth.ParentHash + h.UncleHash = eth.UncleHash + h.Coinbase = eth.Coinbase + h.Root = eth.Root + h.TxHash = eth.TxHash + h.ReceiptHash = eth.ReceiptHash + h.Bloom = eth.Bloom + h.Difficulty = eth.Difficulty + h.Number = eth.Number + h.GasLimit = eth.GasLimit + h.GasUsed = eth.GasUsed + h.Time = eth.Time + h.Extra = eth.Extra + h.MixDigest = eth.MixDigest + h.Nonce = eth.Nonce + h.BaseFee = eth.BaseFee + h.BlobGasUsed = eth.BlobGasUsed + h.ExcessBlobGas = eth.ExcessBlobGas + h.ParentBeaconRoot = eth.ParentBeaconRoot +} + +func (h *HeaderSerializable) updateToEth(eth *ethtypes.Header) { + eth.ParentHash = h.ParentHash + eth.UncleHash = h.UncleHash + eth.Coinbase = h.Coinbase + eth.Root = h.Root + eth.TxHash = h.TxHash + eth.ReceiptHash = h.ReceiptHash + eth.Bloom = h.Bloom + eth.Difficulty = h.Difficulty + eth.Number = h.Number + eth.GasLimit = h.GasLimit + eth.GasUsed = h.GasUsed + eth.Time = h.Time + eth.Extra = h.Extra + eth.MixDigest = h.MixDigest + eth.Nonce = h.Nonce + eth.BaseFee = h.BaseFee + eth.BlobGasUsed = h.BlobGasUsed + eth.ExcessBlobGas = h.ExcessBlobGas + eth.ParentBeaconRoot = h.ParentBeaconRoot +} + +func (h *HeaderSerializable) updateFromExtras(extras *HeaderExtra) { + h.ExtDataHash = extras.ExtDataHash + h.ExtDataGasUsed = extras.ExtDataGasUsed + h.BlockGasCost = extras.BlockGasCost +} + +func (h *HeaderSerializable) updateToExtras(extras *HeaderExtra) { + extras.ExtDataHash = h.ExtDataHash + extras.ExtDataGasUsed = h.ExtDataGasUsed + extras.BlockGasCost = h.BlockGasCost +} + +// NOTE: both generators currently do not support type aliases. +// We are using custom versions of these programs for now to support type aliases, +// see https://github.com/ava-labs/coreth/pull/746#discussion_r1969673252 +//go:generate go run github.com/fjl/gencodec -type HeaderSerializable -field-override headerMarshaling -out gen_header_serializable_json.go +//go:generate go run github.com/ava-labs/libevm/rlp/rlpgen -type HeaderSerializable -out gen_header_serializable_rlp.go + +// HeaderSerializable defines the header of a block in the Ethereum blockchain, +// as it is to be serialized into RLP and JSON. Note it must be exported so that +// rlpgen can generate the serialization code from it. +type HeaderSerializable struct { + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase common.Address `json:"miner" gencodec:"required"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *big.Int `json:"difficulty" gencodec:"required"` + Number *big.Int `json:"number" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Time uint64 `json:"timestamp" gencodec:"required"` + Extra []byte `json:"extraData" gencodec:"required"` + MixDigest common.Hash `json:"mixHash"` + Nonce BlockNonce `json:"nonce"` + ExtDataHash common.Hash `json:"extDataHash" gencodec:"required"` + + // BaseFee was added by EIP-1559 and is ignored in legacy headers. + BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` + + // ExtDataGasUsed was added by Apricot Phase 4 and is ignored in legacy + // headers. + // + // It is not a uint64 like GasLimit or GasUsed because it is not possible to + // correctly encode this field optionally with uint64. + ExtDataGasUsed *big.Int `json:"extDataGasUsed" rlp:"optional"` + + // BlockGasCost was added by Apricot Phase 4 and is ignored in legacy + // headers. + BlockGasCost *big.Int `json:"blockGasCost" rlp:"optional"` + + // BlobGasUsed was added by EIP-4844 and is ignored in legacy headers. + BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"` + + // ExcessBlobGas was added by EIP-4844 and is ignored in legacy headers. + ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"` + + // ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers. + ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` +} + +// field type overrides for gencodec +type headerMarshaling struct { + Difficulty *hexutil.Big + Number *hexutil.Big + GasLimit hexutil.Uint64 + GasUsed hexutil.Uint64 + Time hexutil.Uint64 + Extra hexutil.Bytes + BaseFee *hexutil.Big + ExtDataGasUsed *hexutil.Big + BlockGasCost *hexutil.Big + Hash common.Hash `json:"hash"` // adds call to Hash() in MarshalJSON + BlobGasUsed *hexutil.Uint64 + ExcessBlobGas *hexutil.Uint64 +} + +// Hash returns the block hash of the header, which is simply the keccak256 hash of its +// RLP encoding. +// This function MUST be exported and is used in [HeaderSerializable.EncodeJSON] which is +// generated to the file gen_header_json.go. +func (h *HeaderSerializable) Hash() common.Hash { + return rlpHash(h) +} diff --git a/core/types/header_ext_test.go b/core/types/header_ext_test.go new file mode 100644 index 0000000000..97b22237b3 --- /dev/null +++ b/core/types/header_ext_test.go @@ -0,0 +1,171 @@ +// (c) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package types + +import ( + "encoding/hex" + "encoding/json" + "math/big" + "reflect" + "testing" + + "github.com/ava-labs/libevm/common" + ethtypes "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/rlp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHeaderRLP(t *testing.T) { + t.Parallel() + + got := testHeaderEncodeDecode(t, rlp.EncodeToBytes, rlp.DecodeBytes) + + // Golden data from original coreth implementation, before integration of + // libevm. WARNING: changing these values can break backwards compatibility + // with extreme consequences as block-hash calculation may break. + const ( + wantHex = "f90234a00100000000000000000000000000000000000000000000000000000000000000a00200000000000000000000000000000000000000000000000000000000000000940300000000000000000000000000000000000000a00400000000000000000000000000000000000000000000000000000000000000a00500000000000000000000000000000000000000000000000000000000000000a00600000000000000000000000000000000000000000000000000000000000000b901000700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008090a0b0c0da00e00000000000000000000000000000000000000000000000000000000000000880f00000000000000a015000000000000000000000000000000000000000000000000000000000000001016171213a01400000000000000000000000000000000000000000000000000000000000000" + wantHashHex = "7b63c1c763cc1527f12de7e5331b92d66ea201d1e6d7cd65a79bf8e3b99c104d" + ) + + assert.Equal(t, wantHex, hex.EncodeToString(got), "Header RLP") + + header, _ := headerWithNonZeroFields() + gotHashHex := header.Hash().Hex() + assert.Equal(t, "0x"+wantHashHex, gotHashHex, "Header.Hash()") +} + +func TestHeaderJSON(t *testing.T) { + t.Parallel() + + // Note we ignore the returned encoded bytes because we don't + // need to compare them to a JSON gold standard. + _ = testHeaderEncodeDecode(t, json.Marshal, json.Unmarshal) +} + +func testHeaderEncodeDecode( + t *testing.T, + encode func(any) ([]byte, error), + decode func([]byte, any) error, +) (encoded []byte) { + t.Helper() + + input, _ := headerWithNonZeroFields() // the Header carries the HeaderExtra so we can ignore it + encoded, err := encode(input) + require.NoError(t, err, "encode") + + gotHeader := new(Header) + err = decode(encoded, gotHeader) + require.NoError(t, err, "decode") + gotExtra := GetHeaderExtra(gotHeader) + + wantHeader, wantExtra := headerWithNonZeroFields() + wantHeader.WithdrawalsHash = nil + assert.Equal(t, wantHeader, gotHeader) + assert.Equal(t, wantExtra, gotExtra) + + return encoded +} + +func TestHeaderWithNonZeroFields(t *testing.T) { + t.Parallel() + + header, extra := headerWithNonZeroFields() + t.Run("Header", func(t *testing.T) { allExportedFieldsSet(t, header) }) + t.Run("HeaderExtra", func(t *testing.T) { allExportedFieldsSet(t, extra) }) +} + +// headerWithNonZeroFields returns a [Header] and a [HeaderExtra], +// each with all fields set to non-zero values. +// The [HeaderExtra] extra payload is set in the [Header] via [SetHeaderExtra]. +// +// NOTE: They can be used to demonstrate that RLP and JSON round-trip encoding +// can recover all fields, but not that the encoded format is correct. This is +// very important as the RLP encoding of a [Header] defines its hash. +func headerWithNonZeroFields() (*Header, *HeaderExtra) { + header := ðtypes.Header{ + ParentHash: common.Hash{1}, + UncleHash: common.Hash{2}, + Coinbase: common.Address{3}, + Root: common.Hash{4}, + TxHash: common.Hash{5}, + ReceiptHash: common.Hash{6}, + Bloom: Bloom{7}, + Difficulty: big.NewInt(8), + Number: big.NewInt(9), + GasLimit: 10, + GasUsed: 11, + Time: 12, + Extra: []byte{13}, + MixDigest: common.Hash{14}, + Nonce: BlockNonce{15}, + BaseFee: big.NewInt(16), + WithdrawalsHash: &common.Hash{17}, + BlobGasUsed: ptrTo(uint64(18)), + ExcessBlobGas: ptrTo(uint64(19)), + ParentBeaconRoot: &common.Hash{20}, + } + extra := &HeaderExtra{ + ExtDataHash: common.Hash{21}, + ExtDataGasUsed: big.NewInt(22), + BlockGasCost: big.NewInt(23), + } + SetHeaderExtra(header, extra) + return header, extra +} + +func allExportedFieldsSet[T interface { + ethtypes.Header | HeaderExtra +}](t *testing.T, x *T) { + // We don't test for nil pointers because we're only confirming that + // test-input data is well-formed. A panic due to a dereference will be + // reported anyway. + + v := reflect.ValueOf(*x) + for i := range v.Type().NumField() { + field := v.Type().Field(i) + if !field.IsExported() { + continue + } + + t.Run(field.Name, func(t *testing.T) { + switch f := v.Field(i).Interface().(type) { + case common.Hash: + assertNonZero(t, f) + case common.Address: + assertNonZero(t, f) + case BlockNonce: + assertNonZero(t, f) + case Bloom: + assertNonZero(t, f) + case uint64: + assertNonZero(t, f) + case *big.Int: + assertNonZero(t, f) + case *common.Hash: + assertNonZero(t, f) + case *uint64: + assertNonZero(t, f) + case []uint8: + assert.NotEmpty(t, f) + default: + t.Errorf("Field %q has unsupported type %T", field.Name, f) + } + }) + } +} + +func assertNonZero[T interface { + common.Hash | common.Address | BlockNonce | uint64 | Bloom | + *big.Int | *common.Hash | *uint64 +}](t *testing.T, v T) { + t.Helper() + var zero T + if v == zero { + t.Errorf("must not be zero value for %T", v) + } +} + +func ptrTo[T any](x T) *T { return &x } diff --git a/core/types/imports.go b/core/types/imports.go index 552a501f30..cc25fa9a8e 100644 --- a/core/types/imports.go +++ b/core/types/imports.go @@ -15,8 +15,10 @@ type ( AccessTuple = ethtypes.AccessTuple BlobTx = ethtypes.BlobTx BlobTxSidecar = ethtypes.BlobTxSidecar + BlockNonce = ethtypes.BlockNonce Bloom = ethtypes.Bloom DynamicFeeTx = ethtypes.DynamicFeeTx + Header = ethtypes.Header HomesteadSigner = ethtypes.HomesteadSigner LegacyTx = ethtypes.LegacyTx Receipt = ethtypes.Receipt @@ -50,6 +52,7 @@ var ( BloomLookup = ethtypes.BloomLookup BytesToBloom = ethtypes.BytesToBloom CreateBloom = ethtypes.CreateBloom + EncodeNonce = ethtypes.EncodeNonce FullAccount = ethtypes.FullAccount FullAccountRLP = ethtypes.FullAccountRLP NewContractCreation = ethtypes.NewContractCreation diff --git a/core/types/libevm.go b/core/types/libevm.go index 31a603d421..a5cc469bca 100644 --- a/core/types/libevm.go +++ b/core/types/libevm.go @@ -8,7 +8,7 @@ import ( ) var extras = ethtypes.RegisterExtras[ - ethtypes.NOOPHeaderHooks, *ethtypes.NOOPHeaderHooks, + HeaderExtra, *HeaderExtra, ethtypes.NOOPBlockBodyHooks, *ethtypes.NOOPBlockBodyHooks, isMultiCoin, ]() diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index f99d40be29..05bc4ab4e1 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1232,6 +1232,7 @@ func (s *BlockChainAPI) EstimateGas(ctx context.Context, args TransactionArgs, b // RPCMarshalHeader converts the given header to the RPC output . func RPCMarshalHeader(head *types.Header) map[string]interface{} { + headExtra := types.GetHeaderExtra(head) result := map[string]interface{}{ "number": (*hexutil.Big)(head.Number), "hash": head.Hash(), @@ -1249,16 +1250,16 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { "timestamp": hexutil.Uint64(head.Time), "transactionsRoot": head.TxHash, "receiptsRoot": head.ReceiptHash, - "extDataHash": head.ExtDataHash, + "extDataHash": headExtra.ExtDataHash, } if head.BaseFee != nil { result["baseFeePerGas"] = (*hexutil.Big)(head.BaseFee) } - if head.ExtDataGasUsed != nil { - result["extDataGasUsed"] = (*hexutil.Big)(head.ExtDataGasUsed) + if headExtra.ExtDataGasUsed != nil { + result["extDataGasUsed"] = (*hexutil.Big)(headExtra.ExtDataGasUsed) } - if head.BlockGasCost != nil { - result["blockGasCost"] = (*hexutil.Big)(head.BlockGasCost) + if headExtra.BlockGasCost != nil { + result["blockGasCost"] = (*hexutil.Big)(headExtra.BlockGasCost) } if head.BlobGasUsed != nil { result["blobGasUsed"] = hexutil.Uint64(*head.BlobGasUsed) diff --git a/plugin/evm/block_verification.go b/plugin/evm/block_verification.go index b66e877c7e..7d73e3eece 100644 --- a/plugin/evm/block_verification.go +++ b/plugin/evm/block_verification.go @@ -79,15 +79,16 @@ func (v blockValidator) SyntacticVerify(b *Block, rules params.Rules) error { } // Verify the ExtDataHash field + headerExtra := types.GetHeaderExtra(ethHeader) if rulesExtra.IsApricotPhase1 { - if hash := types.CalcExtDataHash(b.ethBlock.ExtData()); ethHeader.ExtDataHash != hash { - return fmt.Errorf("extra data hash mismatch: have %x, want %x", ethHeader.ExtDataHash, hash) + if hash := types.CalcExtDataHash(b.ethBlock.ExtData()); headerExtra.ExtDataHash != hash { + return fmt.Errorf("extra data hash mismatch: have %x, want %x", headerExtra.ExtDataHash, hash) } } else { - if ethHeader.ExtDataHash != (common.Hash{}) { + if headerExtra.ExtDataHash != (common.Hash{}) { return fmt.Errorf( "expected ExtDataHash to be empty but got %x", - ethHeader.ExtDataHash, + headerExtra.ExtDataHash, ) } } @@ -200,16 +201,16 @@ func (v blockValidator) SyntacticVerify(b *Block, rules params.Rules) error { // If we are in ApricotPhase4, ensure that ExtDataGasUsed is populated correctly. if rulesExtra.IsApricotPhase4 { // Make sure ExtDataGasUsed is not nil and correct - if ethHeader.ExtDataGasUsed == nil { + if headerExtra.ExtDataGasUsed == nil { return errNilExtDataGasUsedApricotPhase4 } if rulesExtra.IsApricotPhase5 { - if !utils.BigLessOrEqualUint64(ethHeader.ExtDataGasUsed, ap5.AtomicGasLimit) { - return fmt.Errorf("too large extDataGasUsed: %d", ethHeader.ExtDataGasUsed) + if !utils.BigLessOrEqualUint64(headerExtra.ExtDataGasUsed, ap5.AtomicGasLimit) { + return fmt.Errorf("too large extDataGasUsed: %d", headerExtra.ExtDataGasUsed) } } else { - if !ethHeader.ExtDataGasUsed.IsUint64() { - return fmt.Errorf("too large extDataGasUsed: %d", ethHeader.ExtDataGasUsed) + if !headerExtra.ExtDataGasUsed.IsUint64() { + return fmt.Errorf("too large extDataGasUsed: %d", headerExtra.ExtDataGasUsed) } } var totalGasUsed uint64 @@ -228,15 +229,15 @@ func (v blockValidator) SyntacticVerify(b *Block, rules params.Rules) error { } switch { - case ethHeader.ExtDataGasUsed.Cmp(new(big.Int).SetUint64(totalGasUsed)) != 0: - return fmt.Errorf("invalid extDataGasUsed: have %d, want %d", ethHeader.ExtDataGasUsed, totalGasUsed) + case headerExtra.ExtDataGasUsed.Cmp(new(big.Int).SetUint64(totalGasUsed)) != 0: + return fmt.Errorf("invalid extDataGasUsed: have %d, want %d", headerExtra.ExtDataGasUsed, totalGasUsed) // Make sure BlockGasCost is not nil // NOTE: ethHeader.BlockGasCost correctness is checked in header verification - case ethHeader.BlockGasCost == nil: + case headerExtra.BlockGasCost == nil: return errNilBlockGasCostApricotPhase4 - case !ethHeader.BlockGasCost.IsUint64(): - return fmt.Errorf("too large blockGasCost: %d", ethHeader.BlockGasCost) + case !headerExtra.BlockGasCost.IsUint64(): + return fmt.Errorf("too large blockGasCost: %d", headerExtra.BlockGasCost) } } diff --git a/plugin/evm/header/base_fee_test.go b/plugin/evm/header/base_fee_test.go index 0deeb4350e..045f67c040 100644 --- a/plugin/evm/header/base_fee_test.go +++ b/plugin/evm/header/base_fee_test.go @@ -21,12 +21,13 @@ import ( func TestBaseFee(t *testing.T) { tests := []struct { - name string - upgrades extras.NetworkUpgrades - parent *types.Header - timestamp uint64 - want *big.Int - wantErr error + name string + upgrades extras.NetworkUpgrades + parent *types.Header + parentExtra *types.HeaderExtra + timestamp uint64 + want *big.Int + wantErr error }{ { name: "ap2", @@ -180,9 +181,11 @@ func TestBaseFee(t *testing.T) { name: "ap4_decrease", upgrades: params.GetExtra(params.TestApricotPhase4Config).NetworkUpgrades, parent: &types.Header{ - Number: big.NewInt(1), - Extra: feeWindowBytes(ap3.Window{}), - BaseFee: big.NewInt(ap4.MaxBaseFee), + Number: big.NewInt(1), + Extra: feeWindowBytes(ap3.Window{}), + BaseFee: big.NewInt(ap4.MaxBaseFee), + }, + parentExtra: &types.HeaderExtra{ BlockGasCost: big.NewInt(ap4.MinBlockGasCost), }, timestamp: 1, @@ -204,10 +207,12 @@ func TestBaseFee(t *testing.T) { name: "ap4_increase", upgrades: params.GetExtra(params.TestApricotPhase4Config).NetworkUpgrades, parent: &types.Header{ - Number: big.NewInt(1), - GasUsed: ap3.TargetGas, - Extra: feeWindowBytes(ap3.Window{}), - BaseFee: big.NewInt(ap4.MinBaseFee), + Number: big.NewInt(1), + GasUsed: ap3.TargetGas, + Extra: feeWindowBytes(ap3.Window{}), + BaseFee: big.NewInt(ap4.MinBaseFee), + }, + parentExtra: &types.HeaderExtra{ ExtDataGasUsed: big.NewInt(ap3.TargetGas), BlockGasCost: big.NewInt(ap4.MinBlockGasCost), }, @@ -261,10 +266,12 @@ func TestBaseFee(t *testing.T) { name: "ap5_increase", upgrades: params.GetExtra(params.TestApricotPhase5Config).NetworkUpgrades, parent: &types.Header{ - Number: big.NewInt(1), - GasUsed: ap5.TargetGas, - Extra: feeWindowBytes(ap3.Window{}), - BaseFee: big.NewInt(ap4.MinBaseFee), + Number: big.NewInt(1), + GasUsed: ap5.TargetGas, + Extra: feeWindowBytes(ap3.Window{}), + BaseFee: big.NewInt(ap4.MinBaseFee), + }, + parentExtra: &types.HeaderExtra{ ExtDataGasUsed: big.NewInt(ap5.TargetGas), }, timestamp: 1, @@ -294,10 +301,12 @@ func TestBaseFee(t *testing.T) { name: "etna_increase", upgrades: params.GetExtra(params.TestEtnaChainConfig).NetworkUpgrades, parent: &types.Header{ - Number: big.NewInt(1), - GasUsed: ap5.TargetGas, - Extra: feeWindowBytes(ap3.Window{}), - BaseFee: big.NewInt(etna.MinBaseFee), + Number: big.NewInt(1), + GasUsed: ap5.TargetGas, + Extra: feeWindowBytes(ap3.Window{}), + BaseFee: big.NewInt(etna.MinBaseFee), + }, + parentExtra: &types.HeaderExtra{ ExtDataGasUsed: big.NewInt(ap5.TargetGas), }, timestamp: 1, @@ -323,6 +332,9 @@ func TestBaseFee(t *testing.T) { config := &extras.ChainConfig{ NetworkUpgrades: test.upgrades, } + if test.parentExtra != nil { + types.SetHeaderExtra(test.parent, test.parentExtra) + } got, err := BaseFee(config, test.parent, test.timestamp) require.ErrorIs(err, test.wantErr) require.Equal(test.want, got) diff --git a/plugin/evm/header/block_gas_cost.go b/plugin/evm/header/block_gas_cost.go index 387b519e83..18e96879b6 100644 --- a/plugin/evm/header/block_gas_cost.go +++ b/plugin/evm/header/block_gas_cost.go @@ -39,7 +39,7 @@ func BlockGasCost( timeElapsed = timestamp - parent.Time } return BlockGasCostWithStep( - parent.BlockGasCost, + types.GetHeaderExtra(parent).BlockGasCost, step, timeElapsed, ) @@ -82,24 +82,25 @@ func EstimateRequiredTip( config *extras.ChainConfig, header *types.Header, ) (*big.Int, error) { + extra := types.GetHeaderExtra(header) switch { case !config.IsApricotPhase4(header.Time): return nil, nil case header.BaseFee == nil: return nil, errBaseFeeNil - case header.BlockGasCost == nil: + case extra.BlockGasCost == nil: return nil, errBlockGasCostNil - case header.ExtDataGasUsed == nil: + case extra.ExtDataGasUsed == nil: return nil, errExtDataGasUsedNil } // totalGasUsed = GasUsed + ExtDataGasUsed totalGasUsed := new(big.Int).SetUint64(header.GasUsed) - totalGasUsed.Add(totalGasUsed, header.ExtDataGasUsed) + totalGasUsed.Add(totalGasUsed, extra.ExtDataGasUsed) // totalRequiredTips = blockGasCost * baseFee totalRequiredTips := new(big.Int) - totalRequiredTips.Mul(header.BlockGasCost, header.BaseFee) + totalRequiredTips.Mul(extra.BlockGasCost, header.BaseFee) // estimatedTip = totalRequiredTips / totalGasUsed estimatedTip := totalRequiredTips.Div(totalRequiredTips, totalGasUsed) diff --git a/plugin/evm/header/block_gas_cost_test.go b/plugin/evm/header/block_gas_cost_test.go index a835c8110c..762baec09c 100644 --- a/plugin/evm/header/block_gas_cost_test.go +++ b/plugin/evm/header/block_gas_cost_test.go @@ -57,9 +57,12 @@ func TestBlockGasCost(t *testing.T) { }, } parent := &types.Header{ - Time: test.parentTime, + Time: test.parentTime, + } + extra := &types.HeaderExtra{ BlockGasCost: test.parentCost, } + types.SetHeaderExtra(parent, extra) assert.Equal(t, test.expected, BlockGasCost( config, @@ -167,6 +170,7 @@ func TestEstimateRequiredTip(t *testing.T) { name string ap4Timestamp *uint64 header *types.Header + headerExtra *types.HeaderExtra want *big.Int wantErr error }{ @@ -178,7 +182,8 @@ func TestEstimateRequiredTip(t *testing.T) { { name: "nil_base_fee", ap4Timestamp: utils.NewUint64(0), - header: &types.Header{ + header: &types.Header{}, + headerExtra: &types.HeaderExtra{ ExtDataGasUsed: big.NewInt(1), BlockGasCost: big.NewInt(1), }, @@ -188,7 +193,9 @@ func TestEstimateRequiredTip(t *testing.T) { name: "nil_block_gas_cost", ap4Timestamp: utils.NewUint64(0), header: &types.Header{ - BaseFee: big.NewInt(1), + BaseFee: big.NewInt(1), + }, + headerExtra: &types.HeaderExtra{ ExtDataGasUsed: big.NewInt(1), }, wantErr: errBlockGasCostNil, @@ -197,7 +204,9 @@ func TestEstimateRequiredTip(t *testing.T) { name: "nil_extra_data_gas_used", ap4Timestamp: utils.NewUint64(0), header: &types.Header{ - BaseFee: big.NewInt(1), + BaseFee: big.NewInt(1), + }, + headerExtra: &types.HeaderExtra{ BlockGasCost: big.NewInt(1), }, wantErr: errExtDataGasUsedNil, @@ -206,9 +215,11 @@ func TestEstimateRequiredTip(t *testing.T) { name: "success", ap4Timestamp: utils.NewUint64(0), header: &types.Header{ - GasUsed: 123, + GasUsed: 123, + BaseFee: big.NewInt(456), + }, + headerExtra: &types.HeaderExtra{ ExtDataGasUsed: big.NewInt(789), - BaseFee: big.NewInt(456), BlockGasCost: big.NewInt(101112), }, // totalGasUsed = GasUsed + ExtDataGasUsed @@ -226,6 +237,7 @@ func TestEstimateRequiredTip(t *testing.T) { ApricotPhase4BlockTimestamp: test.ap4Timestamp, }, } + types.SetHeaderExtra(test.header, test.headerExtra) requiredTip, err := EstimateRequiredTip(config, test.header) require.ErrorIs(err, test.wantErr) require.Equal(test.want, requiredTip) diff --git a/plugin/evm/header/dynamic_fee_windower.go b/plugin/evm/header/dynamic_fee_windower.go index 4e157d316b..d31ba41e6b 100644 --- a/plugin/evm/header/dynamic_fee_windower.go +++ b/plugin/evm/header/dynamic_fee_windower.go @@ -188,8 +188,8 @@ func feeWindow( // At the start of a new network, the parent may not have a populated // ExtDataGasUsed. - if parent.ExtDataGasUsed != nil { - parentExtraStateGasUsed = parent.ExtDataGasUsed.Uint64() + if used := types.GetHeaderExtra(parent).ExtDataGasUsed; used != nil { + parentExtraStateGasUsed = used.Uint64() } case config.IsApricotPhase4(parent.Time): // The blockGasCost is paid by the effective tips in the block using @@ -199,16 +199,17 @@ func feeWindow( // still calculated using the AP4 step. This is different than the // actual BlockGasCost calculation used for the child block. This // behavior is kept to preserve the original behavior of this function. + parentExtra := types.GetHeaderExtra(parent) blockGasCost = BlockGasCostWithStep( - parent.BlockGasCost, + parentExtra.BlockGasCost, ap4.BlockGasCostStep, timeElapsed, ) // On the boundary of AP3 and AP4 or at the start of a new network, the // parent may not have a populated ExtDataGasUsed. - if parent.ExtDataGasUsed != nil { - parentExtraStateGasUsed = parent.ExtDataGasUsed.Uint64() + if parentExtra.ExtDataGasUsed != nil { + parentExtraStateGasUsed = parentExtra.ExtDataGasUsed.Uint64() } default: blockGasCost = ap3.IntrinsicBlockGas diff --git a/plugin/evm/header/extra_test.go b/plugin/evm/header/extra_test.go index b7da6f2150..80feb394f2 100644 --- a/plugin/evm/header/extra_test.go +++ b/plugin/evm/header/extra_test.go @@ -19,12 +19,13 @@ import ( func TestExtraPrefix(t *testing.T) { tests := []struct { - name string - upgrades extras.NetworkUpgrades - parent *types.Header - timestamp uint64 - want []byte - wantErr error + name string + upgrades extras.NetworkUpgrades + parent *types.Header + parentExtra *types.HeaderExtra + timestamp uint64 + want []byte + wantErr error }{ { name: "ap2", @@ -118,9 +119,11 @@ func TestExtraPrefix(t *testing.T) { name: "ap4_with_block_gas_cost", upgrades: params.GetExtra(params.TestApricotPhase4Config).NetworkUpgrades, parent: &types.Header{ - Number: big.NewInt(1), - GasUsed: ap3.TargetGas, - Extra: feeWindowBytes(ap3.Window{}), + Number: big.NewInt(1), + GasUsed: ap3.TargetGas, + Extra: feeWindowBytes(ap3.Window{}), + }, + parentExtra: &types.HeaderExtra{ BlockGasCost: big.NewInt(ap4.MinBlockGasCost), }, timestamp: 1, @@ -138,9 +141,11 @@ func TestExtraPrefix(t *testing.T) { name: "ap4_with_extra_data_gas", upgrades: params.GetExtra(params.TestApricotPhase4Config).NetworkUpgrades, parent: &types.Header{ - Number: big.NewInt(1), - GasUsed: ap3.TargetGas, - Extra: feeWindowBytes(ap3.Window{}), + Number: big.NewInt(1), + GasUsed: ap3.TargetGas, + Extra: feeWindowBytes(ap3.Window{}), + }, + parentExtra: &types.HeaderExtra{ ExtDataGasUsed: big.NewInt(5), }, timestamp: 1, @@ -163,6 +168,8 @@ func TestExtraPrefix(t *testing.T) { Extra: feeWindowBytes(ap3.Window{ 1, 2, 3, 4, }), + }, + parentExtra: &types.HeaderExtra{ ExtDataGasUsed: big.NewInt(5), BlockGasCost: big.NewInt(ap4.MinBlockGasCost), }, @@ -184,9 +191,11 @@ func TestExtraPrefix(t *testing.T) { name: "ap5_no_extra_data_gas", upgrades: params.GetExtra(params.TestApricotPhase5Config).NetworkUpgrades, parent: &types.Header{ - Number: big.NewInt(1), - GasUsed: ap5.TargetGas, - Extra: feeWindowBytes(ap3.Window{}), + Number: big.NewInt(1), + GasUsed: ap5.TargetGas, + Extra: feeWindowBytes(ap3.Window{}), + }, + parentExtra: &types.HeaderExtra{ BlockGasCost: big.NewInt(ap4.MinBlockGasCost), }, timestamp: 1, @@ -206,6 +215,8 @@ func TestExtraPrefix(t *testing.T) { Extra: feeWindowBytes(ap3.Window{ 1, 2, 3, 4, }), + }, + parentExtra: &types.HeaderExtra{ ExtDataGasUsed: big.NewInt(5), BlockGasCost: big.NewInt(ap4.MinBlockGasCost), }, @@ -230,6 +241,9 @@ func TestExtraPrefix(t *testing.T) { config := &extras.ChainConfig{ NetworkUpgrades: test.upgrades, } + if test.parentExtra != nil { + types.SetHeaderExtra(test.parent, test.parentExtra) + } got, err := ExtraPrefix(config, test.parent, test.timestamp) require.ErrorIs(err, test.wantErr) require.Equal(test.want, got) diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 0955902181..d79c966d1c 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -980,7 +980,8 @@ func testConflictingImportTxs(t *testing.T, genesis string) { } header := types.CopyHeader(validEthBlock.Header()) - header.ExtDataGasUsed.Mul(common.Big2, header.ExtDataGasUsed) + headerExtra := types.GetHeaderExtra(header) + headerExtra.ExtDataGasUsed.Mul(common.Big2, headerExtra.ExtDataGasUsed) internalConflictBlock := types.NewBlockWithExtData( header, @@ -2449,7 +2450,7 @@ func TestEmptyBlock(t *testing.T) { false, ) - if len(emptyEthBlock.ExtData()) != 0 || emptyEthBlock.Header().ExtDataHash != (common.Hash{}) { + if len(emptyEthBlock.ExtData()) != 0 || types.GetHeaderExtra(emptyEthBlock.Header()).ExtDataHash != (common.Hash{}) { t.Fatalf("emptyEthBlock should not have any extra data") } diff --git a/scripts/lint_allowed_eth_imports.sh b/scripts/lint_allowed_eth_imports.sh index fa17b2e65f..967d22c20f 100755 --- a/scripts/lint_allowed_eth_imports.sh +++ b/scripts/lint_allowed_eth_imports.sh @@ -11,7 +11,7 @@ set -o pipefail # 4. Print out the difference between the search results and the list of specified allowed package imports from libevm. libevm_regexp='"github.com/ava-labs/libevm/.*"' allow_named_imports='eth\w\+ "' -extra_imports=$(find . -type f \( -name "*.go" \) ! -path "./core/main_test.go" \ +extra_imports=$(find . -type f \( -name "*.go" \) ! -path "./core/main_test.go" ! -name "gen_*.go" \ -exec sh -c 'grep "$0" -h "$2" | grep -v "$1" | grep -o "$0"' "${libevm_regexp}" "${allow_named_imports}" {} \; | \ sort -u | comm -23 - ./scripts/eth-allowed-packages.txt) if [ -n "${extra_imports}" ]; then