Skip to content

Commit

Permalink
chore(core/types): header JSON and RLP serialization hooks (#746)
Browse files Browse the repository at this point in the history
- Implement `HeaderExtra` RLP and JSON serialization methods
- remove `BlockNonce`
- remove `EncodeNonce`
- new functions `GetHeaderExtra` and `SetHeaderExtra` 
- Migrate existing custom `Header` to `HeaderSerializable` in block_ext.go with only `Hash` method for RLP code generation
- Rename files gen_header_json.go to gen_header_serializable_json.go
- Rename files gen_header_rlp.go to gen_header_serializable_rlp.go

Signed-off-by: Quentin McGaw <quentin.mcgaw@gmail.com>
Co-authored-by: Quentin Mc Gaw <quentin.mcgaw@avalabs.org>
Co-authored-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com>
Co-authored-by: Ceyhun Onur <ceyhun.onur@avalabs.org>
  • Loading branch information
4 people authored Feb 25, 2025
1 parent 6448d1c commit 0657877
Show file tree
Hide file tree
Showing 21 changed files with 650 additions and 247 deletions.
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

0 comments on commit 0657877

Please sign in to comment.