Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(core/types): header JSON and RLP serialization hooks (4) #746

Merged
merged 29 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ce204e6
chore(core/types): header hooks JSON and RLP serialization
qdm12 Feb 10, 2025
aa2e380
Add `TestCopyHeader`
qdm12 Feb 12, 2025
0ef64fb
Add TestHeaderExtraGetWith
qdm12 Feb 12, 2025
94ccf1d
Add `TestHeaderSerializable_updates`
qdm12 Feb 12, 2025
2c672c2
`TestHeaderExtraRLP`
qdm12 Feb 12, 2025
96f13ba
TestHeaderExtraJSON
qdm12 Feb 12, 2025
0bac977
Add comments on exported symbols
qdm12 Feb 12, 2025
70124de
Remove test import on libevm/pseudo
qdm12 Feb 21, 2025
cbd644a
Simplify changes in Block.ExtDataGasUsed()
qdm12 Feb 21, 2025
4dbb36e
Simplify changes in Block.BlockGasCost()
qdm12 Feb 21, 2025
59cbb97
Apply PR review suggestions
qdm12 Feb 21, 2025
0ecd53e
WithHeaderExtra -> SetHeaderExtra
qdm12 Feb 21, 2025
15638ec
Add missing SetHeaderExtra calls in plugin/evm/header tests
qdm12 Feb 21, 2025
718e9f7
plugin/evm/header: add missing extra nil checks
qdm12 Feb 21, 2025
d58c459
Minor improvements (feedback)
qdm12 Feb 24, 2025
12811e5
Chery pick ARR4N commit with modifications
ARR4N Feb 21, 2025
4ae3869
PR feedback round 3
qdm12 Feb 24, 2025
7a05828
scripts/lint_allowed_eth_imports.sh: use `-name` instead of `-path` f…
qdm12 Feb 25, 2025
aefb9c1
PR feedback nits
qdm12 Feb 25, 2025
089432e
Remove core/types from scripts/eth-allowed-packages.txt
qdm12 Feb 25, 2025
3b9ef2a
Add comment to HeaderSerializable.Hash method
qdm12 Feb 25, 2025
728cbfe
Re-generate JSON for HeaderSerializable
qdm12 Feb 25, 2025
aab9d7e
Rename generated codec files
qdm12 Feb 25, 2025
56b296b
Add note on custom generators used to support type aliases
qdm12 Feb 25, 2025
a7f9917
remove vmerrs (#829)
ceyonur Feb 24, 2025
4d2f143
Simplify assertDifferentPointers to not rely on testify/assert
qdm12 Feb 25, 2025
57a6734
Revert to using assertNonZero function in `allExportedFieldsSet`
qdm12 Feb 25, 2025
d360c2c
Improve `testHeaderEncodeDecode` signature definition
qdm12 Feb 25, 2025
949ce6d
Merge branch 'libevm' into libevm-upstream-types
qdm12 Feb 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions consensus/dummy/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <nil>", header.BlockGasCost)
if headerExtra.BlockGasCost != nil {
return fmt.Errorf("invalid blockGasCost before fork: have %d, want <nil>", headerExtra.BlockGasCost)
}
if header.ExtDataGasUsed != nil {
return fmt.Errorf("invalid extDataGasUsed before fork: have %d, want <nil>", header.ExtDataGasUsed)
if headerExtra.ExtDataGasUsed != nil {
return fmt.Errorf("invalid extDataGasUsed before fork: have %d, want <nil>", headerExtra.ExtDataGasUsed)
}
return nil
}
Expand All @@ -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
Expand Down Expand Up @@ -422,23 +423,24 @@ 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(
configExtra,
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,
Expand Down
5 changes: 3 additions & 2 deletions core/state_processor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
143 changes: 17 additions & 126 deletions core/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions core/types/block_ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -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 {
Expand Down
108 changes: 108 additions & 0 deletions core/types/block_ext_test.go
Original file line number Diff line number Diff line change
@@ -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.
}
Loading
Loading