diff --git a/consensus/dummy/consensus.go b/consensus/dummy/consensus.go index 4188f32bf0..8a43a01f4f 100644 --- a/consensus/dummy/consensus.go +++ b/consensus/dummy/consensus.go @@ -162,8 +162,9 @@ func verifyHeaderGasFields(config *extras.ChainConfig, header *types.Header, par parent, header.Time, ) - if !utils.BigEqual(header.BlockGasCost, expectedBlockGasCost) { - return fmt.Errorf("invalid block gas cost: have %d, want %d", header.BlockGasCost, expectedBlockGasCost) + headerExtra := types.GetHeaderExtra(header) + if !utils.BigEqual(headerExtra.BlockGasCost, expectedBlockGasCost) { + return fmt.Errorf("invalid block gas cost: have %d, want %d", headerExtra.BlockGasCost, expectedBlockGasCost) } return nil } @@ -373,7 +374,8 @@ func (eng *DummyEngine) FinalizeAndAssemble(chain consensus.ChainHeaderReader, h config := params.GetExtra(chain.Config()) // Calculate the required block gas cost for this block. - header.BlockGasCost = customheader.BlockGasCost( + headerExtra := types.GetHeaderExtra(header) + headerExtra.BlockGasCost = customheader.BlockGasCost( config, feeConfig, parent, @@ -383,7 +385,7 @@ func (eng *DummyEngine) FinalizeAndAssemble(chain consensus.ChainHeaderReader, h // Verify that this block covers the block fee. if err := eng.verifyBlockFee( header.BaseFee, - header.BlockGasCost, + headerExtra.BlockGasCost, txs, receipts, ); err != nil { diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 83861d7f5d..c8aa5fc7b8 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -379,7 +379,8 @@ func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Tr } if params.GetExtra(config).IsSubnetEVM(header.Time) { - header.BlockGasCost = big.NewInt(0) + headerExtra := types.GetHeaderExtra(header) + headerExtra.BlockGasCost = 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/account.go b/core/types/account.go index efc0927770..e85c774c8b 100644 --- a/core/types/account.go +++ b/core/types/account.go @@ -28,7 +28,7 @@ import ( "github.com/ava-labs/libevm/common/math" ) -//go:generate go run github.com/fjl/gencodec -type Account -field-override accountMarshaling -out gen_account.go +//go:generate go run github.com/fjl/gencodec@a3c3302847cea77ab534228aefa025992dc2c696 -type Account -field-override accountMarshaling -out gen_account.go // Account represents an Ethereum account and its attached data. // This type is used to specify accounts in the genesis block state, and diff --git a/core/types/block.go b/core/types/block.go index fa234d9326..98235b5cf3 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -31,123 +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"` - - // BaseFee was added by EIP-1559 and is ignored in legacy headers. - BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` - - // BlockGasCost was added by SubnetEVM 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 - 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 { @@ -232,6 +121,10 @@ func NewBlock( // CopyHeader creates a deep copy of a block header. func CopyHeader(h *Header) *Header { cpy := *h + hExtra := GetHeaderExtra(h) + cpyExtra := &HeaderExtra{} + SetHeaderExtra(&cpy, cpyExtra) + if cpy.Difficulty = new(big.Int); h.Difficulty != nil { cpy.Difficulty.Set(h.Difficulty) } @@ -241,13 +134,17 @@ func CopyHeader(h *Header) *Header { if h.BaseFee != nil { cpy.BaseFee = new(big.Int).Set(h.BaseFee) } - 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 @@ -358,10 +255,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_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/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 3921526a6b..6fdc48e785 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"` @@ -38,7 +38,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 @@ -64,8 +64,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"` @@ -87,60 +87,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 { diff --git a/core/types/gen_header_rlp.go b/core/types/gen_header_serializable_rlp.go similarity index 96% rename from core/types/gen_header_rlp.go rename to core/types/gen_header_serializable_rlp.go index 8ad0c44146..6964f0443a 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 new file mode 100644 index 0000000000..a7faca1b91 --- /dev/null +++ b/core/types/header_ext.go @@ -0,0 +1,201 @@ +// (c) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +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 Subnet-EVM +// 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 { + 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 { + 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 { + 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) { + 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 { + 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.BlockGasCost = extras.BlockGasCost +} + +func (h *HeaderSerializable) updateToExtras(extras *HeaderExtra) { + extras.BlockGasCost = h.BlockGasCost +} + +//go:generate go run github.com/fjl/gencodec@a3c3302847cea77ab534228aefa025992dc2c696 -type HeaderSerializable -field-override headerMarshaling -out gen_header_serializable_json.go +//go:generate go run github.com/ava-labs/libevm/rlp/rlpgen@739ba847f6f407f63fd6a24175b24e56fea583a1 -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"` + + // BaseFee was added by EIP-1559 and is ignored in legacy headers. + BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` + + // BlockGasCost was added by SubnetEVM 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 + 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..70116a65f7 --- /dev/null +++ b/core/types/header_ext_test.go @@ -0,0 +1,169 @@ +// (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 = "f90212a00100000000000000000000000000000000000000000000000000000000000000a00200000000000000000000000000000000000000000000000000000000000000940300000000000000000000000000000000000000a00400000000000000000000000000000000000000000000000000000000000000a00500000000000000000000000000000000000000000000000000000000000000a00600000000000000000000000000000000000000000000000000000000000000b901000700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008090a0b0c0da00e00000000000000000000000000000000000000000000000000000000000000880f0000000000000010151213a01400000000000000000000000000000000000000000000000000000000000000" + wantHashHex = "2453a240c1cfa4eca66bf39db950d5bd57f5e94ffabf9d800497ace33c2a5927" + ) + + 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{ + BlockGasCost: big.NewInt(21), + } + 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 e28905baa8..c9d42ec333 100644 --- a/core/types/imports.go +++ b/core/types/imports.go @@ -15,9 +15,11 @@ type ( AccessTuple = ethtypes.AccessTuple BlobTx = ethtypes.BlobTx BlobTxSidecar = ethtypes.BlobTxSidecar + BlockNonce = ethtypes.BlockNonce Bloom = ethtypes.Bloom DynamicFeeTx = ethtypes.DynamicFeeTx FrontierSigner = ethtypes.FrontierSigner + Header = ethtypes.Header HomesteadSigner = ethtypes.HomesteadSigner LegacyTx = ethtypes.LegacyTx Receipt = ethtypes.Receipt @@ -51,6 +53,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 new file mode 100644 index 0000000000..185fea7595 --- /dev/null +++ b/core/types/libevm.go @@ -0,0 +1,25 @@ +// (c) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package types + +import ( + "io" + + ethtypes "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/rlp" +) + +var extras = ethtypes.RegisterExtras[ + HeaderExtra, *HeaderExtra, + ethtypes.NOOPBlockBodyHooks, *ethtypes.NOOPBlockBodyHooks, + noopStateAccountExtras, +]() + +type noopStateAccountExtras struct{} + +// EncodeRLP implements the [rlp.Encoder] interface. +func (noopStateAccountExtras) EncodeRLP(w io.Writer) error { return nil } + +// DecodeRLP implements the [rlp.Decoder] interface. +func (*noopStateAccountExtras) DecodeRLP(s *rlp.Stream) error { return nil } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 27335d32cd..fa54cc1a8f 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(), @@ -1253,8 +1254,8 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { if head.BaseFee != nil { result["baseFeePerGas"] = (*hexutil.Big)(head.BaseFee) } - 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 04d371bd4e..5aae1d681d 100644 --- a/plugin/evm/block_verification.go +++ b/plugin/evm/block_verification.go @@ -113,13 +113,14 @@ func (v blockValidator) SyntacticVerify(b *Block, rules params.Rules) error { } if rulesExtra.IsSubnetEVM { + blockGasCost := types.GetHeaderExtra(ethHeader).BlockGasCost switch { // Make sure BlockGasCost is not nil // NOTE: ethHeader.BlockGasCost correctness is checked in header verification - case ethHeader.BlockGasCost == nil: + case blockGasCost == nil: return errNilBlockGasCostSubnetEVM - case !ethHeader.BlockGasCost.IsUint64(): - return fmt.Errorf("too large blockGasCost: %d", ethHeader.BlockGasCost) + case !blockGasCost.IsUint64(): + return fmt.Errorf("too large blockGasCost: %d", blockGasCost) } } diff --git a/plugin/evm/header/block_gas_cost.go b/plugin/evm/header/block_gas_cost.go index 49857edc1a..012d3e3a86 100644 --- a/plugin/evm/header/block_gas_cost.go +++ b/plugin/evm/header/block_gas_cost.go @@ -43,7 +43,7 @@ func BlockGasCost( } return new(big.Int).SetUint64(BlockGasCostWithStep( feeConfig, - parent.BlockGasCost, + types.GetHeaderExtra(parent).BlockGasCost, step, timeElapsed, )) @@ -87,12 +87,13 @@ func EstimateRequiredTip( config *extras.ChainConfig, header *types.Header, ) (*big.Int, error) { + extra := types.GetHeaderExtra(header) switch { case !config.IsSubnetEVM(header.Time): return nil, nil case header.BaseFee == nil: return nil, errBaseFeeNil - case header.BlockGasCost == nil: + case extra.BlockGasCost == nil: return nil, errBlockGasCostNil } @@ -106,7 +107,7 @@ func EstimateRequiredTip( // We add totalGasUsed - 1 to ensure that the total required tips // calculation rounds up. totalRequiredTips := new(big.Int) - totalRequiredTips.Mul(header.BlockGasCost, header.BaseFee) + totalRequiredTips.Mul(extra.BlockGasCost, header.BaseFee) totalRequiredTips.Add(totalRequiredTips, totalGasUsed) totalRequiredTips.Sub(totalRequiredTips, common.Big1) diff --git a/plugin/evm/header/block_gas_cost_test.go b/plugin/evm/header/block_gas_cost_test.go index f5dc2fd279..fc10412fd1 100644 --- a/plugin/evm/header/block_gas_cost_test.go +++ b/plugin/evm/header/block_gas_cost_test.go @@ -95,9 +95,12 @@ func BlockGasCostTest(t *testing.T, feeConfig commontype.FeeConfig) { NetworkUpgrades: test.upgrades, } 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, @@ -215,6 +218,7 @@ func TestEstimateRequiredTip(t *testing.T) { name string subnetEVMTimestamp *uint64 header *types.Header + headerExtra *types.HeaderExtra want *big.Int wantErr error }{ @@ -226,7 +230,8 @@ func TestEstimateRequiredTip(t *testing.T) { { name: "nil_base_fee", subnetEVMTimestamp: utils.NewUint64(0), - header: &types.Header{ + header: &types.Header{}, + headerExtra: &types.HeaderExtra{ BlockGasCost: big.NewInt(1), }, wantErr: errBaseFeeNil, @@ -243,8 +248,10 @@ func TestEstimateRequiredTip(t *testing.T) { name: "no_gas_used", subnetEVMTimestamp: utils.NewUint64(0), header: &types.Header{ - GasUsed: 0, - BaseFee: big.NewInt(1), + GasUsed: 0, + BaseFee: big.NewInt(1), + }, + headerExtra: &types.HeaderExtra{ BlockGasCost: big.NewInt(1), }, wantErr: errNoGasUsed, @@ -253,8 +260,10 @@ func TestEstimateRequiredTip(t *testing.T) { name: "success", subnetEVMTimestamp: utils.NewUint64(0), header: &types.Header{ - GasUsed: 912, - BaseFee: big.NewInt(456), + GasUsed: 912, + BaseFee: big.NewInt(456), + }, + headerExtra: &types.HeaderExtra{ BlockGasCost: big.NewInt(101112), }, // totalRequiredTips = BlockGasCost * BaseFee @@ -265,8 +274,10 @@ func TestEstimateRequiredTip(t *testing.T) { name: "success_rounds_up", subnetEVMTimestamp: utils.NewUint64(0), header: &types.Header{ - GasUsed: 124, - BaseFee: big.NewInt(456), + GasUsed: 124, + BaseFee: big.NewInt(456), + }, + headerExtra: &types.HeaderExtra{ BlockGasCost: big.NewInt(101112), }, // totalGasUsed = GasUsed + ExtDataGasUsed @@ -284,6 +295,9 @@ func TestEstimateRequiredTip(t *testing.T) { SubnetEVMTimestamp: test.subnetEVMTimestamp, }, } + if test.headerExtra != nil { + 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/extra_test.go b/plugin/evm/header/extra_test.go index 8e7e57616f..a81b7a6a22 100644 --- a/plugin/evm/header/extra_test.go +++ b/plugin/evm/header/extra_test.go @@ -21,12 +21,13 @@ const ( func TestExtraPrefix(t *testing.T) { tests := []struct { - name string - upgrades extras.NetworkUpgrades - parent *types.Header - header *types.Header - want []byte - wantErr error + name string + upgrades extras.NetworkUpgrades + parent *types.Header + parentExtra *types.HeaderExtra + header *types.Header + want []byte + wantErr error }{ { name: "pre_subnet_evm", @@ -88,6 +89,8 @@ func TestExtraPrefix(t *testing.T) { Extra: (&subnetevm.Window{ 1, 2, 3, 4, }).Bytes(), + }, + parentExtra: &types.HeaderExtra{ BlockGasCost: big.NewInt(blockGas), }, header: &types.Header{ @@ -110,6 +113,10 @@ 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.header) require.ErrorIs(err, test.wantErr) require.Equal(test.want, got) diff --git a/scripts/lint_allowed_eth_imports.sh b/scripts/lint_allowed_eth_imports.sh index 4940d56f5e..d8d56a0fe6 100755 --- a/scripts/lint_allowed_eth_imports.sh +++ b/scripts/lint_allowed_eth_imports.sh @@ -11,7 +11,9 @@ 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=$(grep -r --include='*.go' --exclude=mocks.go --exclude-dir='simulator' "${libevm_regexp}" -h | grep -v "${allow_named_imports}" | grep -o "${libevm_regexp}" | sort -u | comm -23 - ./scripts/eth-allowed-packages.txt) +extra_imports=$(find . -type f \( -name "*.go" \) ! -name "mocks.go" ! -path "simulator" ! -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 echo "new ethereum imports should be added to ./scripts/eth-allowed-packages.txt to prevent accidental imports:" echo "${extra_imports}"