From 8685b80f23b8cdb422b03b9a48e19eb3fe0fc98e Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Tue, 9 Jun 2026 14:05:22 +0200 Subject: [PATCH] beacon/engine/ssz: update types --- beacon/engine/ssz/attributes_forks.go | 136 -------------- beacon/engine/ssz/bodies.go | 161 +++++++--------- beacon/engine/ssz/bytecompat_test.go | 146 ++++++++++++++ beacon/engine/ssz/convert.go | 151 +++++++++------ beacon/engine/ssz/envelopes.go | 6 +- beacon/engine/ssz/forkmap.go | 60 ++++++ beacon/engine/ssz/payload.go | 251 +++++++++++++++++++++++++ beacon/engine/ssz/payload_amsterdam.go | 150 --------------- beacon/engine/ssz/payload_forks.go | 185 ------------------ beacon/engine/ssz/roundtrip_test.go | 48 +++-- beacon/engine/ssz/validate_test.go | 120 ++++++++++++ eth/engineapi/blobs.go | 11 +- eth/engineapi/bodies.go | 57 +++--- eth/engineapi/forkchoice.go | 20 +- eth/engineapi/forkresolve.go | 41 ++++ eth/engineapi/get_payload.go | 13 +- eth/engineapi/media.go | 19 +- eth/engineapi/payloads.go | 16 +- eth/engineapi/router_test.go | 32 ++-- 19 files changed, 921 insertions(+), 702 deletions(-) delete mode 100644 beacon/engine/ssz/attributes_forks.go create mode 100644 beacon/engine/ssz/bytecompat_test.go create mode 100644 beacon/engine/ssz/forkmap.go create mode 100644 beacon/engine/ssz/payload.go delete mode 100644 beacon/engine/ssz/payload_amsterdam.go delete mode 100644 beacon/engine/ssz/payload_forks.go create mode 100644 beacon/engine/ssz/validate_test.go create mode 100644 eth/engineapi/forkresolve.go diff --git a/beacon/engine/ssz/attributes_forks.go b/beacon/engine/ssz/attributes_forks.go deleted file mode 100644 index 38ecbf2e2c..0000000000 --- a/beacon/engine/ssz/attributes_forks.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2026 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package ssz - -import ( - "github.com/ethereum/go-ethereum/common" - "github.com/karalabe/ssz" -) - -// PayloadAttributesCancun is the Cancun/Prague/Osaka build attribute shape. -type PayloadAttributesCancun struct { - Timestamp uint64 - PrevRandao common.Hash - SuggestedFeeRecipient common.Address - Withdrawals []*Withdrawal - ParentBeaconBlockRoot common.Hash -} - -func (a *PayloadAttributesCancun) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - // 8 + 32 + 20 + 4(offset) + 32 = 96 - size := uint32(96) - if fixed { - return size - } - size += ssz.SizeSliceOfStaticObjects(siz, a.Withdrawals) - return size -} - -func (a *PayloadAttributesCancun) DefineSSZ(c *ssz.Codec) { - ssz.DefineUint64(c, &a.Timestamp) - ssz.DefineStaticBytes(c, &a.PrevRandao) - ssz.DefineStaticBytes(c, &a.SuggestedFeeRecipient) - ssz.DefineSliceOfStaticObjectsOffset(c, &a.Withdrawals, MaxWithdrawalsPerPayload) - ssz.DefineStaticBytes(c, &a.ParentBeaconBlockRoot) - - ssz.DefineSliceOfStaticObjectsContent(c, &a.Withdrawals, MaxWithdrawalsPerPayload) -} - -// PayloadAttributesShanghai = Cancun minus parent_beacon_block_root. -type PayloadAttributesShanghai struct { - Timestamp uint64 - PrevRandao common.Hash - SuggestedFeeRecipient common.Address - Withdrawals []*Withdrawal -} - -func (a *PayloadAttributesShanghai) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - size := uint32(8 + 32 + 20 + 4) - if fixed { - return size - } - size += ssz.SizeSliceOfStaticObjects(siz, a.Withdrawals) - return size -} - -func (a *PayloadAttributesShanghai) DefineSSZ(c *ssz.Codec) { - ssz.DefineUint64(c, &a.Timestamp) - ssz.DefineStaticBytes(c, &a.PrevRandao) - ssz.DefineStaticBytes(c, &a.SuggestedFeeRecipient) - ssz.DefineSliceOfStaticObjectsOffset(c, &a.Withdrawals, MaxWithdrawalsPerPayload) - - ssz.DefineSliceOfStaticObjectsContent(c, &a.Withdrawals, MaxWithdrawalsPerPayload) -} - -// PayloadAttributesParis is the original (no withdrawals) shape. -type PayloadAttributesParis struct { - Timestamp uint64 - PrevRandao common.Hash - SuggestedFeeRecipient common.Address -} - -func (*PayloadAttributesParis) SizeSSZ(*ssz.Sizer) uint32 { return 60 } - -func (a *PayloadAttributesParis) DefineSSZ(c *ssz.Codec) { - ssz.DefineUint64(c, &a.Timestamp) - ssz.DefineStaticBytes(c, &a.PrevRandao) - ssz.DefineStaticBytes(c, &a.SuggestedFeeRecipient) -} - -// ExecutionPayloadBodyCancun = Shanghai-shaped body (no BAL). -type ExecutionPayloadBodyCancun struct { - Transactions [][]byte - Withdrawals []*Withdrawal -} - -func (b *ExecutionPayloadBodyCancun) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - size := uint32(8) - if fixed { - return size - } - size += ssz.SizeSliceOfDynamicBytes(siz, b.Transactions) - size += ssz.SizeSliceOfStaticObjects(siz, b.Withdrawals) - return size -} - -func (b *ExecutionPayloadBodyCancun) DefineSSZ(c *ssz.Codec) { - ssz.DefineSliceOfDynamicBytesOffset(c, &b.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - ssz.DefineSliceOfStaticObjectsOffset(c, &b.Withdrawals, MaxWithdrawalsPerPayload) - - ssz.DefineSliceOfDynamicBytesContent(c, &b.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - ssz.DefineSliceOfStaticObjectsContent(c, &b.Withdrawals, MaxWithdrawalsPerPayload) -} - -// ExecutionPayloadBodyParis is the original transactions-only body. -type ExecutionPayloadBodyParis struct { - Transactions [][]byte -} - -func (b *ExecutionPayloadBodyParis) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - size := uint32(4) - if fixed { - return size - } - size += ssz.SizeSliceOfDynamicBytes(siz, b.Transactions) - return size -} - -func (b *ExecutionPayloadBodyParis) DefineSSZ(c *ssz.Codec) { - ssz.DefineSliceOfDynamicBytesOffset(c, &b.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - - ssz.DefineSliceOfDynamicBytesContent(c, &b.Transactions, MaxTxsPerPayload, MaxBytesPerTx) -} diff --git a/beacon/engine/ssz/bodies.go b/beacon/engine/ssz/bodies.go index 6753309fe9..252535a228 100644 --- a/beacon/engine/ssz/bodies.go +++ b/beacon/engine/ssz/bodies.go @@ -17,6 +17,8 @@ package ssz import ( + "fmt" + "github.com/ethereum/go-ethereum/common" "github.com/karalabe/ssz" ) @@ -40,13 +42,70 @@ func (r *BodiesByHashRequest) DefineSSZ(c *ssz.Codec) { ssz.DefineSliceOfStaticBytesContent(c, &r.BlockHashes, MaxBodiesRequest) } -// BodyEntryAmsterdam is the per-block entry returned by /amsterdam/bodies/... -type BodyEntryAmsterdam struct { - Available bool - Body *ExecutionPayloadBodyAmsterdam +// ExecutionPayloadBody is the monolithic body shape returned by /{fork}/bodies. +// +// Field groups: +// - base (Paris): Transactions +// - withdrawals (Shanghai): Withdrawals +// - bal (Amsterdam): BlockAccessList +type ExecutionPayloadBody struct { + Transactions [][]byte + Withdrawals []*Withdrawal // Shanghai+ + BlockAccessList []byte // Amsterdam+ } -func (e *BodyEntryAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { +func (b *ExecutionPayloadBody) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { + var size uint32 = 4 // transactions offset + if siz.Fork() >= ssz.ForkShapella { + size += 4 // withdrawals offset + } + if siz.Fork() >= forkAmsterdam { + size += 4 // block_access_list offset + } + if fixed { + return size + } + size += ssz.SizeSliceOfDynamicBytes(siz, b.Transactions) + if siz.Fork() >= ssz.ForkShapella { + size += ssz.SizeSliceOfStaticObjects(siz, b.Withdrawals) + } + if siz.Fork() >= forkAmsterdam { + size += ssz.SizeDynamicBytes(siz, b.BlockAccessList) + } + return size +} + +func (b *ExecutionPayloadBody) DefineSSZ(c *ssz.Codec) { + // offset phase + ssz.DefineSliceOfDynamicBytesOffset(c, &b.Transactions, MaxTxsPerPayload, MaxBytesPerTx) + ssz.DefineSliceOfStaticObjectsOffsetOnFork(c, &b.Withdrawals, MaxWithdrawalsPerPayload, fromShanghai) + ssz.DefineDynamicBytesOffsetOnFork(c, &b.BlockAccessList, MaxBalBytes, fromAmsterdam) + + // content phase + ssz.DefineSliceOfDynamicBytesContent(c, &b.Transactions, MaxTxsPerPayload, MaxBytesPerTx) + ssz.DefineSliceOfStaticObjectsContentOnFork(c, &b.Withdrawals, MaxWithdrawalsPerPayload, fromShanghai) + ssz.DefineDynamicBytesContentOnFork(c, &b.BlockAccessList, MaxBalBytes, fromAmsterdam) +} + +// Validate enforces that fields absent for the given fork are empty. +func (b *ExecutionPayloadBody) Validate(fork ssz.Fork) error { + if fork < ssz.ForkShapella && len(b.Withdrawals) > 0 { + return fmt.Errorf("withdrawals set but fork %d predates Shanghai", fork) + } + if fork < forkAmsterdam && len(b.BlockAccessList) > 0 { + return fmt.Errorf("block_access_list set but fork %d predates Amsterdam", fork) + } + return nil +} + +// BodyEntry is the per-block entry returned by /{fork}/bodies/... The Body's +// wire shape follows the codec fork, so this single type spans all forks. +type BodyEntry struct { + Available bool + Body *ExecutionPayloadBody +} + +func (e *BodyEntry) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { size := uint32(1 + 4) // bool + offset if fixed { return size @@ -55,19 +114,19 @@ func (e *BodyEntryAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { return size } -func (e *BodyEntryAmsterdam) DefineSSZ(c *ssz.Codec) { +func (e *BodyEntry) DefineSSZ(c *ssz.Codec) { ssz.DefineBool(c, &e.Available) ssz.DefineDynamicObjectOffset(c, &e.Body) ssz.DefineDynamicObjectContent(c, &e.Body) } -// BodiesResponseAmsterdam is the SSZ response of /amsterdam/bodies/... -type BodiesResponseAmsterdam struct { - Entries []*BodyEntryAmsterdam +// BodiesResponse is the SSZ response of /{fork}/bodies/... +type BodiesResponse struct { + Entries []*BodyEntry } -func (r *BodiesResponseAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { +func (r *BodiesResponse) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { size := uint32(4) if fixed { return size @@ -76,87 +135,7 @@ func (r *BodiesResponseAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { return size } -func (r *BodiesResponseAmsterdam) DefineSSZ(c *ssz.Codec) { - ssz.DefineSliceOfDynamicObjectsOffset(c, &r.Entries, MaxBodiesRequest) - ssz.DefineSliceOfDynamicObjectsContent(c, &r.Entries, MaxBodiesRequest) -} - -// BodyEntryCancun is the /cancun/bodies/... entry (Shanghai-shape body). -type BodyEntryCancun struct { - Available bool - Body *ExecutionPayloadBodyCancun -} - -func (e *BodyEntryCancun) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - size := uint32(1 + 4) - if fixed { - return size - } - size += ssz.SizeDynamicObject(siz, e.Body) - return size -} - -func (e *BodyEntryCancun) DefineSSZ(c *ssz.Codec) { - ssz.DefineBool(c, &e.Available) - ssz.DefineDynamicObjectOffset(c, &e.Body) - - ssz.DefineDynamicObjectContent(c, &e.Body) -} - -type BodiesResponseCancun struct { - Entries []*BodyEntryCancun -} - -func (r *BodiesResponseCancun) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - size := uint32(4) - if fixed { - return size - } - size += ssz.SizeSliceOfDynamicObjects(siz, r.Entries) - return size -} - -func (r *BodiesResponseCancun) DefineSSZ(c *ssz.Codec) { - ssz.DefineSliceOfDynamicObjectsOffset(c, &r.Entries, MaxBodiesRequest) - ssz.DefineSliceOfDynamicObjectsContent(c, &r.Entries, MaxBodiesRequest) -} - -// BodyEntryParis is the /paris/bodies/... entry (transactions only). -type BodyEntryParis struct { - Available bool - Body *ExecutionPayloadBodyParis -} - -func (e *BodyEntryParis) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - size := uint32(1 + 4) - if fixed { - return size - } - size += ssz.SizeDynamicObject(siz, e.Body) - return size -} - -func (e *BodyEntryParis) DefineSSZ(c *ssz.Codec) { - ssz.DefineBool(c, &e.Available) - ssz.DefineDynamicObjectOffset(c, &e.Body) - - ssz.DefineDynamicObjectContent(c, &e.Body) -} - -type BodiesResponseParis struct { - Entries []*BodyEntryParis -} - -func (r *BodiesResponseParis) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - size := uint32(4) - if fixed { - return size - } - size += ssz.SizeSliceOfDynamicObjects(siz, r.Entries) - return size -} - -func (r *BodiesResponseParis) DefineSSZ(c *ssz.Codec) { +func (r *BodiesResponse) DefineSSZ(c *ssz.Codec) { ssz.DefineSliceOfDynamicObjectsOffset(c, &r.Entries, MaxBodiesRequest) ssz.DefineSliceOfDynamicObjectsContent(c, &r.Entries, MaxBodiesRequest) } diff --git a/beacon/engine/ssz/bytecompat_test.go b/beacon/engine/ssz/bytecompat_test.go new file mode 100644 index 0000000000..8d71807364 --- /dev/null +++ b/beacon/engine/ssz/bytecompat_test.go @@ -0,0 +1,146 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// Byte-compatibility guard for the monolith refactor. The golden hex strings +// below were captured from the original per-fork structs (ExecutionPayload +// {Paris,Shanghai,Cancun,Amsterdam} etc.) before they were collapsed into the +// monolith types. Encoding the monolith with the same sample data on the +// corresponding fork MUST reproduce these bytes exactly — that is the proof +// the refactor is wire-compatible. + +package ssz + +import ( + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/holiman/uint256" + "github.com/karalabe/ssz" +) + +func encodeHexOnFork(t *testing.T, obj ssz.Object, fork ssz.Fork) string { + t.Helper() + buf := make([]byte, ssz.SizeOnFork(obj, fork)) + if err := ssz.EncodeToBytesOnFork(buf, obj, fork); err != nil { + t.Fatalf("encode: %v", err) + } + return fmt.Sprintf("%x", buf) +} + +// golden bytes captured from the pre-refactor per-fork structs. +const ( + goldenPayloadParis = "1100000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000033000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005500000000000000000000000000000000000000000000000000000000000000640000000000000080c3c90100000000085200000000000000f1536500000000fc01000000863ba1010000000000000000000000000000000000000000000000000000006600000000000000000000000000000000000000000000000000000000000000ff010000657874080000000a000000020304" + goldenPayloadShanghai = "1100000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000033000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005500000000000000000000000000000000000000000000000000000000000000640000000000000080c3c90100000000085200000000000000f15365000000000002000000863ba1010000000000000000000000000000000000000000000000000000006600000000000000000000000000000000000000000000000000000000000000030200000e020000657874080000000a0000000203040100000000000000020000000000000009000000000000000000000000000000000000000300000000000000" + goldenPayloadCancun = "1100000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000033000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005500000000000000000000000000000000000000000000000000000000000000640000000000000080c3c90100000000085200000000000000f15365000000001002000000863ba1010000000000000000000000000000000000000000000000000000006600000000000000000000000000000000000000000000000000000000000000130200001e02000000000200000000000000000000000000657874080000000a0000000203040100000000000000020000000000000009000000000000000000000000000000000000000300000000000000" + goldenPayloadAmsterdam = "1100000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000033000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005500000000000000000000000000000000000000000000000000000000000000640000000000000080c3c90100000000085200000000000000f15365000000001c02000000863ba10100000000000000000000000000000000000000000000000000000066000000000000000000000000000000000000000000000000000000000000001f0200002a0200000000020000000000000000000000000056020000c800000000000000657874080000000a0000000203040100000000000000020000000000000009000000000000000000000000000000000000000300000000000000deadbeef" + + goldenAttrsParis = "00f153650000000055000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000" + goldenAttrsShanghai = "00f153650000000055000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000400000000100000000000000020000000000000009000000000000000000000000000000000000000300000000000000" + goldenAttrsCancun = "00f1536500000000550000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000006000000077000000000000000000000000000000000000000000000000000000000000000100000000000000020000000000000009000000000000000000000000000000000000000300000000000000" + goldenAttrsAmsterdam = "00f1536500000000550000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000007000000077000000000000000000000000000000000000000000000000000000000000002a0000000000000080c3c901000000000100000000000000020000000000000009000000000000000000000000000000000000000300000000000000" + + goldenBodyParis = "04000000080000000a000000020304" + goldenBodyCancun = "0800000013000000080000000a0000000203040100000000000000020000000000000009000000000000000000000000000000000000000300000000000000" + goldenBodyAmsterdam = "0c0000001700000043000000080000000a0000000203040100000000000000020000000000000009000000000000000000000000000000000000000300000000000000deadbeef" +) + +// sample field values, shared so only fork-specific fields move the bytes. +var ( + gParentHash = common.Hash{0x11} + gFeeRecipient = common.Address{0x22} + gStateRoot = common.Hash{0x33} + gReceiptsRoot = common.Hash{0x44} + gPrevRandao = common.Hash{0x55} + gBlockHash = common.Hash{0x66} + gExtra = []byte("ext") + gTxs = [][]byte{{0x02, 0x03}, {0x04}} + gWds = []*Withdrawal{{Index: 1, ValidatorIndex: 2, Address: common.Address{0x09}, Amount: 3}} + gBaseFee = uint256.NewInt(7e9) + gBeaconRoot = common.Hash{0x77} + gBAL = []byte{0xde, 0xad, 0xbe, 0xef} +) + +// basePayload returns a monolith populated for the given fork (only the fork's +// active fields are set, matching what the converters do). +func basePayload(fork ssz.Fork) *ExecutionPayload { + p := &ExecutionPayload{ + ParentHash: gParentHash, FeeRecipient: gFeeRecipient, StateRoot: gStateRoot, + ReceiptsRoot: gReceiptsRoot, PrevRandao: gPrevRandao, BlockNumber: 100, + GasLimit: 30000000, GasUsed: 21000, Timestamp: 1700000000, ExtraData: gExtra, + BaseFeePerGas: gBaseFee, BlockHash: gBlockHash, Transactions: gTxs, + } + if fork >= ssz.ForkShapella { + p.Withdrawals = gWds + } + if fork >= ssz.ForkDencun { + blob, excess := uint64(131072), uint64(0) + p.BlobGasUsed, p.ExcessBlobGas = &blob, &excess + } + if fork >= forkAmsterdam { + slot := uint64(200) + p.BlockAccessList = gBAL + p.SlotNumber = &slot + } + return p +} + +func baseAttrs(fork ssz.Fork) *PayloadAttributes { + a := &PayloadAttributes{ + Timestamp: 1700000000, PrevRandao: gPrevRandao, SuggestedFeeRecipient: gFeeRecipient, + } + if fork >= ssz.ForkShapella { + a.Withdrawals = gWds + } + if fork >= ssz.ForkDencun { + root := gBeaconRoot + a.ParentBeaconBlockRoot = &root + } + if fork >= forkAmsterdam { + slot, tgl := uint64(42), uint64(30000000) + a.SlotNumber, a.TargetGasLimit = &slot, &tgl + } + return a +} + +func baseBody(fork ssz.Fork) *ExecutionPayloadBody { + b := &ExecutionPayloadBody{Transactions: gTxs} + if fork >= ssz.ForkShapella { + b.Withdrawals = gWds + } + if fork >= forkAmsterdam { + b.BlockAccessList = gBAL + } + return b +} + +func TestMonolithByteCompat(t *testing.T) { + cases := []struct { + name string + fork ssz.Fork + obj ssz.Object + want string + }{ + {"payload.paris", ssz.ForkParis, basePayload(ssz.ForkParis), goldenPayloadParis}, + {"payload.shanghai", ssz.ForkShapella, basePayload(ssz.ForkShapella), goldenPayloadShanghai}, + {"payload.cancun", ssz.ForkDencun, basePayload(ssz.ForkDencun), goldenPayloadCancun}, + {"payload.amsterdam", forkAmsterdam, basePayload(forkAmsterdam), goldenPayloadAmsterdam}, + + {"attrs.paris", ssz.ForkParis, baseAttrs(ssz.ForkParis), goldenAttrsParis}, + {"attrs.shanghai", ssz.ForkShapella, baseAttrs(ssz.ForkShapella), goldenAttrsShanghai}, + {"attrs.cancun", ssz.ForkDencun, baseAttrs(ssz.ForkDencun), goldenAttrsCancun}, + {"attrs.amsterdam", forkAmsterdam, baseAttrs(forkAmsterdam), goldenAttrsAmsterdam}, + + {"body.paris", ssz.ForkParis, baseBody(ssz.ForkParis), goldenBodyParis}, + {"body.cancun", ssz.ForkDencun, baseBody(ssz.ForkDencun), goldenBodyCancun}, + {"body.amsterdam", forkAmsterdam, baseBody(forkAmsterdam), goldenBodyAmsterdam}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := encodeHexOnFork(t, tc.obj, tc.fork) + if got != tc.want { + t.Errorf("byte mismatch for %s\nwant %s\n got %s", tc.name, tc.want, got) + } + }) + } +} diff --git a/beacon/engine/ssz/convert.go b/beacon/engine/ssz/convert.go index 70c67d9798..9b46bac469 100644 --- a/beacon/engine/ssz/convert.go +++ b/beacon/engine/ssz/convert.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/holiman/uint256" + "github.com/karalabe/ssz" ) // statusToEnum maps the JSON-RPC PayloadStatusV1.Status string to the SSZ @@ -127,46 +128,68 @@ func withdrawalsToTypes(ws []*Withdrawal) []*types.Withdrawal { return out } -// PayloadAttributesAmsterdamFromEngine converts a JSON-RPC engine.PayloadAttributes -// into its Amsterdam SSZ form. The caller is responsible for ensuring the -// attributes carry every Amsterdam-required field. -func PayloadAttributesAmsterdamFromEngine(a *engine.PayloadAttributes) *PayloadAttributesAmsterdam { - out := &PayloadAttributesAmsterdam{ +// PayloadAttributesFromEngine converts a JSON-RPC engine.PayloadAttributes into +// the monolithic SSZ form for the given fork. Only the fields the fork's wire +// shape carries are populated; the caller should pass the same fork to the +// codec. target_gas_limit has no representation in engine.PayloadAttributes +// today, so it is left nil for Amsterdam unless explicitly populated by the +// caller afterwards. +func PayloadAttributesFromEngine(a *engine.PayloadAttributes, fork ssz.Fork) *PayloadAttributes { + out := &PayloadAttributes{ Timestamp: a.Timestamp, PrevRandao: a.Random, SuggestedFeeRecipient: a.SuggestedFeeRecipient, - Withdrawals: withdrawalsFromTypes(a.Withdrawals), } - if a.BeaconRoot != nil { - out.ParentBeaconBlockRoot = *a.BeaconRoot + if fork >= ssz.ForkShapella { + out.Withdrawals = withdrawalsFromTypes(a.Withdrawals) } - if a.SlotNumber != nil { - out.SlotNumber = *a.SlotNumber + if fork >= ssz.ForkDencun { + root := common.Hash{} + if a.BeaconRoot != nil { + root = *a.BeaconRoot + } + out.ParentBeaconBlockRoot = &root + } + if fork >= forkAmsterdam { + slot := uint64(0) + if a.SlotNumber != nil { + slot = *a.SlotNumber + } + out.SlotNumber = &slot + // target_gas_limit not carried by engine.PayloadAttributes; default 0. + tgl := uint64(0) + out.TargetGasLimit = &tgl } return out } -// PayloadAttributesAmsterdamToEngine is the inverse of the From helper. The -// target_gas_limit field has no representation in engine.PayloadAttributes -// today and is dropped on the way down; the caller can fish it out of the SSZ -// struct directly. -func PayloadAttributesAmsterdamToEngine(a *PayloadAttributesAmsterdam) *engine.PayloadAttributes { - root := a.ParentBeaconBlockRoot - slot := a.SlotNumber - return &engine.PayloadAttributes{ +// PayloadAttributesToEngine is the inverse of PayloadAttributesFromEngine. The +// target_gas_limit field has no representation in engine.PayloadAttributes and +// is dropped; the caller can read it from the SSZ struct directly. +func PayloadAttributesToEngine(a *PayloadAttributes) *engine.PayloadAttributes { + out := &engine.PayloadAttributes{ Timestamp: a.Timestamp, Random: a.PrevRandao, SuggestedFeeRecipient: a.SuggestedFeeRecipient, Withdrawals: withdrawalsToTypes(a.Withdrawals), - BeaconRoot: &root, - SlotNumber: &slot, } + if a.ParentBeaconBlockRoot != nil { + root := *a.ParentBeaconBlockRoot + out.BeaconRoot = &root + } + if a.SlotNumber != nil { + slot := *a.SlotNumber + out.SlotNumber = &slot + } + return out } -// ExecutionPayloadAmsterdamFromEngine converts an ExecutableData into the SSZ -// Amsterdam payload shape. The block_access_list and slot_number fields are -// expected to be present; missing values yield empty/zero defaults. -func ExecutionPayloadAmsterdamFromEngine(d *engine.ExecutableData) *ExecutionPayloadAmsterdam { +// ExecutionPayloadFromEngine converts an ExecutableData into the monolithic SSZ +// payload for the given fork. Only the fork's active fields are populated so +// the codec (driven by the same fork) and Validate(fork) stay consistent. +// BlockAccessList is not yet wired through ExecutableData; the caller sets it +// separately for Amsterdam. +func ExecutionPayloadFromEngine(d *engine.ExecutableData, fork ssz.Fork) *ExecutionPayload { var bloom [256]byte copy(bloom[:], d.LogsBloom) var fee *uint256.Int @@ -174,7 +197,7 @@ func ExecutionPayloadAmsterdamFromEngine(d *engine.ExecutableData) *ExecutionPay fee = new(uint256.Int) fee.SetFromBig(d.BaseFeePerGas) } - out := &ExecutionPayloadAmsterdam{ + out := &ExecutionPayload{ ParentHash: d.ParentHash, FeeRecipient: d.FeeRecipient, StateRoot: d.StateRoot, @@ -189,48 +212,66 @@ func ExecutionPayloadAmsterdamFromEngine(d *engine.ExecutableData) *ExecutionPay BaseFeePerGas: fee, BlockHash: d.BlockHash, Transactions: d.Transactions, - Withdrawals: withdrawalsFromTypes(d.Withdrawals), } - if d.BlobGasUsed != nil { - out.BlobGasUsed = *d.BlobGasUsed + if fork >= ssz.ForkShapella { + out.Withdrawals = withdrawalsFromTypes(d.Withdrawals) } - if d.ExcessBlobGas != nil { - out.ExcessBlobGas = *d.ExcessBlobGas + if fork >= ssz.ForkDencun { + blobGas := uint64(0) + if d.BlobGasUsed != nil { + blobGas = *d.BlobGasUsed + } + excess := uint64(0) + if d.ExcessBlobGas != nil { + excess = *d.ExcessBlobGas + } + out.BlobGasUsed = &blobGas + out.ExcessBlobGas = &excess } - if d.SlotNumber != nil { - out.SlotNumber = *d.SlotNumber + if fork >= forkAmsterdam { + slot := uint64(0) + if d.SlotNumber != nil { + slot = *d.SlotNumber + } + out.SlotNumber = &slot } - // BlockAccessList is not yet wired through ExecutableData; the caller - // passes it as a separate field on the envelope when constructing the - // final wire form. return out } -// ExecutionPayloadAmsterdamToEngine is the inverse helper. The caller is -// responsible for the BAL payload that ExecutableData doesn't yet carry. -func ExecutionPayloadAmsterdamToEngine(p *ExecutionPayloadAmsterdam) *engine.ExecutableData { +// ExecutionPayloadToEngine is the inverse helper. The caller is responsible for +// the BAL payload that ExecutableData doesn't yet carry. +func ExecutionPayloadToEngine(p *ExecutionPayload) *engine.ExecutableData { bloom := append([]byte(nil), p.LogsBloom[:]...) out := &engine.ExecutableData{ - ParentHash: p.ParentHash, - FeeRecipient: p.FeeRecipient, - StateRoot: p.StateRoot, - ReceiptsRoot: p.ReceiptsRoot, - LogsBloom: bloom, - Random: p.PrevRandao, - Number: p.BlockNumber, - GasLimit: p.GasLimit, - GasUsed: p.GasUsed, - Timestamp: p.Timestamp, - ExtraData: append([]byte(nil), p.ExtraData...), - BlockHash: p.BlockHash, - Transactions: p.Transactions, - Withdrawals: withdrawalsToTypes(p.Withdrawals), - BlobGasUsed: &p.BlobGasUsed, - ExcessBlobGas: &p.ExcessBlobGas, - SlotNumber: &p.SlotNumber, + ParentHash: p.ParentHash, + FeeRecipient: p.FeeRecipient, + StateRoot: p.StateRoot, + ReceiptsRoot: p.ReceiptsRoot, + LogsBloom: bloom, + Random: p.PrevRandao, + Number: p.BlockNumber, + GasLimit: p.GasLimit, + GasUsed: p.GasUsed, + Timestamp: p.Timestamp, + ExtraData: append([]byte(nil), p.ExtraData...), + BlockHash: p.BlockHash, + Transactions: p.Transactions, + Withdrawals: withdrawalsToTypes(p.Withdrawals), } if p.BaseFeePerGas != nil { out.BaseFeePerGas = p.BaseFeePerGas.ToBig() } + if p.BlobGasUsed != nil { + blobGas := *p.BlobGasUsed + out.BlobGasUsed = &blobGas + } + if p.ExcessBlobGas != nil { + excess := *p.ExcessBlobGas + out.ExcessBlobGas = &excess + } + if p.SlotNumber != nil { + slot := *p.SlotNumber + out.SlotNumber = &slot + } return out } diff --git a/beacon/engine/ssz/envelopes.go b/beacon/engine/ssz/envelopes.go index d69641e555..944cd7e328 100644 --- a/beacon/engine/ssz/envelopes.go +++ b/beacon/engine/ssz/envelopes.go @@ -25,7 +25,7 @@ import ( // ExecutionPayloadEnvelopeAmsterdam is the request body of // POST /amsterdam/payloads. type ExecutionPayloadEnvelopeAmsterdam struct { - Payload *ExecutionPayloadAmsterdam + Payload *ExecutionPayload ParentBeaconBlockRoot common.Hash ExecutionRequests [][]byte } @@ -82,7 +82,7 @@ func (b *BlobsBundleV2) DefineSSZ(c *ssz.Codec) { // BuiltPayloadAmsterdam is the response of GET /amsterdam/payloads/{id}. type BuiltPayloadAmsterdam struct { - Payload *ExecutionPayloadAmsterdam + Payload *ExecutionPayload BlockValue *uint256.Int BlobsBundle *BlobsBundleV2 ExecutionRequests [][]byte @@ -120,7 +120,7 @@ func (p *BuiltPayloadAmsterdam) DefineSSZ(c *ssz.Codec) { // length 0 or 1 of the 16-byte Bitvector wrapper. type ForkchoiceUpdateAmsterdam struct { ForkchoiceState *ForkchoiceState - PayloadAttributes []*PayloadAttributesAmsterdam + PayloadAttributes []*PayloadAttributes CustodyColumns []*Bitvector128 } diff --git a/beacon/engine/ssz/forkmap.go b/beacon/engine/ssz/forkmap.go new file mode 100644 index 0000000000..d66901af00 --- /dev/null +++ b/beacon/engine/ssz/forkmap.go @@ -0,0 +1,60 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ssz + +import ( + "github.com/ethereum/go-ethereum/params/forks" + "github.com/karalabe/ssz" +) + +// karalabe/ssz's Fork enum currently stops at Pectra (Prague). The monolith +// types below gate fields with ForkFilter{Added,Removed}, and the codec only +// performs ordered comparisons (`fork < Added`, `fork >= Removed`) against +// Codec.fork. Any strictly-monotonic values placed after ForkPectra therefore +// behave correctly. We define local aliases for the forks the library doesn't +// name yet. +// +// IMPORTANT: these are only meaningful relative to one another and to the +// library's own enum; never persist them. If a future karalabe/ssz release +// adds Osaka/Amsterdam (or new forks between Pectra and these), revisit this +// table so the ordering stays gap-consistent with the upstream enum. +const ( + forkOsaka = ssz.ForkPectra + 1 + forkAmsterdam = ssz.ForkPectra + 2 +) + +// ForkFor maps a geth params/forks.Fork onto the karalabe/ssz Fork value +// the codec multiplexes on. The bool is false for forks that predate the +// Engine API (and thus have no SSZ wire representation here). +func ForkFor(f forks.Fork) (ssz.Fork, bool) { + switch f { + case forks.Paris: + return ssz.ForkParis, true + case forks.Shanghai: + return ssz.ForkShapella, true // EL Shanghai == CL Shapella + case forks.Cancun: + return ssz.ForkDencun, true // EL Cancun == CL Dencun + case forks.Prague: + return ssz.ForkPectra, true // EL Prague == CL Pectra + case forks.Osaka: + return forkOsaka, true + case forks.Amsterdam: + return forkAmsterdam, true + default: + return ssz.ForkUnknown, false + } +} diff --git a/beacon/engine/ssz/payload.go b/beacon/engine/ssz/payload.go new file mode 100644 index 0000000000..751d648252 --- /dev/null +++ b/beacon/engine/ssz/payload.go @@ -0,0 +1,251 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ssz + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/holiman/uint256" + "github.com/karalabe/ssz" +) + +// Fork-boundary filters. Fields gated by these only participate in size, +// encode, decode and hash when Codec.fork is in range — see forkmap.go for how +// the geth fork maps onto ssz.Fork. Grouping the filters here (rather than one +// flag per field) keeps the "field-group" structure visible: each filter is a +// group boundary, and DefineSSZ applies it to every field the group owns. +var ( + // withdrawals group, introduced in Shanghai (Shapella). + fromShanghai = ssz.ForkFilter{Added: ssz.ForkShapella} + // blob-gas group, introduced in Cancun (Dencun). + fromCancun = ssz.ForkFilter{Added: ssz.ForkDencun} + // beacon-root attribute, introduced in Cancun (Dencun). + // (shares the Cancun boundary but kept named for clarity at call sites) + // bal + slot group, introduced in Amsterdam. + fromAmsterdam = ssz.ForkFilter{Added: forkAmsterdam} +) + +// ExecutionPayload is the monolithic execution payload spanning every fork from +// Paris onward. Fork-specific fields are gated with ...OnFork; the in-memory +// struct is a superset and the fork passed to the codec selects the wire shape. +// +// Field groups (see the filter vars above): +// - base (Paris): ParentHash … Transactions +// - withdrawals (Shanghai): Withdrawals +// - blobGas (Cancun): BlobGasUsed, ExcessBlobGas +// - bal (Amsterdam): BlockAccessList, SlotNumber +// +// Gated scalars are pointers so the codec can distinguish "absent for this +// fork" (nil) from "present and zero"; Validate(fork) enforces that invariant. +type ExecutionPayload struct { + // base group (Paris) + ParentHash common.Hash + FeeRecipient common.Address + StateRoot common.Hash + ReceiptsRoot common.Hash + LogsBloom [256]byte + PrevRandao common.Hash + BlockNumber uint64 + GasLimit uint64 + GasUsed uint64 + Timestamp uint64 + ExtraData []byte + BaseFeePerGas *uint256.Int + BlockHash common.Hash + Transactions [][]byte + + // withdrawals group (Shanghai) + Withdrawals []*Withdrawal + + // blobGas group (Cancun) + BlobGasUsed *uint64 + ExcessBlobGas *uint64 + + // bal group (Amsterdam) + BlockAccessList []byte + SlotNumber *uint64 +} + +func (p *ExecutionPayload) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { + // Fixed portion is fork-dependent; let the codec primitives account for it + // via the active fork rather than hand-summing magic byte constants. + var size uint32 + // base fixed: 5 hashes(32) + addr(20) + bloom(256) + 4 u64s + uint256(32) + // + 2 offsets(extra, txs)(4) + size += 5*32 + 20 + 256 + 4*8 + 32 + 2*4 + if siz.Fork() >= ssz.ForkShapella { + size += 4 // withdrawals offset + } + if siz.Fork() >= ssz.ForkDencun { + size += 2 * 8 // blob_gas_used, excess_blob_gas + } + if siz.Fork() >= forkAmsterdam { + size += 4 + 8 // block_access_list offset, slot_number + } + if fixed { + return size + } + size += ssz.SizeDynamicBytes(siz, p.ExtraData) + size += ssz.SizeSliceOfDynamicBytes(siz, p.Transactions) + if siz.Fork() >= ssz.ForkShapella { + size += ssz.SizeSliceOfStaticObjects(siz, p.Withdrawals) + } + if siz.Fork() >= forkAmsterdam { + size += ssz.SizeDynamicBytes(siz, p.BlockAccessList) + } + return size +} + +func (p *ExecutionPayload) DefineSSZ(c *ssz.Codec) { + // --- offset phase --- + ssz.DefineStaticBytes(c, &p.ParentHash) + ssz.DefineStaticBytes(c, &p.FeeRecipient) + ssz.DefineStaticBytes(c, &p.StateRoot) + ssz.DefineStaticBytes(c, &p.ReceiptsRoot) + ssz.DefineStaticBytes(c, &p.LogsBloom) + ssz.DefineStaticBytes(c, &p.PrevRandao) + ssz.DefineUint64(c, &p.BlockNumber) + ssz.DefineUint64(c, &p.GasLimit) + ssz.DefineUint64(c, &p.GasUsed) + ssz.DefineUint64(c, &p.Timestamp) + ssz.DefineDynamicBytesOffset(c, &p.ExtraData, MaxExtraDataBytes) + ssz.DefineUint256(c, &p.BaseFeePerGas) + ssz.DefineStaticBytes(c, &p.BlockHash) + ssz.DefineSliceOfDynamicBytesOffset(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx) + ssz.DefineSliceOfStaticObjectsOffsetOnFork(c, &p.Withdrawals, MaxWithdrawalsPerPayload, fromShanghai) + ssz.DefineUint64PointerOnFork(c, &p.BlobGasUsed, fromCancun) + ssz.DefineUint64PointerOnFork(c, &p.ExcessBlobGas, fromCancun) + ssz.DefineDynamicBytesOffsetOnFork(c, &p.BlockAccessList, MaxBalBytes, fromAmsterdam) + ssz.DefineUint64PointerOnFork(c, &p.SlotNumber, fromAmsterdam) + + // --- content phase --- + ssz.DefineDynamicBytesContent(c, &p.ExtraData, MaxExtraDataBytes) + ssz.DefineSliceOfDynamicBytesContent(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx) + ssz.DefineSliceOfStaticObjectsContentOnFork(c, &p.Withdrawals, MaxWithdrawalsPerPayload, fromShanghai) + ssz.DefineDynamicBytesContentOnFork(c, &p.BlockAccessList, MaxBalBytes, fromAmsterdam) +} + +// Validate checks that the in-memory field set matches what the given fork's +// wire shape carries: gated scalars must be present iff active, and absent +// fields must be empty so the codec doesn't silently drop populated data. +func (p *ExecutionPayload) Validate(fork ssz.Fork) error { + // withdrawals (Shanghai+) + if fork < ssz.ForkShapella && len(p.Withdrawals) > 0 { + return fmt.Errorf("withdrawals set but fork %d predates Shanghai", fork) + } + // blobGas (Cancun+) + if fork >= ssz.ForkDencun { + if p.BlobGasUsed == nil || p.ExcessBlobGas == nil { + return fmt.Errorf("blob gas fields required at fork %d", fork) + } + } else if p.BlobGasUsed != nil || p.ExcessBlobGas != nil { + return fmt.Errorf("blob gas fields set but fork %d predates Cancun", fork) + } + // bal + slot (Amsterdam+) + if fork >= forkAmsterdam { + if p.SlotNumber == nil { + return fmt.Errorf("slot_number required at fork %d", fork) + } + } else { + if len(p.BlockAccessList) > 0 { + return fmt.Errorf("block_access_list set but fork %d predates Amsterdam", fork) + } + if p.SlotNumber != nil { + return fmt.Errorf("slot_number set but fork %d predates Amsterdam", fork) + } + } + return nil +} + +// PayloadAttributes is the monolithic build-attributes container spanning every +// fork from Paris onward. +// +// Field groups: +// - base (Paris): Timestamp, PrevRandao, SuggestedFeeRecipient +// - withdrawals (Shanghai): Withdrawals +// - beaconRoot (Cancun): ParentBeaconBlockRoot +// - amsterdam (Amsterdam): SlotNumber, TargetGasLimit +type PayloadAttributes struct { + Timestamp uint64 + PrevRandao common.Hash + SuggestedFeeRecipient common.Address + + Withdrawals []*Withdrawal // Shanghai+ + + ParentBeaconBlockRoot *common.Hash // Cancun+ + + SlotNumber *uint64 // Amsterdam+ + TargetGasLimit *uint64 // Amsterdam+ +} + +func (a *PayloadAttributes) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { + // base fixed: timestamp(8) + randao(32) + fee_recipient(20) + var size uint32 = 8 + 32 + 20 + if siz.Fork() >= ssz.ForkShapella { + size += 4 // withdrawals offset + } + if siz.Fork() >= ssz.ForkDencun { + size += 32 // parent_beacon_block_root + } + if siz.Fork() >= forkAmsterdam { + size += 8 + 8 // slot_number, target_gas_limit + } + if fixed { + return size + } + if siz.Fork() >= ssz.ForkShapella { + size += ssz.SizeSliceOfStaticObjects(siz, a.Withdrawals) + } + return size +} + +func (a *PayloadAttributes) DefineSSZ(c *ssz.Codec) { + // offset phase + ssz.DefineUint64(c, &a.Timestamp) + ssz.DefineStaticBytes(c, &a.PrevRandao) + ssz.DefineStaticBytes(c, &a.SuggestedFeeRecipient) + ssz.DefineSliceOfStaticObjectsOffsetOnFork(c, &a.Withdrawals, MaxWithdrawalsPerPayload, fromShanghai) + ssz.DefineStaticBytesPointerOnFork(c, &a.ParentBeaconBlockRoot, fromCancun) + ssz.DefineUint64PointerOnFork(c, &a.SlotNumber, fromAmsterdam) + ssz.DefineUint64PointerOnFork(c, &a.TargetGasLimit, fromAmsterdam) + + // content phase + ssz.DefineSliceOfStaticObjectsContentOnFork(c, &a.Withdrawals, MaxWithdrawalsPerPayload, fromShanghai) +} + +// Validate enforces presence/absence of gated fields for the given fork. +func (a *PayloadAttributes) Validate(fork ssz.Fork) error { + if fork < ssz.ForkShapella && len(a.Withdrawals) > 0 { + return fmt.Errorf("withdrawals set but fork %d predates Shanghai", fork) + } + if fork >= ssz.ForkDencun { + if a.ParentBeaconBlockRoot == nil { + return fmt.Errorf("parent_beacon_block_root required at fork %d", fork) + } + } else if a.ParentBeaconBlockRoot != nil { + return fmt.Errorf("parent_beacon_block_root set but fork %d predates Cancun", fork) + } + if fork >= forkAmsterdam { + if a.SlotNumber == nil || a.TargetGasLimit == nil { + return fmt.Errorf("slot_number and target_gas_limit required at fork %d", fork) + } + } else if a.SlotNumber != nil || a.TargetGasLimit != nil { + return fmt.Errorf("amsterdam attributes set but fork %d predates Amsterdam", fork) + } + return nil +} diff --git a/beacon/engine/ssz/payload_amsterdam.go b/beacon/engine/ssz/payload_amsterdam.go deleted file mode 100644 index a21dde94d7..0000000000 --- a/beacon/engine/ssz/payload_amsterdam.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2026 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package ssz - -import ( - "github.com/ethereum/go-ethereum/common" - "github.com/holiman/uint256" - "github.com/karalabe/ssz" -) - -// ExecutionPayloadAmsterdam carries the Amsterdam-shaped payload over the wire. -type ExecutionPayloadAmsterdam struct { - ParentHash common.Hash - FeeRecipient common.Address - StateRoot common.Hash - ReceiptsRoot common.Hash - LogsBloom [256]byte - PrevRandao common.Hash - BlockNumber uint64 - GasLimit uint64 - GasUsed uint64 - Timestamp uint64 - ExtraData []byte - BaseFeePerGas *uint256.Int - BlockHash common.Hash - Transactions [][]byte - Withdrawals []*Withdrawal - BlobGasUsed uint64 - ExcessBlobGas uint64 - BlockAccessList []byte - SlotNumber uint64 -} - -func (p *ExecutionPayloadAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - // 5 hashes(32) + addr(20) + bloom(256) + 4 u64s + uint256(32) - // + 4 offsets(4) + 2 u64s (blob gas) + u64 (slot) = 540 - size := uint32(5*32 + 20 + 256 + 4*8 + 32 + 4*4 + 2*8 + 8) - if fixed { - return size - } - size += ssz.SizeDynamicBytes(siz, p.ExtraData) - size += ssz.SizeSliceOfDynamicBytes(siz, p.Transactions) - size += ssz.SizeSliceOfStaticObjects(siz, p.Withdrawals) - size += ssz.SizeDynamicBytes(siz, p.BlockAccessList) - return size -} - -func (p *ExecutionPayloadAmsterdam) DefineSSZ(c *ssz.Codec) { - ssz.DefineStaticBytes(c, &p.ParentHash) - ssz.DefineStaticBytes(c, &p.FeeRecipient) - ssz.DefineStaticBytes(c, &p.StateRoot) - ssz.DefineStaticBytes(c, &p.ReceiptsRoot) - ssz.DefineStaticBytes(c, &p.LogsBloom) - ssz.DefineStaticBytes(c, &p.PrevRandao) - ssz.DefineUint64(c, &p.BlockNumber) - ssz.DefineUint64(c, &p.GasLimit) - ssz.DefineUint64(c, &p.GasUsed) - ssz.DefineUint64(c, &p.Timestamp) - ssz.DefineDynamicBytesOffset(c, &p.ExtraData, MaxExtraDataBytes) - ssz.DefineUint256(c, &p.BaseFeePerGas) - ssz.DefineStaticBytes(c, &p.BlockHash) - ssz.DefineSliceOfDynamicBytesOffset(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - ssz.DefineSliceOfStaticObjectsOffset(c, &p.Withdrawals, MaxWithdrawalsPerPayload) - ssz.DefineUint64(c, &p.BlobGasUsed) - ssz.DefineUint64(c, &p.ExcessBlobGas) - ssz.DefineDynamicBytesOffset(c, &p.BlockAccessList, MaxBalBytes) - ssz.DefineUint64(c, &p.SlotNumber) - - ssz.DefineDynamicBytesContent(c, &p.ExtraData, MaxExtraDataBytes) - ssz.DefineSliceOfDynamicBytesContent(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - ssz.DefineSliceOfStaticObjectsContent(c, &p.Withdrawals, MaxWithdrawalsPerPayload) - ssz.DefineDynamicBytesContent(c, &p.BlockAccessList, MaxBalBytes) -} - -// PayloadAttributesAmsterdam carries Amsterdam build attributes. -type PayloadAttributesAmsterdam struct { - Timestamp uint64 - PrevRandao common.Hash - SuggestedFeeRecipient common.Address - Withdrawals []*Withdrawal - ParentBeaconBlockRoot common.Hash - SlotNumber uint64 - TargetGasLimit uint64 -} - -func (a *PayloadAttributesAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - // timestamp(8) + randao(32) + fee_recipient(20) + offset(4) - // + parent_beacon_block_root(32) + slot(8) + target_gas(8) = 112 - size := uint32(112) - if fixed { - return size - } - size += ssz.SizeSliceOfStaticObjects(siz, a.Withdrawals) - return size -} - -func (a *PayloadAttributesAmsterdam) DefineSSZ(c *ssz.Codec) { - ssz.DefineUint64(c, &a.Timestamp) - ssz.DefineStaticBytes(c, &a.PrevRandao) - ssz.DefineStaticBytes(c, &a.SuggestedFeeRecipient) - ssz.DefineSliceOfStaticObjectsOffset(c, &a.Withdrawals, MaxWithdrawalsPerPayload) - ssz.DefineStaticBytes(c, &a.ParentBeaconBlockRoot) - ssz.DefineUint64(c, &a.SlotNumber) - ssz.DefineUint64(c, &a.TargetGasLimit) - - ssz.DefineSliceOfStaticObjectsContent(c, &a.Withdrawals, MaxWithdrawalsPerPayload) -} - -// ExecutionPayloadBodyAmsterdam mirrors the /amsterdam/bodies response shape. -type ExecutionPayloadBodyAmsterdam struct { - Transactions [][]byte - Withdrawals []*Withdrawal - BlockAccessList []byte -} - -func (b *ExecutionPayloadBodyAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - // 3 offsets - size := uint32(12) - if fixed { - return size - } - size += ssz.SizeSliceOfDynamicBytes(siz, b.Transactions) - size += ssz.SizeSliceOfStaticObjects(siz, b.Withdrawals) - size += ssz.SizeDynamicBytes(siz, b.BlockAccessList) - return size -} - -func (b *ExecutionPayloadBodyAmsterdam) DefineSSZ(c *ssz.Codec) { - ssz.DefineSliceOfDynamicBytesOffset(c, &b.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - ssz.DefineSliceOfStaticObjectsOffset(c, &b.Withdrawals, MaxWithdrawalsPerPayload) - ssz.DefineDynamicBytesOffset(c, &b.BlockAccessList, MaxBalBytes) - - ssz.DefineSliceOfDynamicBytesContent(c, &b.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - ssz.DefineSliceOfStaticObjectsContent(c, &b.Withdrawals, MaxWithdrawalsPerPayload) - ssz.DefineDynamicBytesContent(c, &b.BlockAccessList, MaxBalBytes) -} diff --git a/beacon/engine/ssz/payload_forks.go b/beacon/engine/ssz/payload_forks.go deleted file mode 100644 index 9d48d8706d..0000000000 --- a/beacon/engine/ssz/payload_forks.go +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright 2026 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package ssz - -import ( - "github.com/ethereum/go-ethereum/common" - "github.com/holiman/uint256" - "github.com/karalabe/ssz" -) - -// ExecutionPayloadCancun is the Cancun/Prague/Osaka payload shape. -// Prague and Osaka don't change the inner payload — execution_requests -// and the BlobsBundle revision change at the envelope level, not here. -type ExecutionPayloadCancun struct { - ParentHash common.Hash - FeeRecipient common.Address - StateRoot common.Hash - ReceiptsRoot common.Hash - LogsBloom [256]byte - PrevRandao common.Hash - BlockNumber uint64 - GasLimit uint64 - GasUsed uint64 - Timestamp uint64 - ExtraData []byte - BaseFeePerGas *uint256.Int - BlockHash common.Hash - Transactions [][]byte - Withdrawals []*Withdrawal - BlobGasUsed uint64 - ExcessBlobGas uint64 -} - -func (p *ExecutionPayloadCancun) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - // 5 hashes(32) + addr(20) + bloom(256) + 4 u64s + uint256(32) - // + 3 offsets(4) + 2 u64s (blob gas) = 528 - size := uint32(5*32 + 20 + 256 + 4*8 + 32 + 3*4 + 2*8) - if fixed { - return size - } - size += ssz.SizeDynamicBytes(siz, p.ExtraData) - size += ssz.SizeSliceOfDynamicBytes(siz, p.Transactions) - size += ssz.SizeSliceOfStaticObjects(siz, p.Withdrawals) - return size -} - -func (p *ExecutionPayloadCancun) DefineSSZ(c *ssz.Codec) { - ssz.DefineStaticBytes(c, &p.ParentHash) - ssz.DefineStaticBytes(c, &p.FeeRecipient) - ssz.DefineStaticBytes(c, &p.StateRoot) - ssz.DefineStaticBytes(c, &p.ReceiptsRoot) - ssz.DefineStaticBytes(c, &p.LogsBloom) - ssz.DefineStaticBytes(c, &p.PrevRandao) - ssz.DefineUint64(c, &p.BlockNumber) - ssz.DefineUint64(c, &p.GasLimit) - ssz.DefineUint64(c, &p.GasUsed) - ssz.DefineUint64(c, &p.Timestamp) - ssz.DefineDynamicBytesOffset(c, &p.ExtraData, MaxExtraDataBytes) - ssz.DefineUint256(c, &p.BaseFeePerGas) - ssz.DefineStaticBytes(c, &p.BlockHash) - ssz.DefineSliceOfDynamicBytesOffset(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - ssz.DefineSliceOfStaticObjectsOffset(c, &p.Withdrawals, MaxWithdrawalsPerPayload) - ssz.DefineUint64(c, &p.BlobGasUsed) - ssz.DefineUint64(c, &p.ExcessBlobGas) - - ssz.DefineDynamicBytesContent(c, &p.ExtraData, MaxExtraDataBytes) - ssz.DefineSliceOfDynamicBytesContent(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - ssz.DefineSliceOfStaticObjectsContent(c, &p.Withdrawals, MaxWithdrawalsPerPayload) -} - -// ExecutionPayloadShanghai is the Shanghai payload shape (Cancun minus blob gas). -type ExecutionPayloadShanghai struct { - ParentHash common.Hash - FeeRecipient common.Address - StateRoot common.Hash - ReceiptsRoot common.Hash - LogsBloom [256]byte - PrevRandao common.Hash - BlockNumber uint64 - GasLimit uint64 - GasUsed uint64 - Timestamp uint64 - ExtraData []byte - BaseFeePerGas *uint256.Int - BlockHash common.Hash - Transactions [][]byte - Withdrawals []*Withdrawal -} - -func (p *ExecutionPayloadShanghai) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - // 5 hashes(32) + addr(20) + bloom(256) + 4 u64s + uint256(32) + 3 offsets(4) = 512 - size := uint32(5*32 + 20 + 256 + 4*8 + 32 + 3*4) - if fixed { - return size - } - size += ssz.SizeDynamicBytes(siz, p.ExtraData) - size += ssz.SizeSliceOfDynamicBytes(siz, p.Transactions) - size += ssz.SizeSliceOfStaticObjects(siz, p.Withdrawals) - return size -} - -func (p *ExecutionPayloadShanghai) DefineSSZ(c *ssz.Codec) { - ssz.DefineStaticBytes(c, &p.ParentHash) - ssz.DefineStaticBytes(c, &p.FeeRecipient) - ssz.DefineStaticBytes(c, &p.StateRoot) - ssz.DefineStaticBytes(c, &p.ReceiptsRoot) - ssz.DefineStaticBytes(c, &p.LogsBloom) - ssz.DefineStaticBytes(c, &p.PrevRandao) - ssz.DefineUint64(c, &p.BlockNumber) - ssz.DefineUint64(c, &p.GasLimit) - ssz.DefineUint64(c, &p.GasUsed) - ssz.DefineUint64(c, &p.Timestamp) - ssz.DefineDynamicBytesOffset(c, &p.ExtraData, MaxExtraDataBytes) - ssz.DefineUint256(c, &p.BaseFeePerGas) - ssz.DefineStaticBytes(c, &p.BlockHash) - ssz.DefineSliceOfDynamicBytesOffset(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - ssz.DefineSliceOfStaticObjectsOffset(c, &p.Withdrawals, MaxWithdrawalsPerPayload) - - ssz.DefineDynamicBytesContent(c, &p.ExtraData, MaxExtraDataBytes) - ssz.DefineSliceOfDynamicBytesContent(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - ssz.DefineSliceOfStaticObjectsContent(c, &p.Withdrawals, MaxWithdrawalsPerPayload) -} - -// ExecutionPayloadParis is the original Bellatrix/Paris payload shape. -type ExecutionPayloadParis struct { - ParentHash common.Hash - FeeRecipient common.Address - StateRoot common.Hash - ReceiptsRoot common.Hash - LogsBloom [256]byte - PrevRandao common.Hash - BlockNumber uint64 - GasLimit uint64 - GasUsed uint64 - Timestamp uint64 - ExtraData []byte - BaseFeePerGas *uint256.Int - BlockHash common.Hash - Transactions [][]byte -} - -func (p *ExecutionPayloadParis) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - // 5 hashes(32) + addr(20) + bloom(256) + 4 u64s + uint256(32) + 2 offsets(4) = 508 - size := uint32(5*32 + 20 + 256 + 4*8 + 32 + 2*4) - if fixed { - return size - } - size += ssz.SizeDynamicBytes(siz, p.ExtraData) - size += ssz.SizeSliceOfDynamicBytes(siz, p.Transactions) - return size -} - -func (p *ExecutionPayloadParis) DefineSSZ(c *ssz.Codec) { - ssz.DefineStaticBytes(c, &p.ParentHash) - ssz.DefineStaticBytes(c, &p.FeeRecipient) - ssz.DefineStaticBytes(c, &p.StateRoot) - ssz.DefineStaticBytes(c, &p.ReceiptsRoot) - ssz.DefineStaticBytes(c, &p.LogsBloom) - ssz.DefineStaticBytes(c, &p.PrevRandao) - ssz.DefineUint64(c, &p.BlockNumber) - ssz.DefineUint64(c, &p.GasLimit) - ssz.DefineUint64(c, &p.GasUsed) - ssz.DefineUint64(c, &p.Timestamp) - ssz.DefineDynamicBytesOffset(c, &p.ExtraData, MaxExtraDataBytes) - ssz.DefineUint256(c, &p.BaseFeePerGas) - ssz.DefineStaticBytes(c, &p.BlockHash) - ssz.DefineSliceOfDynamicBytesOffset(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx) - - ssz.DefineDynamicBytesContent(c, &p.ExtraData, MaxExtraDataBytes) - ssz.DefineSliceOfDynamicBytesContent(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx) -} diff --git a/beacon/engine/ssz/roundtrip_test.go b/beacon/engine/ssz/roundtrip_test.go index 25feb1a277..2e5afa23b8 100644 --- a/beacon/engine/ssz/roundtrip_test.go +++ b/beacon/engine/ssz/roundtrip_test.go @@ -77,8 +77,30 @@ func TestPayloadStatusRoundtrip(t *testing.T) { encDec(t, &PayloadStatus{Status: StatusSyncing}, func() *PayloadStatus { return new(PayloadStatus) }) } +// encDecOnFork is the fork-aware sibling of encDec for monolith types. +func encDecOnFork[T ssz.Object](t *testing.T, obj T, fork ssz.Fork, newEmpty func() T) { + t.Helper() + size := ssz.SizeOnFork(obj, fork) + buf := make([]byte, size) + if err := ssz.EncodeToBytesOnFork(buf, obj, fork); err != nil { + t.Fatalf("encode: %v", err) + } + got := newEmpty() + if err := ssz.DecodeFromBytesOnFork(buf, got, fork); err != nil { + t.Fatalf("decode: %v", err) + } + rebuf := make([]byte, ssz.SizeOnFork(got, fork)) + if err := ssz.EncodeToBytesOnFork(rebuf, got, fork); err != nil { + t.Fatalf("re-encode: %v", err) + } + if !reflect.DeepEqual(buf, rebuf) { + t.Fatalf("round-trip mismatch\nwant bytes: %x\n got bytes: %x", buf, rebuf) + } +} + func TestExecutionPayloadAmsterdamRoundtrip(t *testing.T) { - p := &ExecutionPayloadAmsterdam{ + blob, excess, slot := uint64(131072), uint64(0), uint64(200) + p := &ExecutionPayload{ ParentHash: common.Hash{0x11}, FeeRecipient: common.Address{0x22}, StateRoot: common.Hash{0x33}, @@ -93,26 +115,28 @@ func TestExecutionPayloadAmsterdamRoundtrip(t *testing.T) { BlockHash: common.Hash{0x66}, Transactions: [][]byte{{0x02, 0x03}, {0x04}}, Withdrawals: []*Withdrawal{{Index: 1, ValidatorIndex: 2, Amount: 3}}, - BlobGasUsed: 131072, - ExcessBlobGas: 0, + BlobGasUsed: &blob, + ExcessBlobGas: &excess, BlockAccessList: []byte{0xde, 0xad, 0xbe, 0xef}, - SlotNumber: 200, + SlotNumber: &slot, } - encDec(t, p, func() *ExecutionPayloadAmsterdam { return new(ExecutionPayloadAmsterdam) }) + encDecOnFork(t, p, forkAmsterdam, func() *ExecutionPayload { return new(ExecutionPayload) }) } func TestForkchoiceUpdateAmsterdamRoundtrip(t *testing.T) { // With payload_attributes and custody_columns present. bits := &Bitvector128{Bytes: make([]byte, CellsPerExtBlob/8)} bits.Bytes[0] = 0x01 - attrs := &PayloadAttributesAmsterdam{ + root := common.Hash{0x77} + slot, tgl := uint64(42), uint64(30000000) + attrs := &PayloadAttributes{ Timestamp: 1700000000, PrevRandao: common.Hash{0x55}, SuggestedFeeRecipient: common.Address{0x66}, Withdrawals: []*Withdrawal{}, - ParentBeaconBlockRoot: common.Hash{0x77}, - SlotNumber: 42, - TargetGasLimit: 30000000, + ParentBeaconBlockRoot: &root, + SlotNumber: &slot, + TargetGasLimit: &tgl, } fcu := &ForkchoiceUpdateAmsterdam{ ForkchoiceState: &ForkchoiceState{ @@ -120,10 +144,10 @@ func TestForkchoiceUpdateAmsterdamRoundtrip(t *testing.T) { SafeBlockHash: common.Hash{0xbb}, FinalizedBlockHash: common.Hash{0xcc}, }, - PayloadAttributes: []*PayloadAttributesAmsterdam{attrs}, + PayloadAttributes: []*PayloadAttributes{attrs}, CustodyColumns: []*Bitvector128{bits}, } - encDec(t, fcu, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) }) + encDecOnFork(t, fcu, forkAmsterdam, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) }) // Without optionals. bare := &ForkchoiceUpdateAmsterdam{ @@ -131,5 +155,5 @@ func TestForkchoiceUpdateAmsterdamRoundtrip(t *testing.T) { HeadBlockHash: common.Hash{0xdd}, }, } - encDec(t, bare, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) }) + encDecOnFork(t, bare, forkAmsterdam, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) }) } diff --git a/beacon/engine/ssz/validate_test.go b/beacon/engine/ssz/validate_test.go new file mode 100644 index 0000000000..0c2568c7dc --- /dev/null +++ b/beacon/engine/ssz/validate_test.go @@ -0,0 +1,120 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ssz + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/karalabe/ssz" +) + +func u64p(v uint64) *uint64 { return &v } + +func TestExecutionPayloadValidate(t *testing.T) { + tests := []struct { + name string + fork ssz.Fork + mutate func(*ExecutionPayload) + wantErr bool + }{ + {"paris ok", ssz.ForkParis, func(p *ExecutionPayload) {}, false}, + {"paris with withdrawals", ssz.ForkParis, func(p *ExecutionPayload) { + p.Withdrawals = []*Withdrawal{{}} + }, true}, + {"paris with blob gas", ssz.ForkParis, func(p *ExecutionPayload) { + p.BlobGasUsed = u64p(1) + }, true}, + {"cancun missing blob gas", ssz.ForkDencun, func(p *ExecutionPayload) { + p.BlobGasUsed, p.ExcessBlobGas = nil, nil + }, true}, + {"cancun ok", ssz.ForkDencun, func(p *ExecutionPayload) { + p.BlobGasUsed, p.ExcessBlobGas = u64p(0), u64p(0) + }, false}, + {"cancun with bal", ssz.ForkDencun, func(p *ExecutionPayload) { + p.BlobGasUsed, p.ExcessBlobGas = u64p(0), u64p(0) + p.BlockAccessList = []byte{0x01} + }, true}, + {"amsterdam missing slot", forkAmsterdam, func(p *ExecutionPayload) { + p.BlobGasUsed, p.ExcessBlobGas = u64p(0), u64p(0) + p.SlotNumber = nil + }, true}, + {"amsterdam ok", forkAmsterdam, func(p *ExecutionPayload) { + p.BlobGasUsed, p.ExcessBlobGas = u64p(0), u64p(0) + p.SlotNumber = u64p(1) + p.BlockAccessList = []byte{0x01} + }, false}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + p := &ExecutionPayload{} + tc.mutate(p) + err := p.Validate(tc.fork) + if (err != nil) != tc.wantErr { + t.Fatalf("Validate(%d) err=%v wantErr=%v", tc.fork, err, tc.wantErr) + } + }) + } +} + +func TestPayloadAttributesValidate(t *testing.T) { + root := common.Hash{0x77} + tests := []struct { + name string + fork ssz.Fork + mutate func(*PayloadAttributes) + wantErr bool + }{ + {"paris ok", ssz.ForkParis, func(a *PayloadAttributes) {}, false}, + {"cancun missing beacon root", ssz.ForkDencun, func(a *PayloadAttributes) {}, true}, + {"cancun ok", ssz.ForkDencun, func(a *PayloadAttributes) { + a.ParentBeaconBlockRoot = &root + }, false}, + {"paris with beacon root", ssz.ForkParis, func(a *PayloadAttributes) { + a.ParentBeaconBlockRoot = &root + }, true}, + {"amsterdam missing slot/tgl", forkAmsterdam, func(a *PayloadAttributes) { + a.ParentBeaconBlockRoot = &root + }, true}, + {"amsterdam ok", forkAmsterdam, func(a *PayloadAttributes) { + a.ParentBeaconBlockRoot = &root + a.SlotNumber, a.TargetGasLimit = u64p(1), u64p(2) + }, false}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + a := &PayloadAttributes{} + tc.mutate(a) + err := a.Validate(tc.fork) + if (err != nil) != tc.wantErr { + t.Fatalf("Validate(%d) err=%v wantErr=%v", tc.fork, err, tc.wantErr) + } + }) + } +} + +func TestExecutionPayloadBodyValidate(t *testing.T) { + if err := (&ExecutionPayloadBody{Withdrawals: []*Withdrawal{{}}}).Validate(ssz.ForkParis); err == nil { + t.Error("expected error for withdrawals at Paris") + } + if err := (&ExecutionPayloadBody{BlockAccessList: []byte{1}}).Validate(ssz.ForkDencun); err == nil { + t.Error("expected error for bal at Cancun") + } + if err := (&ExecutionPayloadBody{Withdrawals: []*Withdrawal{{}}}).Validate(forkAmsterdam); err != nil { + t.Errorf("unexpected error at Amsterdam: %v", err) + } +} diff --git a/eth/engineapi/blobs.go b/eth/engineapi/blobs.go index c13362fca9..0d80da45c6 100644 --- a/eth/engineapi/blobs.go +++ b/eth/engineapi/blobs.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/beacon/engine" sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz" + "github.com/karalabe/ssz" ) // handleBlobs dispatches POST /engine/v2/blobs/v{1,2,3,4}. @@ -46,7 +47,7 @@ func (rt *Router) handleBlobs(w http.ResponseWriter, r *http.Request, suffix str func (rt *Router) handleBlobsV1(w http.ResponseWriter, r *http.Request) { req := new(sszt.BlobsVersionedHashesRequest) - if !readSSZRequest(w, r, req, maxPayloadBytes) { + if !readSSZRequest(w, r, req, ssz.ForkUnknown, maxPayloadBytes) { return } if len(req.VersionedHashes) > sszt.MaxBlobsRequest { @@ -69,14 +70,14 @@ func (rt *Router) handleBlobsV1(w http.ResponseWriter, r *http.Request) { } resp.Entries[i] = e } - writeSSZResponse(w, resp) + writeSSZResponse(w, resp, ssz.ForkUnknown) } // handleBlobsV2 serves both /v2 (all-or-nothing) and /v3 (partial). The // allowPartial flag selects between them. func (rt *Router) handleBlobsV2(w http.ResponseWriter, r *http.Request, allowPartial bool) { req := new(sszt.BlobsVersionedHashesRequest) - if !readSSZRequest(w, r, req, maxPayloadBytes) { + if !readSSZRequest(w, r, req, ssz.ForkUnknown, maxPayloadBytes) { return } if len(req.VersionedHashes) > sszt.MaxBlobsRequest { @@ -106,7 +107,7 @@ func (rt *Router) handleBlobsV2(w http.ResponseWriter, r *http.Request, allowPar } resp.Entries[i] = e } - writeSSZResponse(w, resp) + writeSSZResponse(w, resp, ssz.ForkUnknown) } // handleBlobsV4 implements POST /engine/v2/blobs/v4 (cell-range selection). @@ -115,7 +116,7 @@ func (rt *Router) handleBlobsV2(w http.ResponseWriter, r *http.Request, allowPar // request body and the indices_bitarray length to keep the wire contract live. func (rt *Router) handleBlobsV4(w http.ResponseWriter, r *http.Request) { req := new(sszt.BlobsV4Request) - if !readSSZRequest(w, r, req, maxPayloadBytes) { + if !readSSZRequest(w, r, req, ssz.ForkUnknown, maxPayloadBytes) { return } if req.IndicesBitarray == nil || len(req.IndicesBitarray.Bytes) != sszt.CellsPerExtBlob/8 { diff --git a/eth/engineapi/bodies.go b/eth/engineapi/bodies.go index abc907ed38..6dd77209b8 100644 --- a/eth/engineapi/bodies.go +++ b/eth/engineapi/bodies.go @@ -23,16 +23,17 @@ import ( sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params/forks" + "github.com/karalabe/ssz" ) // handleBodiesByHash implements POST /engine/v2/{fork}/bodies/hash. func (rt *Router) handleBodiesByHash(w http.ResponseWriter, r *http.Request, fork forks.Fork) { - if fork != forks.Amsterdam { - writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") + sf, ok := resolveFork(w, fork, forks.Paris) + if !ok { return } req := new(sszt.BodiesByHashRequest) - if !readSSZRequest(w, r, req, maxPayloadBytes) { + if !readSSZRequest(w, r, req, sf, maxPayloadBytes) { return } if len(req.BlockHashes) > sszt.MaxBodiesRequest { @@ -40,13 +41,13 @@ func (rt *Router) handleBodiesByHash(w http.ResponseWriter, r *http.Request, for return } bodies, timestamps := rt.backend.BodiesByHash(req.BlockHashes) - writeSSZResponse(w, buildBodiesResponseAmsterdam(rt.backend, fork, bodies, timestamps)) + writeSSZResponse(w, buildBodiesResponse(rt.backend, fork, sf, bodies, timestamps), sf) } // handleBodiesByRange implements GET /engine/v2/{fork}/bodies?from=&count=. func (rt *Router) handleBodiesByRange(w http.ResponseWriter, r *http.Request, fork forks.Fork) { - if fork != forks.Amsterdam { - writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") + sf, ok := resolveFork(w, fork, forks.Paris) + if !ok { return } q := r.URL.Query() @@ -65,43 +66,49 @@ func (rt *Router) handleBodiesByRange(w http.ResponseWriter, r *http.Request, fo return } bodies, timestamps := rt.backend.BodiesByRange(from, count) - writeSSZResponse(w, buildBodiesResponseAmsterdam(rt.backend, fork, bodies, timestamps)) + writeSSZResponse(w, buildBodiesResponse(rt.backend, fork, sf, bodies, timestamps), sf) } -// buildBodiesResponseAmsterdam assembles a BodiesResponseAmsterdam, marking -// out-of-era blocks as available=false per the URL fork window. -func buildBodiesResponseAmsterdam(b Backend, fork forks.Fork, bodies []*types.Body, ts []uint64) *sszt.BodiesResponseAmsterdam { - out := &sszt.BodiesResponseAmsterdam{ - Entries: make([]*sszt.BodyEntryAmsterdam, len(bodies)), +// buildBodiesResponse assembles a BodiesResponse for the given fork, marking +// out-of-era blocks as available=false per the URL fork window. The body shape +// is fork-driven by the codec; bodyToSSZ populates the superset and the codec +// emits only the fork's active fields. +func buildBodiesResponse(b Backend, fork forks.Fork, sf ssz.Fork, bodies []*types.Body, ts []uint64) *sszt.BodiesResponse { + out := &sszt.BodiesResponse{ + Entries: make([]*sszt.BodyEntry, len(bodies)), } for i, body := range bodies { - entry := &sszt.BodyEntryAmsterdam{Body: new(sszt.ExecutionPayloadBodyAmsterdam)} + entry := &sszt.BodyEntry{Body: new(sszt.ExecutionPayloadBody)} if body != nil && b.ForkFromTimestamp(ts[i]) == fork { entry.Available = true - entry.Body = bodyToAmsterdamSSZ(body) + entry.Body = bodyToSSZ(body, sf) } out.Entries[i] = entry } return out } -// bodyToAmsterdamSSZ flattens a *types.Body into the Amsterdam body shape. -func bodyToAmsterdamSSZ(body *types.Body) *sszt.ExecutionPayloadBodyAmsterdam { - out := &sszt.ExecutionPayloadBodyAmsterdam{ +// bodyToSSZ flattens a *types.Body into the monolithic body shape. Withdrawals +// are only attached from Shanghai on, matching the fork's wire shape. +func bodyToSSZ(body *types.Body, sf ssz.Fork) *sszt.ExecutionPayloadBody { + out := &sszt.ExecutionPayloadBody{ Transactions: make([][]byte, len(body.Transactions)), - Withdrawals: make([]*sszt.Withdrawal, len(body.Withdrawals)), } for i, tx := range body.Transactions { out.Transactions[i], _ = tx.MarshalBinary() } - for i, w := range body.Withdrawals { - out.Withdrawals[i] = &sszt.Withdrawal{ - Index: w.Index, - ValidatorIndex: w.Validator, - Address: w.Address, - Amount: w.Amount, + if sf >= ssz.ForkShapella { + out.Withdrawals = make([]*sszt.Withdrawal, len(body.Withdrawals)) + for i, w := range body.Withdrawals { + out.Withdrawals[i] = &sszt.Withdrawal{ + Index: w.Index, + ValidatorIndex: w.Validator, + Address: w.Address, + Amount: w.Amount, + } } } - // BlockAccessList is not yet on types.Body; once wired through, copy here. + // BlockAccessList is not yet on types.Body; once wired through, copy here + // (gated by sf >= forkAmsterdam). return out } diff --git a/eth/engineapi/forkchoice.go b/eth/engineapi/forkchoice.go index ef4568eb47..15acd786c6 100644 --- a/eth/engineapi/forkchoice.go +++ b/eth/engineapi/forkchoice.go @@ -26,12 +26,14 @@ import ( // handleForkchoice implements POST /engine/v2/{fork}/forkchoice. func (rt *Router) handleForkchoice(w http.ResponseWriter, r *http.Request, fork forks.Fork) { - if fork != forks.Amsterdam { - writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") + // The execution-apis spec currently only defines the forkchoice envelope + // for Amsterdam; the inner attributes shape is fork-driven by the codec. + sf, ok := resolveFork(w, fork, forks.Amsterdam) + if !ok { return } fcu := new(sszt.ForkchoiceUpdateAmsterdam) - if !readSSZRequest(w, r, fcu, maxPayloadBytes) { + if !readSSZRequest(w, r, fcu, sf, maxPayloadBytes) { return } if err := fcu.Validate(); err != nil { @@ -41,17 +43,21 @@ func (rt *Router) handleForkchoice(w http.ResponseWriter, r *http.Request, fork state := sszt.ForkchoiceStateToV1(fcu.ForkchoiceState) var attrs *engine.PayloadAttributes if len(fcu.PayloadAttributes) == 1 { - ssz := fcu.PayloadAttributes[0] + attr := fcu.PayloadAttributes[0] + if err := attr.Validate(sf); err != nil { + writeProblem(w, http.StatusBadRequest, ErrInvalidAttributes, err.Error()) + return + } // If PayloadAttributes is present the URL fork MUST match the fork // the new payload would belong to. Today only Amsterdam URL exists // in this implementation so the timestamp check is implicit; we // keep an explicit guard for future fork URLs. - if rt.backend.ForkFromTimestamp(ssz.Timestamp) != fork { + if rt.backend.ForkFromTimestamp(attr.Timestamp) != fork { writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "payload_attributes timestamp does not match URL fork") return } - attrs = sszt.PayloadAttributesAmsterdamToEngine(ssz) + attrs = sszt.PayloadAttributesToEngine(attr) // target_gas_limit and custody_columns are not yet plumbed into // the JSON-RPC engine API. Custody is parsed-but-stubbed per // agreed scope; target_gas_limit will be picked up when the @@ -73,5 +79,5 @@ func (rt *Router) handleForkchoice(w http.ResponseWriter, r *http.Request, fork if resp.PayloadID != nil { out.PayloadID = [][8]byte{[8]byte(*resp.PayloadID)} } - writeSSZResponse(w, out) + writeSSZResponse(w, out, sf) } diff --git a/eth/engineapi/forkresolve.go b/eth/engineapi/forkresolve.go new file mode 100644 index 0000000000..3f4af2c657 --- /dev/null +++ b/eth/engineapi/forkresolve.go @@ -0,0 +1,41 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package engineapi + +import ( + "net/http" + + sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz" + "github.com/ethereum/go-ethereum/params/forks" + "github.com/karalabe/ssz" +) + +// resolveFork maps the URL fork onto the ssz.Fork the codec multiplexes on, +// enforcing that it is at least min. On failure it writes the appropriate +// problem response and returns ok=false; the caller should return immediately. +func resolveFork(w http.ResponseWriter, fork, min forks.Fork) (ssz.Fork, bool) { + if fork < min { + writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") + return ssz.ForkUnknown, false + } + sf, ok := sszt.ForkFor(fork) + if !ok { + writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") + return ssz.ForkUnknown, false + } + return sf, true +} diff --git a/eth/engineapi/get_payload.go b/eth/engineapi/get_payload.go index ccc51a8578..5975c0114f 100644 --- a/eth/engineapi/get_payload.go +++ b/eth/engineapi/get_payload.go @@ -24,12 +24,13 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/params/forks" "github.com/holiman/uint256" + "github.com/karalabe/ssz" ) // handleGetPayload implements GET /engine/v2/{fork}/payloads/{payloadId}. func (rt *Router) handleGetPayload(w http.ResponseWriter, r *http.Request, fork forks.Fork, idHex string) { - if fork != forks.Amsterdam { - writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") + sf, ok := resolveFork(w, fork, forks.Amsterdam) + if !ok { return } raw, err := hexutil.Decode(idHex) @@ -46,17 +47,17 @@ func (rt *Router) handleGetPayload(w http.ResponseWriter, r *http.Request, fork return } - out := buildBuiltPayloadAmsterdam(env) + out := buildBuiltPayloadAmsterdam(env, sf) w.Header().Set("Cache-Control", "no-store") - writeSSZResponse(w, out) + writeSSZResponse(w, out, sf) } // buildBuiltPayloadAmsterdam packages an engine.ExecutionPayloadEnvelope into // the SSZ BuiltPayload shape. BlockValue/BlobsBundle/Requests come straight // across; the inner payload goes through the SSZ converter. -func buildBuiltPayloadAmsterdam(env *engine.ExecutionPayloadEnvelope) *sszt.BuiltPayloadAmsterdam { +func buildBuiltPayloadAmsterdam(env *engine.ExecutionPayloadEnvelope, sf ssz.Fork) *sszt.BuiltPayloadAmsterdam { out := &sszt.BuiltPayloadAmsterdam{ - Payload: sszt.ExecutionPayloadAmsterdamFromEngine(env.ExecutionPayload), + Payload: sszt.ExecutionPayloadFromEngine(env.ExecutionPayload, sf), BlockValue: new(uint256.Int), ExecutionRequests: env.Requests, ShouldOverrideBuilder: env.Override, diff --git a/eth/engineapi/media.go b/eth/engineapi/media.go index 34df822a48..4b0063520b 100644 --- a/eth/engineapi/media.go +++ b/eth/engineapi/media.go @@ -34,9 +34,10 @@ const ( ) // readSSZRequest enforces the SSZ content-type and Content-Length cap, then -// decodes the request body into obj. Writes the appropriate problem response -// on failure and returns false; the caller should return immediately. -func readSSZRequest(w http.ResponseWriter, r *http.Request, obj ssz.Object, max int64) bool { +// decodes the request body into obj for the given fork. Writes the appropriate +// problem response on failure and returns false; the caller should return +// immediately. +func readSSZRequest(w http.ResponseWriter, r *http.Request, obj ssz.Object, fork ssz.Fork, max int64) bool { if !strings.EqualFold(r.Header.Get("Content-Type"), sszContentType) { writeProblem(w, http.StatusUnsupportedMediaType, ErrUnsupportedMedia, "expected "+sszContentType) return false @@ -54,18 +55,18 @@ func readSSZRequest(w http.ResponseWriter, r *http.Request, obj ssz.Object, max writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "") return false } - if err := ssz.DecodeFromBytes(body, obj); err != nil { + if err := ssz.DecodeFromBytesOnFork(body, obj, fork); err != nil { writeProblem(w, http.StatusBadRequest, ErrSSZDecode, "") return false } return true } -// writeSSZResponse encodes obj into the response body. On encode failure it -// writes an internal-error problem and returns false. -func writeSSZResponse(w http.ResponseWriter, obj ssz.Object) bool { - buf := make([]byte, ssz.Size(obj)) - if err := ssz.EncodeToBytes(buf, obj); err != nil { +// writeSSZResponse encodes obj into the response body for the given fork. On +// encode failure it writes an internal-error problem and returns false. +func writeSSZResponse(w http.ResponseWriter, obj ssz.Object, fork ssz.Fork) bool { + buf := make([]byte, ssz.SizeOnFork(obj, fork)) + if err := ssz.EncodeToBytesOnFork(buf, obj, fork); err != nil { writeProblem(w, http.StatusInternalServerError, ErrInternal, err.Error()) return false } diff --git a/eth/engineapi/payloads.go b/eth/engineapi/payloads.go index 18858fe7fd..6e4e946fb7 100644 --- a/eth/engineapi/payloads.go +++ b/eth/engineapi/payloads.go @@ -27,15 +27,21 @@ import ( // handleNewPayload implements POST /engine/v2/{fork}/payloads. func (rt *Router) handleNewPayload(w http.ResponseWriter, r *http.Request, fork forks.Fork) { - if fork != forks.Amsterdam { - writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") + // The execution-apis spec currently only defines the payload envelope for + // Amsterdam; the inner payload shape is fork-driven by the codec. + sf, ok := resolveFork(w, fork, forks.Amsterdam) + if !ok { return } env := new(sszt.ExecutionPayloadEnvelopeAmsterdam) - if !readSSZRequest(w, r, env, maxPayloadBytes) { + if !readSSZRequest(w, r, env, sf, maxPayloadBytes) { return } - data := sszt.ExecutionPayloadAmsterdamToEngine(env.Payload) + if err := env.Payload.Validate(sf); err != nil { + writeProblem(w, http.StatusUnprocessableEntity, ErrInvalidBody, err.Error()) + return + } + data := sszt.ExecutionPayloadToEngine(env.Payload) // The spec drops expectedBlobVersionedHashes; we recompute them from the // payload's transactions before passing to the EL. @@ -50,7 +56,7 @@ func (rt *Router) handleNewPayload(w http.ResponseWriter, r *http.Request, fork mapBackendErr(w, err) return } - writeSSZResponse(w, sszt.PayloadStatusFromV1(&status)) + writeSSZResponse(w, sszt.PayloadStatusFromV1(&status), sf) } // versionedHashesFromTxs extracts the blob versioned-hash list from the diff --git a/eth/engineapi/router_test.go b/eth/engineapi/router_test.go index e7cd2f8d67..1012628401 100644 --- a/eth/engineapi/router_test.go +++ b/eth/engineapi/router_test.go @@ -103,10 +103,10 @@ func newTestServer(t *testing.T, b Backend) *httptest.Server { return httptest.NewServer(http.StripPrefix(BasePath, NewRouter(b))) } -func sszPost(t *testing.T, srv *httptest.Server, path string, body ssz.Object) *http.Response { +func sszPost(t *testing.T, srv *httptest.Server, path string, body ssz.Object, fork ssz.Fork) *http.Response { t.Helper() - buf := make([]byte, ssz.Size(body)) - if err := ssz.EncodeToBytes(buf, body); err != nil { + buf := make([]byte, ssz.SizeOnFork(body, fork)) + if err := ssz.EncodeToBytesOnFork(buf, body, fork); err != nil { t.Fatalf("encode: %v", err) } req, _ := http.NewRequest(http.MethodPost, srv.URL+BasePath+path, bytes.NewReader(buf)) @@ -118,14 +118,14 @@ func sszPost(t *testing.T, srv *httptest.Server, path string, body ssz.Object) * return resp } -func decodeSSZ[T ssz.Object](t *testing.T, resp *http.Response, obj T) { +func decodeSSZ[T ssz.Object](t *testing.T, resp *http.Response, obj T, fork ssz.Fork) { t.Helper() defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("read body: %v", err) } - if err := ssz.DecodeFromBytes(body, obj); err != nil { + if err := ssz.DecodeFromBytesOnFork(body, obj, fork); err != nil { t.Fatalf("decode response: %v\nbody: %x", err, body) } } @@ -139,19 +139,24 @@ func TestRouterNewPayload(t *testing.T) { srv := newTestServer(t, b) defer srv.Close() + amsterdam, _ := sszt.ForkFor(forks.Amsterdam) + blob, excess, slot := uint64(0), uint64(0), uint64(0) env := &sszt.ExecutionPayloadEnvelopeAmsterdam{ - Payload: &sszt.ExecutionPayloadAmsterdam{ + Payload: &sszt.ExecutionPayload{ BaseFeePerGas: uint256.NewInt(7e9), LogsBloom: [256]byte{}, + BlobGasUsed: &blob, + ExcessBlobGas: &excess, + SlotNumber: &slot, }, ParentBeaconBlockRoot: common.Hash{0x55}, } - resp := sszPost(t, srv, "/amsterdam/payloads", env) + resp := sszPost(t, srv, "/amsterdam/payloads", env, amsterdam) if resp.StatusCode != 200 { t.Fatalf("want 200, got %d", resp.StatusCode) } got := new(sszt.PayloadStatus) - decodeSSZ(t, resp, got) + decodeSSZ(t, resp, got, amsterdam) if got.Status != sszt.StatusValid { t.Errorf("status=%d want VALID(%d)", got.Status, sszt.StatusValid) } @@ -169,15 +174,16 @@ func TestRouterForkchoice(t *testing.T) { srv := newTestServer(t, b) defer srv.Close() + amsterdam, _ := sszt.ForkFor(forks.Amsterdam) fcu := &sszt.ForkchoiceUpdateAmsterdam{ ForkchoiceState: &sszt.ForkchoiceState{HeadBlockHash: common.Hash{0xaa}}, } - resp := sszPost(t, srv, "/amsterdam/forkchoice", fcu) + resp := sszPost(t, srv, "/amsterdam/forkchoice", fcu, amsterdam) if resp.StatusCode != 200 { t.Fatalf("want 200, got %d", resp.StatusCode) } got := new(sszt.ForkchoiceUpdateResponseAmsterdam) - decodeSSZ(t, resp, got) + decodeSSZ(t, resp, got, amsterdam) if got.PayloadStatus == nil || got.PayloadStatus.Status != sszt.StatusValid { t.Errorf("status: %#v", got.PayloadStatus) } @@ -310,7 +316,7 @@ func TestRouterBlobsV2AllOrNothing(t *testing.T) { srv := newTestServer(t, b) defer srv.Close() req := &sszt.BlobsVersionedHashesRequest{VersionedHashes: []common.Hash{{0x01}}} - resp := sszPost(t, srv, "/blobs/v2", req) + resp := sszPost(t, srv, "/blobs/v2", req, ssz.ForkUnknown) defer resp.Body.Close() if resp.StatusCode != 204 { t.Fatalf("want 204, got %d", resp.StatusCode) @@ -322,12 +328,12 @@ func TestRouterBlobsV3PartialOK(t *testing.T) { srv := newTestServer(t, b) defer srv.Close() req := &sszt.BlobsVersionedHashesRequest{VersionedHashes: []common.Hash{{0x01}, {0x02}}} - resp := sszPost(t, srv, "/blobs/v3", req) + resp := sszPost(t, srv, "/blobs/v3", req, ssz.ForkUnknown) if resp.StatusCode != 200 { t.Fatalf("want 200, got %d", resp.StatusCode) } got := new(sszt.BlobsV2Response) - decodeSSZ(t, resp, got) + decodeSSZ(t, resp, got, ssz.ForkUnknown) if len(got.Entries) != 2 { t.Fatalf("entries=%d", len(got.Entries)) }