beacon/engine/ssz: update types

This commit is contained in:
MariusVanDerWijden 2026-06-09 14:05:22 +02:00
parent b694357889
commit 8685b80f23
No known key found for this signature in database
19 changed files with 921 additions and 702 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View file

@ -17,6 +17,8 @@
package ssz package ssz
import ( import (
"fmt"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/karalabe/ssz" "github.com/karalabe/ssz"
) )
@ -40,13 +42,70 @@ func (r *BodiesByHashRequest) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfStaticBytesContent(c, &r.BlockHashes, MaxBodiesRequest) ssz.DefineSliceOfStaticBytesContent(c, &r.BlockHashes, MaxBodiesRequest)
} }
// BodyEntryAmsterdam is the per-block entry returned by /amsterdam/bodies/... // ExecutionPayloadBody is the monolithic body shape returned by /{fork}/bodies.
type BodyEntryAmsterdam struct { //
Available bool // Field groups:
Body *ExecutionPayloadBodyAmsterdam // - 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 size := uint32(1 + 4) // bool + offset
if fixed { if fixed {
return size return size
@ -55,19 +114,19 @@ func (e *BodyEntryAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
return size return size
} }
func (e *BodyEntryAmsterdam) DefineSSZ(c *ssz.Codec) { func (e *BodyEntry) DefineSSZ(c *ssz.Codec) {
ssz.DefineBool(c, &e.Available) ssz.DefineBool(c, &e.Available)
ssz.DefineDynamicObjectOffset(c, &e.Body) ssz.DefineDynamicObjectOffset(c, &e.Body)
ssz.DefineDynamicObjectContent(c, &e.Body) ssz.DefineDynamicObjectContent(c, &e.Body)
} }
// BodiesResponseAmsterdam is the SSZ response of /amsterdam/bodies/... // BodiesResponse is the SSZ response of /{fork}/bodies/...
type BodiesResponseAmsterdam struct { type BodiesResponse struct {
Entries []*BodyEntryAmsterdam 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) size := uint32(4)
if fixed { if fixed {
return size return size
@ -76,87 +135,7 @@ func (r *BodiesResponseAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
return size return size
} }
func (r *BodiesResponseAmsterdam) DefineSSZ(c *ssz.Codec) { func (r *BodiesResponse) 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) {
ssz.DefineSliceOfDynamicObjectsOffset(c, &r.Entries, MaxBodiesRequest) ssz.DefineSliceOfDynamicObjectsOffset(c, &r.Entries, MaxBodiesRequest)
ssz.DefineSliceOfDynamicObjectsContent(c, &r.Entries, MaxBodiesRequest) ssz.DefineSliceOfDynamicObjectsContent(c, &r.Entries, MaxBodiesRequest)
} }

View file

@ -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)
}
})
}
}

View file

@ -21,6 +21,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/holiman/uint256" "github.com/holiman/uint256"
"github.com/karalabe/ssz"
) )
// statusToEnum maps the JSON-RPC PayloadStatusV1.Status string to the SSZ // statusToEnum maps the JSON-RPC PayloadStatusV1.Status string to the SSZ
@ -127,46 +128,68 @@ func withdrawalsToTypes(ws []*Withdrawal) []*types.Withdrawal {
return out return out
} }
// PayloadAttributesAmsterdamFromEngine converts a JSON-RPC engine.PayloadAttributes // PayloadAttributesFromEngine converts a JSON-RPC engine.PayloadAttributes into
// into its Amsterdam SSZ form. The caller is responsible for ensuring the // the monolithic SSZ form for the given fork. Only the fields the fork's wire
// attributes carry every Amsterdam-required field. // shape carries are populated; the caller should pass the same fork to the
func PayloadAttributesAmsterdamFromEngine(a *engine.PayloadAttributes) *PayloadAttributesAmsterdam { // codec. target_gas_limit has no representation in engine.PayloadAttributes
out := &PayloadAttributesAmsterdam{ // 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, Timestamp: a.Timestamp,
PrevRandao: a.Random, PrevRandao: a.Random,
SuggestedFeeRecipient: a.SuggestedFeeRecipient, SuggestedFeeRecipient: a.SuggestedFeeRecipient,
Withdrawals: withdrawalsFromTypes(a.Withdrawals),
} }
if a.BeaconRoot != nil { if fork >= ssz.ForkShapella {
out.ParentBeaconBlockRoot = *a.BeaconRoot out.Withdrawals = withdrawalsFromTypes(a.Withdrawals)
} }
if a.SlotNumber != nil { if fork >= ssz.ForkDencun {
out.SlotNumber = *a.SlotNumber 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 return out
} }
// PayloadAttributesAmsterdamToEngine is the inverse of the From helper. The // PayloadAttributesToEngine is the inverse of PayloadAttributesFromEngine. The
// target_gas_limit field has no representation in engine.PayloadAttributes // target_gas_limit field has no representation in engine.PayloadAttributes and
// today and is dropped on the way down; the caller can fish it out of the SSZ // is dropped; the caller can read it from the SSZ struct directly.
// struct directly. func PayloadAttributesToEngine(a *PayloadAttributes) *engine.PayloadAttributes {
func PayloadAttributesAmsterdamToEngine(a *PayloadAttributesAmsterdam) *engine.PayloadAttributes { out := &engine.PayloadAttributes{
root := a.ParentBeaconBlockRoot
slot := a.SlotNumber
return &engine.PayloadAttributes{
Timestamp: a.Timestamp, Timestamp: a.Timestamp,
Random: a.PrevRandao, Random: a.PrevRandao,
SuggestedFeeRecipient: a.SuggestedFeeRecipient, SuggestedFeeRecipient: a.SuggestedFeeRecipient,
Withdrawals: withdrawalsToTypes(a.Withdrawals), 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 // ExecutionPayloadFromEngine converts an ExecutableData into the monolithic SSZ
// Amsterdam payload shape. The block_access_list and slot_number fields are // payload for the given fork. Only the fork's active fields are populated so
// expected to be present; missing values yield empty/zero defaults. // the codec (driven by the same fork) and Validate(fork) stay consistent.
func ExecutionPayloadAmsterdamFromEngine(d *engine.ExecutableData) *ExecutionPayloadAmsterdam { // 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 var bloom [256]byte
copy(bloom[:], d.LogsBloom) copy(bloom[:], d.LogsBloom)
var fee *uint256.Int var fee *uint256.Int
@ -174,7 +197,7 @@ func ExecutionPayloadAmsterdamFromEngine(d *engine.ExecutableData) *ExecutionPay
fee = new(uint256.Int) fee = new(uint256.Int)
fee.SetFromBig(d.BaseFeePerGas) fee.SetFromBig(d.BaseFeePerGas)
} }
out := &ExecutionPayloadAmsterdam{ out := &ExecutionPayload{
ParentHash: d.ParentHash, ParentHash: d.ParentHash,
FeeRecipient: d.FeeRecipient, FeeRecipient: d.FeeRecipient,
StateRoot: d.StateRoot, StateRoot: d.StateRoot,
@ -189,48 +212,66 @@ func ExecutionPayloadAmsterdamFromEngine(d *engine.ExecutableData) *ExecutionPay
BaseFeePerGas: fee, BaseFeePerGas: fee,
BlockHash: d.BlockHash, BlockHash: d.BlockHash,
Transactions: d.Transactions, Transactions: d.Transactions,
Withdrawals: withdrawalsFromTypes(d.Withdrawals),
} }
if d.BlobGasUsed != nil { if fork >= ssz.ForkShapella {
out.BlobGasUsed = *d.BlobGasUsed out.Withdrawals = withdrawalsFromTypes(d.Withdrawals)
} }
if d.ExcessBlobGas != nil { if fork >= ssz.ForkDencun {
out.ExcessBlobGas = *d.ExcessBlobGas 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 { if fork >= forkAmsterdam {
out.SlotNumber = *d.SlotNumber 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 return out
} }
// ExecutionPayloadAmsterdamToEngine is the inverse helper. The caller is // ExecutionPayloadToEngine is the inverse helper. The caller is responsible for
// responsible for the BAL payload that ExecutableData doesn't yet carry. // the BAL payload that ExecutableData doesn't yet carry.
func ExecutionPayloadAmsterdamToEngine(p *ExecutionPayloadAmsterdam) *engine.ExecutableData { func ExecutionPayloadToEngine(p *ExecutionPayload) *engine.ExecutableData {
bloom := append([]byte(nil), p.LogsBloom[:]...) bloom := append([]byte(nil), p.LogsBloom[:]...)
out := &engine.ExecutableData{ out := &engine.ExecutableData{
ParentHash: p.ParentHash, ParentHash: p.ParentHash,
FeeRecipient: p.FeeRecipient, FeeRecipient: p.FeeRecipient,
StateRoot: p.StateRoot, StateRoot: p.StateRoot,
ReceiptsRoot: p.ReceiptsRoot, ReceiptsRoot: p.ReceiptsRoot,
LogsBloom: bloom, LogsBloom: bloom,
Random: p.PrevRandao, Random: p.PrevRandao,
Number: p.BlockNumber, Number: p.BlockNumber,
GasLimit: p.GasLimit, GasLimit: p.GasLimit,
GasUsed: p.GasUsed, GasUsed: p.GasUsed,
Timestamp: p.Timestamp, Timestamp: p.Timestamp,
ExtraData: append([]byte(nil), p.ExtraData...), ExtraData: append([]byte(nil), p.ExtraData...),
BlockHash: p.BlockHash, BlockHash: p.BlockHash,
Transactions: p.Transactions, Transactions: p.Transactions,
Withdrawals: withdrawalsToTypes(p.Withdrawals), Withdrawals: withdrawalsToTypes(p.Withdrawals),
BlobGasUsed: &p.BlobGasUsed,
ExcessBlobGas: &p.ExcessBlobGas,
SlotNumber: &p.SlotNumber,
} }
if p.BaseFeePerGas != nil { if p.BaseFeePerGas != nil {
out.BaseFeePerGas = p.BaseFeePerGas.ToBig() 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 return out
} }

View file

@ -25,7 +25,7 @@ import (
// ExecutionPayloadEnvelopeAmsterdam is the request body of // ExecutionPayloadEnvelopeAmsterdam is the request body of
// POST /amsterdam/payloads. // POST /amsterdam/payloads.
type ExecutionPayloadEnvelopeAmsterdam struct { type ExecutionPayloadEnvelopeAmsterdam struct {
Payload *ExecutionPayloadAmsterdam Payload *ExecutionPayload
ParentBeaconBlockRoot common.Hash ParentBeaconBlockRoot common.Hash
ExecutionRequests [][]byte ExecutionRequests [][]byte
} }
@ -82,7 +82,7 @@ func (b *BlobsBundleV2) DefineSSZ(c *ssz.Codec) {
// BuiltPayloadAmsterdam is the response of GET /amsterdam/payloads/{id}. // BuiltPayloadAmsterdam is the response of GET /amsterdam/payloads/{id}.
type BuiltPayloadAmsterdam struct { type BuiltPayloadAmsterdam struct {
Payload *ExecutionPayloadAmsterdam Payload *ExecutionPayload
BlockValue *uint256.Int BlockValue *uint256.Int
BlobsBundle *BlobsBundleV2 BlobsBundle *BlobsBundleV2
ExecutionRequests [][]byte ExecutionRequests [][]byte
@ -120,7 +120,7 @@ func (p *BuiltPayloadAmsterdam) DefineSSZ(c *ssz.Codec) {
// length 0 or 1 of the 16-byte Bitvector wrapper. // length 0 or 1 of the 16-byte Bitvector wrapper.
type ForkchoiceUpdateAmsterdam struct { type ForkchoiceUpdateAmsterdam struct {
ForkchoiceState *ForkchoiceState ForkchoiceState *ForkchoiceState
PayloadAttributes []*PayloadAttributesAmsterdam PayloadAttributes []*PayloadAttributes
CustodyColumns []*Bitvector128 CustodyColumns []*Bitvector128
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View file

@ -77,8 +77,30 @@ func TestPayloadStatusRoundtrip(t *testing.T) {
encDec(t, &PayloadStatus{Status: StatusSyncing}, func() *PayloadStatus { return new(PayloadStatus) }) 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) { func TestExecutionPayloadAmsterdamRoundtrip(t *testing.T) {
p := &ExecutionPayloadAmsterdam{ blob, excess, slot := uint64(131072), uint64(0), uint64(200)
p := &ExecutionPayload{
ParentHash: common.Hash{0x11}, ParentHash: common.Hash{0x11},
FeeRecipient: common.Address{0x22}, FeeRecipient: common.Address{0x22},
StateRoot: common.Hash{0x33}, StateRoot: common.Hash{0x33},
@ -93,26 +115,28 @@ func TestExecutionPayloadAmsterdamRoundtrip(t *testing.T) {
BlockHash: common.Hash{0x66}, BlockHash: common.Hash{0x66},
Transactions: [][]byte{{0x02, 0x03}, {0x04}}, Transactions: [][]byte{{0x02, 0x03}, {0x04}},
Withdrawals: []*Withdrawal{{Index: 1, ValidatorIndex: 2, Amount: 3}}, Withdrawals: []*Withdrawal{{Index: 1, ValidatorIndex: 2, Amount: 3}},
BlobGasUsed: 131072, BlobGasUsed: &blob,
ExcessBlobGas: 0, ExcessBlobGas: &excess,
BlockAccessList: []byte{0xde, 0xad, 0xbe, 0xef}, 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) { func TestForkchoiceUpdateAmsterdamRoundtrip(t *testing.T) {
// With payload_attributes and custody_columns present. // With payload_attributes and custody_columns present.
bits := &Bitvector128{Bytes: make([]byte, CellsPerExtBlob/8)} bits := &Bitvector128{Bytes: make([]byte, CellsPerExtBlob/8)}
bits.Bytes[0] = 0x01 bits.Bytes[0] = 0x01
attrs := &PayloadAttributesAmsterdam{ root := common.Hash{0x77}
slot, tgl := uint64(42), uint64(30000000)
attrs := &PayloadAttributes{
Timestamp: 1700000000, Timestamp: 1700000000,
PrevRandao: common.Hash{0x55}, PrevRandao: common.Hash{0x55},
SuggestedFeeRecipient: common.Address{0x66}, SuggestedFeeRecipient: common.Address{0x66},
Withdrawals: []*Withdrawal{}, Withdrawals: []*Withdrawal{},
ParentBeaconBlockRoot: common.Hash{0x77}, ParentBeaconBlockRoot: &root,
SlotNumber: 42, SlotNumber: &slot,
TargetGasLimit: 30000000, TargetGasLimit: &tgl,
} }
fcu := &ForkchoiceUpdateAmsterdam{ fcu := &ForkchoiceUpdateAmsterdam{
ForkchoiceState: &ForkchoiceState{ ForkchoiceState: &ForkchoiceState{
@ -120,10 +144,10 @@ func TestForkchoiceUpdateAmsterdamRoundtrip(t *testing.T) {
SafeBlockHash: common.Hash{0xbb}, SafeBlockHash: common.Hash{0xbb},
FinalizedBlockHash: common.Hash{0xcc}, FinalizedBlockHash: common.Hash{0xcc},
}, },
PayloadAttributes: []*PayloadAttributesAmsterdam{attrs}, PayloadAttributes: []*PayloadAttributes{attrs},
CustodyColumns: []*Bitvector128{bits}, CustodyColumns: []*Bitvector128{bits},
} }
encDec(t, fcu, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) }) encDecOnFork(t, fcu, forkAmsterdam, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) })
// Without optionals. // Without optionals.
bare := &ForkchoiceUpdateAmsterdam{ bare := &ForkchoiceUpdateAmsterdam{
@ -131,5 +155,5 @@ func TestForkchoiceUpdateAmsterdamRoundtrip(t *testing.T) {
HeadBlockHash: common.Hash{0xdd}, HeadBlockHash: common.Hash{0xdd},
}, },
} }
encDec(t, bare, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) }) encDecOnFork(t, bare, forkAmsterdam, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) })
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

View file

@ -21,6 +21,7 @@ import (
"github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/beacon/engine"
sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz" sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz"
"github.com/karalabe/ssz"
) )
// handleBlobs dispatches POST /engine/v2/blobs/v{1,2,3,4}. // 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) { func (rt *Router) handleBlobsV1(w http.ResponseWriter, r *http.Request) {
req := new(sszt.BlobsVersionedHashesRequest) req := new(sszt.BlobsVersionedHashesRequest)
if !readSSZRequest(w, r, req, maxPayloadBytes) { if !readSSZRequest(w, r, req, ssz.ForkUnknown, maxPayloadBytes) {
return return
} }
if len(req.VersionedHashes) > sszt.MaxBlobsRequest { if len(req.VersionedHashes) > sszt.MaxBlobsRequest {
@ -69,14 +70,14 @@ func (rt *Router) handleBlobsV1(w http.ResponseWriter, r *http.Request) {
} }
resp.Entries[i] = e resp.Entries[i] = e
} }
writeSSZResponse(w, resp) writeSSZResponse(w, resp, ssz.ForkUnknown)
} }
// handleBlobsV2 serves both /v2 (all-or-nothing) and /v3 (partial). The // handleBlobsV2 serves both /v2 (all-or-nothing) and /v3 (partial). The
// allowPartial flag selects between them. // allowPartial flag selects between them.
func (rt *Router) handleBlobsV2(w http.ResponseWriter, r *http.Request, allowPartial bool) { func (rt *Router) handleBlobsV2(w http.ResponseWriter, r *http.Request, allowPartial bool) {
req := new(sszt.BlobsVersionedHashesRequest) req := new(sszt.BlobsVersionedHashesRequest)
if !readSSZRequest(w, r, req, maxPayloadBytes) { if !readSSZRequest(w, r, req, ssz.ForkUnknown, maxPayloadBytes) {
return return
} }
if len(req.VersionedHashes) > sszt.MaxBlobsRequest { 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 resp.Entries[i] = e
} }
writeSSZResponse(w, resp) writeSSZResponse(w, resp, ssz.ForkUnknown)
} }
// handleBlobsV4 implements POST /engine/v2/blobs/v4 (cell-range selection). // 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. // request body and the indices_bitarray length to keep the wire contract live.
func (rt *Router) handleBlobsV4(w http.ResponseWriter, r *http.Request) { func (rt *Router) handleBlobsV4(w http.ResponseWriter, r *http.Request) {
req := new(sszt.BlobsV4Request) req := new(sszt.BlobsV4Request)
if !readSSZRequest(w, r, req, maxPayloadBytes) { if !readSSZRequest(w, r, req, ssz.ForkUnknown, maxPayloadBytes) {
return return
} }
if req.IndicesBitarray == nil || len(req.IndicesBitarray.Bytes) != sszt.CellsPerExtBlob/8 { if req.IndicesBitarray == nil || len(req.IndicesBitarray.Bytes) != sszt.CellsPerExtBlob/8 {

View file

@ -23,16 +23,17 @@ import (
sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz" sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params/forks" "github.com/ethereum/go-ethereum/params/forks"
"github.com/karalabe/ssz"
) )
// handleBodiesByHash implements POST /engine/v2/{fork}/bodies/hash. // handleBodiesByHash implements POST /engine/v2/{fork}/bodies/hash.
func (rt *Router) handleBodiesByHash(w http.ResponseWriter, r *http.Request, fork forks.Fork) { func (rt *Router) handleBodiesByHash(w http.ResponseWriter, r *http.Request, fork forks.Fork) {
if fork != forks.Amsterdam { sf, ok := resolveFork(w, fork, forks.Paris)
writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") if !ok {
return return
} }
req := new(sszt.BodiesByHashRequest) req := new(sszt.BodiesByHashRequest)
if !readSSZRequest(w, r, req, maxPayloadBytes) { if !readSSZRequest(w, r, req, sf, maxPayloadBytes) {
return return
} }
if len(req.BlockHashes) > sszt.MaxBodiesRequest { if len(req.BlockHashes) > sszt.MaxBodiesRequest {
@ -40,13 +41,13 @@ func (rt *Router) handleBodiesByHash(w http.ResponseWriter, r *http.Request, for
return return
} }
bodies, timestamps := rt.backend.BodiesByHash(req.BlockHashes) 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=. // handleBodiesByRange implements GET /engine/v2/{fork}/bodies?from=&count=.
func (rt *Router) handleBodiesByRange(w http.ResponseWriter, r *http.Request, fork forks.Fork) { func (rt *Router) handleBodiesByRange(w http.ResponseWriter, r *http.Request, fork forks.Fork) {
if fork != forks.Amsterdam { sf, ok := resolveFork(w, fork, forks.Paris)
writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") if !ok {
return return
} }
q := r.URL.Query() q := r.URL.Query()
@ -65,43 +66,49 @@ func (rt *Router) handleBodiesByRange(w http.ResponseWriter, r *http.Request, fo
return return
} }
bodies, timestamps := rt.backend.BodiesByRange(from, count) 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 // buildBodiesResponse assembles a BodiesResponse for the given fork, marking
// out-of-era blocks as available=false per the URL fork window. // out-of-era blocks as available=false per the URL fork window. The body shape
func buildBodiesResponseAmsterdam(b Backend, fork forks.Fork, bodies []*types.Body, ts []uint64) *sszt.BodiesResponseAmsterdam { // is fork-driven by the codec; bodyToSSZ populates the superset and the codec
out := &sszt.BodiesResponseAmsterdam{ // emits only the fork's active fields.
Entries: make([]*sszt.BodyEntryAmsterdam, len(bodies)), 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 { 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 { if body != nil && b.ForkFromTimestamp(ts[i]) == fork {
entry.Available = true entry.Available = true
entry.Body = bodyToAmsterdamSSZ(body) entry.Body = bodyToSSZ(body, sf)
} }
out.Entries[i] = entry out.Entries[i] = entry
} }
return out return out
} }
// bodyToAmsterdamSSZ flattens a *types.Body into the Amsterdam body shape. // bodyToSSZ flattens a *types.Body into the monolithic body shape. Withdrawals
func bodyToAmsterdamSSZ(body *types.Body) *sszt.ExecutionPayloadBodyAmsterdam { // are only attached from Shanghai on, matching the fork's wire shape.
out := &sszt.ExecutionPayloadBodyAmsterdam{ func bodyToSSZ(body *types.Body, sf ssz.Fork) *sszt.ExecutionPayloadBody {
out := &sszt.ExecutionPayloadBody{
Transactions: make([][]byte, len(body.Transactions)), Transactions: make([][]byte, len(body.Transactions)),
Withdrawals: make([]*sszt.Withdrawal, len(body.Withdrawals)),
} }
for i, tx := range body.Transactions { for i, tx := range body.Transactions {
out.Transactions[i], _ = tx.MarshalBinary() out.Transactions[i], _ = tx.MarshalBinary()
} }
for i, w := range body.Withdrawals { if sf >= ssz.ForkShapella {
out.Withdrawals[i] = &sszt.Withdrawal{ out.Withdrawals = make([]*sszt.Withdrawal, len(body.Withdrawals))
Index: w.Index, for i, w := range body.Withdrawals {
ValidatorIndex: w.Validator, out.Withdrawals[i] = &sszt.Withdrawal{
Address: w.Address, Index: w.Index,
Amount: w.Amount, 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 return out
} }

View file

@ -26,12 +26,14 @@ import (
// handleForkchoice implements POST /engine/v2/{fork}/forkchoice. // handleForkchoice implements POST /engine/v2/{fork}/forkchoice.
func (rt *Router) handleForkchoice(w http.ResponseWriter, r *http.Request, fork forks.Fork) { func (rt *Router) handleForkchoice(w http.ResponseWriter, r *http.Request, fork forks.Fork) {
if fork != forks.Amsterdam { // The execution-apis spec currently only defines the forkchoice envelope
writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") // for Amsterdam; the inner attributes shape is fork-driven by the codec.
sf, ok := resolveFork(w, fork, forks.Amsterdam)
if !ok {
return return
} }
fcu := new(sszt.ForkchoiceUpdateAmsterdam) fcu := new(sszt.ForkchoiceUpdateAmsterdam)
if !readSSZRequest(w, r, fcu, maxPayloadBytes) { if !readSSZRequest(w, r, fcu, sf, maxPayloadBytes) {
return return
} }
if err := fcu.Validate(); err != nil { 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) state := sszt.ForkchoiceStateToV1(fcu.ForkchoiceState)
var attrs *engine.PayloadAttributes var attrs *engine.PayloadAttributes
if len(fcu.PayloadAttributes) == 1 { 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 // If PayloadAttributes is present the URL fork MUST match the fork
// the new payload would belong to. Today only Amsterdam URL exists // the new payload would belong to. Today only Amsterdam URL exists
// in this implementation so the timestamp check is implicit; we // in this implementation so the timestamp check is implicit; we
// keep an explicit guard for future fork URLs. // 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, writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork,
"payload_attributes timestamp does not match URL fork") "payload_attributes timestamp does not match URL fork")
return return
} }
attrs = sszt.PayloadAttributesAmsterdamToEngine(ssz) attrs = sszt.PayloadAttributesToEngine(attr)
// target_gas_limit and custody_columns are not yet plumbed into // target_gas_limit and custody_columns are not yet plumbed into
// the JSON-RPC engine API. Custody is parsed-but-stubbed per // the JSON-RPC engine API. Custody is parsed-but-stubbed per
// agreed scope; target_gas_limit will be picked up when the // 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 { if resp.PayloadID != nil {
out.PayloadID = [][8]byte{[8]byte(*resp.PayloadID)} out.PayloadID = [][8]byte{[8]byte(*resp.PayloadID)}
} }
writeSSZResponse(w, out) writeSSZResponse(w, out, sf)
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -24,12 +24,13 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/params/forks" "github.com/ethereum/go-ethereum/params/forks"
"github.com/holiman/uint256" "github.com/holiman/uint256"
"github.com/karalabe/ssz"
) )
// handleGetPayload implements GET /engine/v2/{fork}/payloads/{payloadId}. // handleGetPayload implements GET /engine/v2/{fork}/payloads/{payloadId}.
func (rt *Router) handleGetPayload(w http.ResponseWriter, r *http.Request, fork forks.Fork, idHex string) { func (rt *Router) handleGetPayload(w http.ResponseWriter, r *http.Request, fork forks.Fork, idHex string) {
if fork != forks.Amsterdam { sf, ok := resolveFork(w, fork, forks.Amsterdam)
writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") if !ok {
return return
} }
raw, err := hexutil.Decode(idHex) raw, err := hexutil.Decode(idHex)
@ -46,17 +47,17 @@ func (rt *Router) handleGetPayload(w http.ResponseWriter, r *http.Request, fork
return return
} }
out := buildBuiltPayloadAmsterdam(env) out := buildBuiltPayloadAmsterdam(env, sf)
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Cache-Control", "no-store")
writeSSZResponse(w, out) writeSSZResponse(w, out, sf)
} }
// buildBuiltPayloadAmsterdam packages an engine.ExecutionPayloadEnvelope into // buildBuiltPayloadAmsterdam packages an engine.ExecutionPayloadEnvelope into
// the SSZ BuiltPayload shape. BlockValue/BlobsBundle/Requests come straight // the SSZ BuiltPayload shape. BlockValue/BlobsBundle/Requests come straight
// across; the inner payload goes through the SSZ converter. // 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{ out := &sszt.BuiltPayloadAmsterdam{
Payload: sszt.ExecutionPayloadAmsterdamFromEngine(env.ExecutionPayload), Payload: sszt.ExecutionPayloadFromEngine(env.ExecutionPayload, sf),
BlockValue: new(uint256.Int), BlockValue: new(uint256.Int),
ExecutionRequests: env.Requests, ExecutionRequests: env.Requests,
ShouldOverrideBuilder: env.Override, ShouldOverrideBuilder: env.Override,

View file

@ -34,9 +34,10 @@ const (
) )
// readSSZRequest enforces the SSZ content-type and Content-Length cap, then // readSSZRequest enforces the SSZ content-type and Content-Length cap, then
// decodes the request body into obj. Writes the appropriate problem response // decodes the request body into obj for the given fork. Writes the appropriate
// on failure and returns false; the caller should return immediately. // problem response on failure and returns false; the caller should return
func readSSZRequest(w http.ResponseWriter, r *http.Request, obj ssz.Object, max int64) bool { // 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) { if !strings.EqualFold(r.Header.Get("Content-Type"), sszContentType) {
writeProblem(w, http.StatusUnsupportedMediaType, ErrUnsupportedMedia, "expected "+sszContentType) writeProblem(w, http.StatusUnsupportedMediaType, ErrUnsupportedMedia, "expected "+sszContentType)
return false return false
@ -54,18 +55,18 @@ func readSSZRequest(w http.ResponseWriter, r *http.Request, obj ssz.Object, max
writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "") writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "")
return false return false
} }
if err := ssz.DecodeFromBytes(body, obj); err != nil { if err := ssz.DecodeFromBytesOnFork(body, obj, fork); err != nil {
writeProblem(w, http.StatusBadRequest, ErrSSZDecode, "") writeProblem(w, http.StatusBadRequest, ErrSSZDecode, "")
return false return false
} }
return true return true
} }
// writeSSZResponse encodes obj into the response body. On encode failure it // writeSSZResponse encodes obj into the response body for the given fork. On
// writes an internal-error problem and returns false. // encode failure it writes an internal-error problem and returns false.
func writeSSZResponse(w http.ResponseWriter, obj ssz.Object) bool { func writeSSZResponse(w http.ResponseWriter, obj ssz.Object, fork ssz.Fork) bool {
buf := make([]byte, ssz.Size(obj)) buf := make([]byte, ssz.SizeOnFork(obj, fork))
if err := ssz.EncodeToBytes(buf, obj); err != nil { if err := ssz.EncodeToBytesOnFork(buf, obj, fork); err != nil {
writeProblem(w, http.StatusInternalServerError, ErrInternal, err.Error()) writeProblem(w, http.StatusInternalServerError, ErrInternal, err.Error())
return false return false
} }

View file

@ -27,15 +27,21 @@ import (
// handleNewPayload implements POST /engine/v2/{fork}/payloads. // handleNewPayload implements POST /engine/v2/{fork}/payloads.
func (rt *Router) handleNewPayload(w http.ResponseWriter, r *http.Request, fork forks.Fork) { func (rt *Router) handleNewPayload(w http.ResponseWriter, r *http.Request, fork forks.Fork) {
if fork != forks.Amsterdam { // The execution-apis spec currently only defines the payload envelope for
writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "") // Amsterdam; the inner payload shape is fork-driven by the codec.
sf, ok := resolveFork(w, fork, forks.Amsterdam)
if !ok {
return return
} }
env := new(sszt.ExecutionPayloadEnvelopeAmsterdam) env := new(sszt.ExecutionPayloadEnvelopeAmsterdam)
if !readSSZRequest(w, r, env, maxPayloadBytes) { if !readSSZRequest(w, r, env, sf, maxPayloadBytes) {
return 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 // The spec drops expectedBlobVersionedHashes; we recompute them from the
// payload's transactions before passing to the EL. // 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) mapBackendErr(w, err)
return return
} }
writeSSZResponse(w, sszt.PayloadStatusFromV1(&status)) writeSSZResponse(w, sszt.PayloadStatusFromV1(&status), sf)
} }
// versionedHashesFromTxs extracts the blob versioned-hash list from the // versionedHashesFromTxs extracts the blob versioned-hash list from the

View file

@ -103,10 +103,10 @@ func newTestServer(t *testing.T, b Backend) *httptest.Server {
return httptest.NewServer(http.StripPrefix(BasePath, NewRouter(b))) 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() t.Helper()
buf := make([]byte, ssz.Size(body)) buf := make([]byte, ssz.SizeOnFork(body, fork))
if err := ssz.EncodeToBytes(buf, body); err != nil { if err := ssz.EncodeToBytesOnFork(buf, body, fork); err != nil {
t.Fatalf("encode: %v", err) t.Fatalf("encode: %v", err)
} }
req, _ := http.NewRequest(http.MethodPost, srv.URL+BasePath+path, bytes.NewReader(buf)) 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 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() t.Helper()
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
t.Fatalf("read body: %v", err) 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) t.Fatalf("decode response: %v\nbody: %x", err, body)
} }
} }
@ -139,19 +139,24 @@ func TestRouterNewPayload(t *testing.T) {
srv := newTestServer(t, b) srv := newTestServer(t, b)
defer srv.Close() defer srv.Close()
amsterdam, _ := sszt.ForkFor(forks.Amsterdam)
blob, excess, slot := uint64(0), uint64(0), uint64(0)
env := &sszt.ExecutionPayloadEnvelopeAmsterdam{ env := &sszt.ExecutionPayloadEnvelopeAmsterdam{
Payload: &sszt.ExecutionPayloadAmsterdam{ Payload: &sszt.ExecutionPayload{
BaseFeePerGas: uint256.NewInt(7e9), BaseFeePerGas: uint256.NewInt(7e9),
LogsBloom: [256]byte{}, LogsBloom: [256]byte{},
BlobGasUsed: &blob,
ExcessBlobGas: &excess,
SlotNumber: &slot,
}, },
ParentBeaconBlockRoot: common.Hash{0x55}, ParentBeaconBlockRoot: common.Hash{0x55},
} }
resp := sszPost(t, srv, "/amsterdam/payloads", env) resp := sszPost(t, srv, "/amsterdam/payloads", env, amsterdam)
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
t.Fatalf("want 200, got %d", resp.StatusCode) t.Fatalf("want 200, got %d", resp.StatusCode)
} }
got := new(sszt.PayloadStatus) got := new(sszt.PayloadStatus)
decodeSSZ(t, resp, got) decodeSSZ(t, resp, got, amsterdam)
if got.Status != sszt.StatusValid { if got.Status != sszt.StatusValid {
t.Errorf("status=%d want VALID(%d)", 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) srv := newTestServer(t, b)
defer srv.Close() defer srv.Close()
amsterdam, _ := sszt.ForkFor(forks.Amsterdam)
fcu := &sszt.ForkchoiceUpdateAmsterdam{ fcu := &sszt.ForkchoiceUpdateAmsterdam{
ForkchoiceState: &sszt.ForkchoiceState{HeadBlockHash: common.Hash{0xaa}}, 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 { if resp.StatusCode != 200 {
t.Fatalf("want 200, got %d", resp.StatusCode) t.Fatalf("want 200, got %d", resp.StatusCode)
} }
got := new(sszt.ForkchoiceUpdateResponseAmsterdam) got := new(sszt.ForkchoiceUpdateResponseAmsterdam)
decodeSSZ(t, resp, got) decodeSSZ(t, resp, got, amsterdam)
if got.PayloadStatus == nil || got.PayloadStatus.Status != sszt.StatusValid { if got.PayloadStatus == nil || got.PayloadStatus.Status != sszt.StatusValid {
t.Errorf("status: %#v", got.PayloadStatus) t.Errorf("status: %#v", got.PayloadStatus)
} }
@ -310,7 +316,7 @@ func TestRouterBlobsV2AllOrNothing(t *testing.T) {
srv := newTestServer(t, b) srv := newTestServer(t, b)
defer srv.Close() defer srv.Close()
req := &sszt.BlobsVersionedHashesRequest{VersionedHashes: []common.Hash{{0x01}}} 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() defer resp.Body.Close()
if resp.StatusCode != 204 { if resp.StatusCode != 204 {
t.Fatalf("want 204, got %d", resp.StatusCode) t.Fatalf("want 204, got %d", resp.StatusCode)
@ -322,12 +328,12 @@ func TestRouterBlobsV3PartialOK(t *testing.T) {
srv := newTestServer(t, b) srv := newTestServer(t, b)
defer srv.Close() defer srv.Close()
req := &sszt.BlobsVersionedHashesRequest{VersionedHashes: []common.Hash{{0x01}, {0x02}}} 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 { if resp.StatusCode != 200 {
t.Fatalf("want 200, got %d", resp.StatusCode) t.Fatalf("want 200, got %d", resp.StatusCode)
} }
got := new(sszt.BlobsV2Response) got := new(sszt.BlobsV2Response)
decodeSSZ(t, resp, got) decodeSSZ(t, resp, got, ssz.ForkUnknown)
if len(got.Entries) != 2 { if len(got.Entries) != 2 {
t.Fatalf("entries=%d", len(got.Entries)) t.Fatalf("entries=%d", len(got.Entries))
} }