Skip to content

Commit 0657877

Browse files
Darioush Jalaliqdm12ARR4Nceyonur
authored
chore(core/types): header JSON and RLP serialization hooks (#746)
- 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 <[email protected]> Co-authored-by: Quentin Mc Gaw <[email protected]> Co-authored-by: Arran Schlosberg <[email protected]> Co-authored-by: Ceyhun Onur <[email protected]>
1 parent 6448d1c commit 0657877

21 files changed

+650
-247
lines changed

consensus/dummy/consensus.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,13 @@ func (eng *DummyEngine) verifyHeaderGasFields(config *params.ChainConfig, header
159159
}
160160

161161
// Verify BlockGasCost, ExtDataGasUsed not present before AP4
162+
headerExtra := types.GetHeaderExtra(header)
162163
if !configExtra.IsApricotPhase4(header.Time) {
163-
if header.BlockGasCost != nil {
164-
return fmt.Errorf("invalid blockGasCost before fork: have %d, want <nil>", header.BlockGasCost)
164+
if headerExtra.BlockGasCost != nil {
165+
return fmt.Errorf("invalid blockGasCost before fork: have %d, want <nil>", headerExtra.BlockGasCost)
165166
}
166-
if header.ExtDataGasUsed != nil {
167-
return fmt.Errorf("invalid extDataGasUsed before fork: have %d, want <nil>", header.ExtDataGasUsed)
167+
if headerExtra.ExtDataGasUsed != nil {
168+
return fmt.Errorf("invalid extDataGasUsed before fork: have %d, want <nil>", headerExtra.ExtDataGasUsed)
168169
}
169170
return nil
170171
}
@@ -175,16 +176,16 @@ func (eng *DummyEngine) verifyHeaderGasFields(config *params.ChainConfig, header
175176
parent,
176177
header.Time,
177178
)
178-
if !utils.BigEqualUint64(header.BlockGasCost, expectedBlockGasCost) {
179-
return fmt.Errorf("invalid block gas cost: have %d, want %d", header.BlockGasCost, expectedBlockGasCost)
179+
if !utils.BigEqualUint64(headerExtra.BlockGasCost, expectedBlockGasCost) {
180+
return fmt.Errorf("invalid block gas cost: have %d, want %d", headerExtra.BlockGasCost, expectedBlockGasCost)
180181
}
181182

182183
// ExtDataGasUsed correctness is checked during block validation
183184
// (when the validator has access to the block contents)
184-
if header.ExtDataGasUsed == nil {
185+
if headerExtra.ExtDataGasUsed == nil {
185186
return errExtDataGasUsedNil
186187
}
187-
if !header.ExtDataGasUsed.IsUint64() {
188+
if !headerExtra.ExtDataGasUsed.IsUint64() {
188189
return errExtDataGasUsedTooLarge
189190
}
190191
return nil
@@ -422,23 +423,24 @@ func (eng *DummyEngine) FinalizeAndAssemble(chain consensus.ChainHeaderReader, h
422423
}
423424

424425
configExtra := params.GetExtra(chain.Config())
426+
headerExtra := types.GetHeaderExtra(header)
425427
if configExtra.IsApricotPhase4(header.Time) {
426-
header.ExtDataGasUsed = extDataGasUsed
427-
if header.ExtDataGasUsed == nil {
428-
header.ExtDataGasUsed = new(big.Int).Set(common.Big0)
428+
headerExtra.ExtDataGasUsed = extDataGasUsed
429+
if headerExtra.ExtDataGasUsed == nil {
430+
headerExtra.ExtDataGasUsed = new(big.Int).Set(common.Big0)
429431
}
430432
// Calculate the required block gas cost for this block.
431433
blockGasCost := customheader.BlockGasCost(
432434
configExtra,
433435
parent,
434436
header.Time,
435437
)
436-
header.BlockGasCost = new(big.Int).SetUint64(blockGasCost)
438+
headerExtra.BlockGasCost = new(big.Int).SetUint64(blockGasCost)
437439

438440
// Verify that this block covers the block fee.
439441
if err := eng.verifyBlockFee(
440442
header.BaseFee,
441-
header.BlockGasCost,
443+
headerExtra.BlockGasCost,
442444
txs,
443445
receipts,
444446
contribution,

core/state_processor_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,8 +383,9 @@ func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Tr
383383
BaseFee: baseFee,
384384
}
385385
if configExtra.IsApricotPhase4(header.Time) {
386-
header.BlockGasCost = big.NewInt(0)
387-
header.ExtDataGasUsed = big.NewInt(0)
386+
headerExtra := types.GetHeaderExtra(header)
387+
headerExtra.BlockGasCost = big.NewInt(0)
388+
headerExtra.ExtDataGasUsed = big.NewInt(0)
388389
}
389390
var receipts []*types.Receipt
390391
// The post-state result doesn't need to be correct (this is a bad block), but we do need something there

core/types/block.go

Lines changed: 17 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -31,132 +31,12 @@ import (
3131
"encoding/binary"
3232
"io"
3333
"math/big"
34-
"reflect"
3534
"sync/atomic"
3635

3736
"github.com/ava-labs/libevm/common"
38-
"github.com/ava-labs/libevm/common/hexutil"
3937
"github.com/ava-labs/libevm/rlp"
4038
)
4139

42-
// A BlockNonce is a 64-bit hash which proves (combined with the
43-
// mix-hash) that a sufficient amount of computation has been carried
44-
// out on a block.
45-
type BlockNonce [8]byte
46-
47-
// EncodeNonce converts the given integer to a block nonce.
48-
func EncodeNonce(i uint64) BlockNonce {
49-
var n BlockNonce
50-
binary.BigEndian.PutUint64(n[:], i)
51-
return n
52-
}
53-
54-
// Uint64 returns the integer value of a block nonce.
55-
func (n BlockNonce) Uint64() uint64 {
56-
return binary.BigEndian.Uint64(n[:])
57-
}
58-
59-
// MarshalText encodes n as a hex string with 0x prefix.
60-
func (n BlockNonce) MarshalText() ([]byte, error) {
61-
return hexutil.Bytes(n[:]).MarshalText()
62-
}
63-
64-
// UnmarshalText implements encoding.TextUnmarshaler.
65-
func (n *BlockNonce) UnmarshalText(input []byte) error {
66-
return hexutil.UnmarshalFixedText("BlockNonce", input, n[:])
67-
}
68-
69-
//go:generate go run github.com/fjl/gencodec -type Header -field-override headerMarshaling -out gen_header_json.go
70-
//go:generate go run github.com/ava-labs/libevm/rlp/rlpgen -type Header -out gen_header_rlp.go
71-
72-
// Header represents a block header in the Ethereum blockchain.
73-
type Header struct {
74-
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
75-
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
76-
Coinbase common.Address `json:"miner" gencodec:"required"`
77-
Root common.Hash `json:"stateRoot" gencodec:"required"`
78-
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
79-
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
80-
Bloom Bloom `json:"logsBloom" gencodec:"required"`
81-
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
82-
Number *big.Int `json:"number" gencodec:"required"`
83-
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
84-
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
85-
Time uint64 `json:"timestamp" gencodec:"required"`
86-
Extra []byte `json:"extraData" gencodec:"required"`
87-
MixDigest common.Hash `json:"mixHash"`
88-
Nonce BlockNonce `json:"nonce"`
89-
ExtDataHash common.Hash `json:"extDataHash" gencodec:"required"`
90-
91-
// BaseFee was added by EIP-1559 and is ignored in legacy headers.
92-
BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`
93-
94-
// ExtDataGasUsed was added by Apricot Phase 4 and is ignored in legacy
95-
// headers.
96-
//
97-
// It is not a uint64 like GasLimit or GasUsed because it is not possible to
98-
// correctly encode this field optionally with uint64.
99-
ExtDataGasUsed *big.Int `json:"extDataGasUsed" rlp:"optional"`
100-
101-
// BlockGasCost was added by Apricot Phase 4 and is ignored in legacy
102-
// headers.
103-
BlockGasCost *big.Int `json:"blockGasCost" rlp:"optional"`
104-
105-
// BlobGasUsed was added by EIP-4844 and is ignored in legacy headers.
106-
BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"`
107-
108-
// ExcessBlobGas was added by EIP-4844 and is ignored in legacy headers.
109-
ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"`
110-
111-
// ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers.
112-
ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"`
113-
}
114-
115-
// field type overrides for gencodec
116-
type headerMarshaling struct {
117-
Difficulty *hexutil.Big
118-
Number *hexutil.Big
119-
GasLimit hexutil.Uint64
120-
GasUsed hexutil.Uint64
121-
Time hexutil.Uint64
122-
Extra hexutil.Bytes
123-
BaseFee *hexutil.Big
124-
ExtDataGasUsed *hexutil.Big
125-
BlockGasCost *hexutil.Big
126-
Hash common.Hash `json:"hash"` // adds call to Hash() in MarshalJSON
127-
BlobGasUsed *hexutil.Uint64
128-
ExcessBlobGas *hexutil.Uint64
129-
}
130-
131-
// Hash returns the block hash of the header, which is simply the keccak256 hash of its
132-
// RLP encoding.
133-
func (h *Header) Hash() common.Hash {
134-
return rlpHash(h)
135-
}
136-
137-
var headerSize = common.StorageSize(reflect.TypeOf(Header{}).Size())
138-
139-
// Size returns the approximate memory used by all internal contents. It is used
140-
// to approximate and limit the memory consumption of various caches.
141-
func (h *Header) Size() common.StorageSize {
142-
var baseFeeBits int
143-
if h.BaseFee != nil {
144-
baseFeeBits = h.BaseFee.BitLen()
145-
}
146-
return headerSize + common.StorageSize(len(h.Extra)+(h.Difficulty.BitLen()+h.Number.BitLen()+baseFeeBits)/8)
147-
}
148-
149-
// EmptyBody returns true if there is no additional 'body' to complete the header
150-
// that is: no transactions and no uncles.
151-
func (h *Header) EmptyBody() bool {
152-
return h.TxHash == EmptyTxsHash && h.UncleHash == EmptyUncleHash
153-
}
154-
155-
// EmptyReceipts returns true if there are no receipts for this header/block.
156-
func (h *Header) EmptyReceipts() bool {
157-
return h.ReceiptHash == EmptyReceiptsHash
158-
}
159-
16040
// Body is a simple (mutable, non-safe) data container for storing and moving
16141
// a block's data contents (transactions and uncles) together.
16242
type Body struct {
@@ -249,6 +129,12 @@ func NewBlock(
249129
// CopyHeader creates a deep copy of a block header.
250130
func CopyHeader(h *Header) *Header {
251131
cpy := *h
132+
hExtra := GetHeaderExtra(h)
133+
cpyExtra := &HeaderExtra{
134+
ExtDataHash: hExtra.ExtDataHash,
135+
}
136+
SetHeaderExtra(&cpy, cpyExtra)
137+
252138
if cpy.Difficulty = new(big.Int); h.Difficulty != nil {
253139
cpy.Difficulty.Set(h.Difficulty)
254140
}
@@ -258,16 +144,20 @@ func CopyHeader(h *Header) *Header {
258144
if h.BaseFee != nil {
259145
cpy.BaseFee = new(big.Int).Set(h.BaseFee)
260146
}
261-
if h.ExtDataGasUsed != nil {
262-
cpy.ExtDataGasUsed = new(big.Int).Set(h.ExtDataGasUsed)
147+
if hExtra.ExtDataGasUsed != nil {
148+
cpyExtra.ExtDataGasUsed = new(big.Int).Set(hExtra.ExtDataGasUsed)
263149
}
264-
if h.BlockGasCost != nil {
265-
cpy.BlockGasCost = new(big.Int).Set(h.BlockGasCost)
150+
if hExtra.BlockGasCost != nil {
151+
cpyExtra.BlockGasCost = new(big.Int).Set(hExtra.BlockGasCost)
266152
}
267153
if len(h.Extra) > 0 {
268154
cpy.Extra = make([]byte, len(h.Extra))
269155
copy(cpy.Extra, h.Extra)
270156
}
157+
if h.WithdrawalsHash != nil {
158+
cpy.WithdrawalsHash = new(common.Hash)
159+
*cpy.WithdrawalsHash = *h.WithdrawalsHash
160+
}
271161
if h.ExcessBlobGas != nil {
272162
cpy.ExcessBlobGas = new(uint64)
273163
*cpy.ExcessBlobGas = *h.ExcessBlobGas
@@ -380,10 +270,11 @@ func (b *Block) BlobGasUsed() *uint64 {
380270
}
381271

382272
func (b *Block) BlockGasCost() *big.Int {
383-
if b.header.BlockGasCost == nil {
273+
cost := GetHeaderExtra(b.header).BlockGasCost
274+
if cost == nil {
384275
return nil
385276
}
386-
return new(big.Int).Set(b.header.BlockGasCost)
277+
return new(big.Int).Set(cost)
387278
}
388279

389280
// Size returns the true RLP encoded storage size of the block, either by encoding

core/types/block_ext.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func (b *Block) setExtData(data []byte, recalc bool) {
2828
b.extdata = &_data
2929
copy(*b.extdata, data)
3030
if recalc {
31-
b.header.ExtDataHash = CalcExtDataHash(*b.extdata)
31+
GetHeaderExtra(b.header).ExtDataHash = CalcExtDataHash(*b.extdata)
3232
}
3333
}
3434

@@ -44,10 +44,11 @@ func (b *Block) Version() uint32 {
4444
}
4545

4646
func (b *Block) ExtDataGasUsed() *big.Int {
47-
if b.header.ExtDataGasUsed == nil {
47+
used := GetHeaderExtra(b.header).ExtDataGasUsed
48+
if used == nil {
4849
return nil
4950
}
50-
return new(big.Int).Set(b.header.ExtDataGasUsed)
51+
return new(big.Int).Set(used)
5152
}
5253

5354
func CalcExtDataHash(extdata []byte) common.Hash {

core/types/block_ext_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// (c) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package types
5+
6+
import (
7+
"math/big"
8+
"reflect"
9+
"testing"
10+
"unsafe"
11+
12+
"github.com/ava-labs/libevm/common"
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestCopyHeader(t *testing.T) {
17+
t.Parallel()
18+
19+
t.Run("empty_header", func(t *testing.T) {
20+
t.Parallel()
21+
22+
empty := &Header{}
23+
24+
headerExtra := &HeaderExtra{}
25+
extras.Header.Set(empty, headerExtra)
26+
27+
cpy := CopyHeader(empty)
28+
29+
want := &Header{
30+
Difficulty: new(big.Int),
31+
Number: new(big.Int),
32+
}
33+
34+
headerExtra = &HeaderExtra{}
35+
extras.Header.Set(want, headerExtra)
36+
37+
assert.Equal(t, want, cpy)
38+
})
39+
40+
t.Run("filled_header", func(t *testing.T) {
41+
t.Parallel()
42+
43+
header, _ := headerWithNonZeroFields() // the header carries the [HeaderExtra] so we can ignore it
44+
45+
gotHeader := CopyHeader(header)
46+
gotExtra := GetHeaderExtra(gotHeader)
47+
48+
wantHeader, wantExtra := headerWithNonZeroFields()
49+
assert.Equal(t, wantHeader, gotHeader)
50+
assert.Equal(t, wantExtra, gotExtra)
51+
52+
exportedFieldsPointToDifferentMemory(t, header, gotHeader)
53+
exportedFieldsPointToDifferentMemory(t, GetHeaderExtra(header), gotExtra)
54+
})
55+
}
56+
57+
func exportedFieldsPointToDifferentMemory[T interface {
58+
Header | HeaderExtra
59+
}](t *testing.T, original, cpy *T) {
60+
t.Helper()
61+
62+
v := reflect.ValueOf(*original)
63+
typ := v.Type()
64+
cp := reflect.ValueOf(*cpy)
65+
for i := range v.NumField() {
66+
field := typ.Field(i)
67+
if !field.IsExported() {
68+
continue
69+
}
70+
switch field.Type.Kind() {
71+
case reflect.Array, reflect.Uint64:
72+
// Not pointers, but using explicit Kinds for safety
73+
continue
74+
}
75+
76+
t.Run(field.Name, func(t *testing.T) {
77+
fieldCp := cp.Field(i).Interface()
78+
switch f := v.Field(i).Interface().(type) {
79+
case *big.Int:
80+
assertDifferentPointers(t, f, fieldCp)
81+
case *common.Hash:
82+
assertDifferentPointers(t, f, fieldCp)
83+
case *uint64:
84+
assertDifferentPointers(t, f, fieldCp)
85+
case []uint8:
86+
assertDifferentPointers(t, unsafe.SliceData(f), unsafe.SliceData(fieldCp.([]uint8)))
87+
default:
88+
t.Errorf("field %q type %T needs to be added to switch cases of exportedFieldsDeepCopied", field.Name, f)
89+
}
90+
})
91+
}
92+
}
93+
94+
// assertDifferentPointers asserts that `a` and `b` are both non-nil
95+
// pointers pointing to different memory locations.
96+
func assertDifferentPointers[T any](t *testing.T, a *T, b any) {
97+
t.Helper()
98+
switch {
99+
case a == nil:
100+
t.Errorf("a (%T) cannot be nil", a)
101+
case b == nil:
102+
t.Errorf("b (%T) cannot be nil", b)
103+
case a == b:
104+
t.Errorf("pointers to same memory")
105+
}
106+
// Note: no need to check `b` is of the same type as `a`, otherwise
107+
// the memory address would be different as well.
108+
}

0 commit comments

Comments
 (0)