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

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/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
}

View file

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

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

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"
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 {

View file

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

View file

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

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/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,

View file

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

View file

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

View file

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