eth/catalyst: added rest-ssz engine api

This commit is contained in:
Marius van der Wijden 2026-06-08 17:11:08 +02:00
parent 1149f76dca
commit b694357889
28 changed files with 3013 additions and 3 deletions

View file

@ -0,0 +1,136 @@
// 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)
}

238
beacon/engine/ssz/blobs.go Normal file
View file

@ -0,0 +1,238 @@
// 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"
)
// BlobsVersionedHashesRequest is the SSZ body of POST /blobs/v{1,2,3}.
type BlobsVersionedHashesRequest struct {
VersionedHashes []common.Hash
}
func (r *BlobsVersionedHashesRequest) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
size := uint32(4)
if fixed {
return size
}
size += ssz.SizeSliceOfStaticBytes(siz, r.VersionedHashes)
return size
}
func (r *BlobsVersionedHashesRequest) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfStaticBytesOffset(c, &r.VersionedHashes, MaxBlobsRequest)
ssz.DefineSliceOfStaticBytesContent(c, &r.VersionedHashes, MaxBlobsRequest)
}
// BlobAndProofV1 — Cancun whole-blob with one KZG proof.
type BlobAndProofV1 struct {
Blob *Blob
Proof [48]byte
}
func (*BlobAndProofV1) SizeSSZ(*ssz.Sizer) uint32 { return BytesPerBlob + 48 }
func (e *BlobAndProofV1) DefineSSZ(c *ssz.Codec) {
ssz.DefineStaticObject(c, &e.Blob)
ssz.DefineStaticBytes(c, &e.Proof)
}
// BlobV1Entry pairs an availability flag with the contents.
type BlobV1Entry struct {
Available bool
Contents *BlobAndProofV1
}
func (*BlobV1Entry) SizeSSZ(*ssz.Sizer) uint32 { return 1 + BytesPerBlob + 48 }
func (e *BlobV1Entry) DefineSSZ(c *ssz.Codec) {
ssz.DefineBool(c, &e.Available)
ssz.DefineStaticObject(c, &e.Contents)
}
// BlobsV1Response is the SSZ response of POST /blobs/v1.
type BlobsV1Response struct {
Entries []*BlobV1Entry
}
func (r *BlobsV1Response) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
size := uint32(4)
if fixed {
return size
}
size += ssz.SizeSliceOfStaticObjects(siz, r.Entries)
return size
}
func (r *BlobsV1Response) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfStaticObjectsOffset(c, &r.Entries, MaxBlobsRequest)
ssz.DefineSliceOfStaticObjectsContent(c, &r.Entries, MaxBlobsRequest)
}
// BlobAndProofV2 — Osaka whole-blob with CELLS_PER_EXT_BLOB cell proofs.
// proofs is a List with maxItems=128, so it is dynamic.
type BlobAndProofV2 struct {
Blob *Blob
Proofs [][48]byte
}
func (b *BlobAndProofV2) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
// blob (static) + offset(proofs)
size := uint32(BytesPerBlob + 4)
if fixed {
return size
}
size += ssz.SizeSliceOfStaticBytes(siz, b.Proofs)
return size
}
func (b *BlobAndProofV2) DefineSSZ(c *ssz.Codec) {
ssz.DefineStaticObject(c, &b.Blob)
ssz.DefineSliceOfStaticBytesOffset(c, &b.Proofs, CellsPerExtBlob)
ssz.DefineSliceOfStaticBytesContent(c, &b.Proofs, CellsPerExtBlob)
}
// BlobV2Entry is shared by /blobs/v2 and /blobs/v3 (V3 just enables
// per-entry available=false reporting).
type BlobV2Entry struct {
Available bool
Contents *BlobAndProofV2
}
func (e *BlobV2Entry) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
size := uint32(1 + 4) // bool + offset(contents)
if fixed {
return size
}
size += ssz.SizeDynamicObject(siz, e.Contents)
return size
}
func (e *BlobV2Entry) DefineSSZ(c *ssz.Codec) {
ssz.DefineBool(c, &e.Available)
ssz.DefineDynamicObjectOffset(c, &e.Contents)
ssz.DefineDynamicObjectContent(c, &e.Contents)
}
type BlobsV2Response struct {
Entries []*BlobV2Entry
}
func (r *BlobsV2Response) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
size := uint32(4)
if fixed {
return size
}
size += ssz.SizeSliceOfDynamicObjects(siz, r.Entries)
return size
}
func (r *BlobsV2Response) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfDynamicObjectsOffset(c, &r.Entries, MaxBlobsRequest)
ssz.DefineSliceOfDynamicObjectsContent(c, &r.Entries, MaxBlobsRequest)
}
// BlobsV4Request is the SSZ body of POST /blobs/v4 (cell-range selection).
type BlobsV4Request struct {
VersionedHashes []common.Hash
IndicesBitarray *Bitvector128
}
func (r *BlobsV4Request) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
// offset(hashes) + 16(bitvector)
size := uint32(4 + CellsPerExtBlob/8)
if fixed {
return size
}
size += ssz.SizeSliceOfStaticBytes(siz, r.VersionedHashes)
return size
}
func (r *BlobsV4Request) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfStaticBytesOffset(c, &r.VersionedHashes, MaxBlobsRequest)
ssz.DefineStaticObject(c, &r.IndicesBitarray)
ssz.DefineSliceOfStaticBytesContent(c, &r.VersionedHashes, MaxBlobsRequest)
}
// BlobCellsAndProofs — Amsterdam cell-range response payload.
type BlobCellsAndProofs struct {
BlobCells []*OptionalBlobCell // List[Optional[ByteVector[BYTES_PER_CELL]], CELLS_PER_EXT_BLOB]
Proofs []*OptionalProof // List[Optional[Bytes48], CELLS_PER_EXT_BLOB]
}
func (b *BlobCellsAndProofs) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
size := uint32(8) // 2 offsets
if fixed {
return size
}
size += ssz.SizeSliceOfDynamicObjects(siz, b.BlobCells)
size += ssz.SizeSliceOfDynamicObjects(siz, b.Proofs)
return size
}
func (b *BlobCellsAndProofs) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfDynamicObjectsOffset(c, &b.BlobCells, CellsPerExtBlob)
ssz.DefineSliceOfDynamicObjectsOffset(c, &b.Proofs, CellsPerExtBlob)
ssz.DefineSliceOfDynamicObjectsContent(c, &b.BlobCells, CellsPerExtBlob)
ssz.DefineSliceOfDynamicObjectsContent(c, &b.Proofs, CellsPerExtBlob)
}
// BlobV4Entry pairs availability with the cell-range contents.
type BlobV4Entry struct {
Available bool
Contents *BlobCellsAndProofs
}
func (e *BlobV4Entry) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
size := uint32(1 + 4)
if fixed {
return size
}
size += ssz.SizeDynamicObject(siz, e.Contents)
return size
}
func (e *BlobV4Entry) DefineSSZ(c *ssz.Codec) {
ssz.DefineBool(c, &e.Available)
ssz.DefineDynamicObjectOffset(c, &e.Contents)
ssz.DefineDynamicObjectContent(c, &e.Contents)
}
type BlobsV4Response struct {
Entries []*BlobV4Entry
}
func (r *BlobsV4Response) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
size := uint32(4)
if fixed {
return size
}
size += ssz.SizeSliceOfDynamicObjects(siz, r.Entries)
return size
}
func (r *BlobsV4Response) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfDynamicObjectsOffset(c, &r.Entries, MaxBlobsRequest)
ssz.DefineSliceOfDynamicObjectsContent(c, &r.Entries, MaxBlobsRequest)
}

162
beacon/engine/ssz/bodies.go Normal file
View file

@ -0,0 +1,162 @@
// 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"
)
// BodiesByHashRequest is the SSZ body of POST /{fork}/bodies/hash.
type BodiesByHashRequest struct {
BlockHashes []common.Hash
}
func (r *BodiesByHashRequest) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
size := uint32(4)
if fixed {
return size
}
size += ssz.SizeSliceOfStaticBytes(siz, r.BlockHashes)
return size
}
func (r *BodiesByHashRequest) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfStaticBytesOffset(c, &r.BlockHashes, MaxBodiesRequest)
ssz.DefineSliceOfStaticBytesContent(c, &r.BlockHashes, MaxBodiesRequest)
}
// BodyEntryAmsterdam is the per-block entry returned by /amsterdam/bodies/...
type BodyEntryAmsterdam struct {
Available bool
Body *ExecutionPayloadBodyAmsterdam
}
func (e *BodyEntryAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
size := uint32(1 + 4) // bool + offset
if fixed {
return size
}
size += ssz.SizeDynamicObject(siz, e.Body)
return size
}
func (e *BodyEntryAmsterdam) 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
}
func (r *BodiesResponseAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
size := uint32(4)
if fixed {
return size
}
size += ssz.SizeSliceOfDynamicObjects(siz, r.Entries)
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) {
ssz.DefineSliceOfDynamicObjectsOffset(c, &r.Entries, MaxBodiesRequest)
ssz.DefineSliceOfDynamicObjectsContent(c, &r.Entries, MaxBodiesRequest)
}

View file

@ -0,0 +1,50 @@
// 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 contains the SSZ wire types for the REST-SSZ Engine API
// (execution-apis PR #793). The containers here are intentionally separate
// from beacon/engine.ExecutableData & friends; they map onto the JSON-RPC
// types via convert.go.
package ssz
// MAX_* constants from refactor-ssz.md.
const (
MaxBytesPerTx = 1 << 30 // 1 GiB, EIP-4844
MaxTxsPerPayload = 1 << 20 // 1,048,576, Bellatrix
MaxWithdrawalsPerPayload = 16 // Capella
MaxExtraDataBytes = 32 // Bellatrix
MaxBlobCommitmentsPerBlock = 1 << 12 // 4,096, Deneb
FieldElementsPerBlob = 4096 // EIP-4844
BytesPerFieldElement = 32 // EIP-4844
BytesPerBlob = FieldElementsPerBlob * BytesPerFieldElement
CellsPerExtBlob = 128 // EIP-7594
BytesPerCell = BytesPerBlob / CellsPerExtBlob
MaxBalBytes = MaxBytesPerTx // EIP-7928 placeholder
MaxExecutionRequestsPerPayload = 1 << 8 // 256, EIP-7685
MaxBytesPerExecutionRequest = MaxBytesPerTx // placeholder
MaxVersionedHashesPerRequest = 128 // Osaka
MaxBlobsRequest = MaxVersionedHashesPerRequest
MaxBodiesRequest = 128 // see spec open question; capabilities example uses 128
MaxErrorBytes = 1024
)
// Status enum values for PayloadStatus.
const (
StatusValid uint8 = 0
StatusInvalid uint8 = 1
StatusSyncing uint8 = 2
StatusAccepted uint8 = 3
)

View file

@ -0,0 +1,236 @@
// 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/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/holiman/uint256"
)
// statusToEnum maps the JSON-RPC PayloadStatusV1.Status string to the SSZ
// uint8. ACCEPTED is only valid on /payloads responses; the caller is
// responsible for downstream restrictions.
func statusToEnum(s string) uint8 {
switch s {
case engine.VALID:
return StatusValid
case engine.INVALID:
return StatusInvalid
case engine.SYNCING:
return StatusSyncing
case engine.ACCEPTED:
return StatusAccepted
}
return StatusInvalid
}
// statusFromEnum is the inverse of statusToEnum.
func statusFromEnum(s uint8) string {
switch s {
case StatusValid:
return engine.VALID
case StatusInvalid:
return engine.INVALID
case StatusSyncing:
return engine.SYNCING
case StatusAccepted:
return engine.ACCEPTED
}
return engine.INVALID
}
// PayloadStatusFromV1 converts the JSON-RPC PayloadStatusV1 into its SSZ wire form.
func PayloadStatusFromV1(s *engine.PayloadStatusV1) *PayloadStatus {
out := &PayloadStatus{Status: statusToEnum(s.Status)}
if s.LatestValidHash != nil {
out.LatestValidHash = []common.Hash{*s.LatestValidHash}
}
if s.ValidationError != nil {
out.ValidationError = [][]byte{[]byte(*s.ValidationError)}
}
return out
}
// PayloadStatusToV1 is the inverse of PayloadStatusFromV1.
func PayloadStatusToV1(s *PayloadStatus) engine.PayloadStatusV1 {
out := engine.PayloadStatusV1{Status: statusFromEnum(s.Status)}
if len(s.LatestValidHash) == 1 {
h := s.LatestValidHash[0]
out.LatestValidHash = &h
}
if len(s.ValidationError) == 1 {
msg := string(s.ValidationError[0])
out.ValidationError = &msg
}
return out
}
// ForkchoiceStateFromV1 converts ForkchoiceStateV1 to its SSZ form.
func ForkchoiceStateFromV1(s engine.ForkchoiceStateV1) *ForkchoiceState {
return &ForkchoiceState{
HeadBlockHash: s.HeadBlockHash,
SafeBlockHash: s.SafeBlockHash,
FinalizedBlockHash: s.FinalizedBlockHash,
}
}
// ForkchoiceStateToV1 is the inverse of ForkchoiceStateFromV1.
func ForkchoiceStateToV1(s *ForkchoiceState) engine.ForkchoiceStateV1 {
return engine.ForkchoiceStateV1{
HeadBlockHash: s.HeadBlockHash,
SafeBlockHash: s.SafeBlockHash,
FinalizedBlockHash: s.FinalizedBlockHash,
}
}
// withdrawalsFromTypes converts geth Withdrawals into SSZ Withdrawals.
func withdrawalsFromTypes(ws []*types.Withdrawal) []*Withdrawal {
out := make([]*Withdrawal, len(ws))
for i, w := range ws {
out[i] = &Withdrawal{
Index: w.Index,
ValidatorIndex: w.Validator,
Address: w.Address,
Amount: w.Amount,
}
}
return out
}
// withdrawalsToTypes is the inverse of withdrawalsFromTypes.
func withdrawalsToTypes(ws []*Withdrawal) []*types.Withdrawal {
out := make([]*types.Withdrawal, len(ws))
for i, w := range ws {
out[i] = &types.Withdrawal{
Index: w.Index,
Validator: w.ValidatorIndex,
Address: w.Address,
Amount: w.Amount,
}
}
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{
Timestamp: a.Timestamp,
PrevRandao: a.Random,
SuggestedFeeRecipient: a.SuggestedFeeRecipient,
Withdrawals: withdrawalsFromTypes(a.Withdrawals),
}
if a.BeaconRoot != nil {
out.ParentBeaconBlockRoot = *a.BeaconRoot
}
if a.SlotNumber != nil {
out.SlotNumber = *a.SlotNumber
}
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{
Timestamp: a.Timestamp,
Random: a.PrevRandao,
SuggestedFeeRecipient: a.SuggestedFeeRecipient,
Withdrawals: withdrawalsToTypes(a.Withdrawals),
BeaconRoot: &root,
SlotNumber: &slot,
}
}
// 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 {
var bloom [256]byte
copy(bloom[:], d.LogsBloom)
var fee *uint256.Int
if d.BaseFeePerGas != nil {
fee = new(uint256.Int)
fee.SetFromBig(d.BaseFeePerGas)
}
out := &ExecutionPayloadAmsterdam{
ParentHash: d.ParentHash,
FeeRecipient: d.FeeRecipient,
StateRoot: d.StateRoot,
ReceiptsRoot: d.ReceiptsRoot,
LogsBloom: bloom,
PrevRandao: d.Random,
BlockNumber: d.Number,
GasLimit: d.GasLimit,
GasUsed: d.GasUsed,
Timestamp: d.Timestamp,
ExtraData: append([]byte(nil), d.ExtraData...),
BaseFeePerGas: fee,
BlockHash: d.BlockHash,
Transactions: d.Transactions,
Withdrawals: withdrawalsFromTypes(d.Withdrawals),
}
if d.BlobGasUsed != nil {
out.BlobGasUsed = *d.BlobGasUsed
}
if d.ExcessBlobGas != nil {
out.ExcessBlobGas = *d.ExcessBlobGas
}
if d.SlotNumber != nil {
out.SlotNumber = *d.SlotNumber
}
// 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 {
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,
}
if p.BaseFeePerGas != nil {
out.BaseFeePerGas = p.BaseFeePerGas.ToBig()
}
return out
}

View file

@ -0,0 +1,189 @@
// 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"
)
// ExecutionPayloadEnvelopeAmsterdam is the request body of
// POST /amsterdam/payloads.
type ExecutionPayloadEnvelopeAmsterdam struct {
Payload *ExecutionPayloadAmsterdam
ParentBeaconBlockRoot common.Hash
ExecutionRequests [][]byte
}
func (e *ExecutionPayloadEnvelopeAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
// offset(payload) + 32(root) + offset(requests)
size := uint32(40)
if fixed {
return size
}
size += ssz.SizeDynamicObject(siz, e.Payload)
size += ssz.SizeSliceOfDynamicBytes(siz, e.ExecutionRequests)
return size
}
func (e *ExecutionPayloadEnvelopeAmsterdam) DefineSSZ(c *ssz.Codec) {
ssz.DefineDynamicObjectOffset(c, &e.Payload)
ssz.DefineStaticBytes(c, &e.ParentBeaconBlockRoot)
ssz.DefineSliceOfDynamicBytesOffset(c, &e.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest)
ssz.DefineDynamicObjectContent(c, &e.Payload)
ssz.DefineSliceOfDynamicBytesContent(c, &e.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest)
}
// BlobsBundleV2 is the cell-proof blob bundle carried by BuiltPayload
// from Osaka onwards.
type BlobsBundleV2 struct {
Commitments [][48]byte
Proofs [][48]byte
Blobs []*Blob
}
func (b *BlobsBundleV2) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
// 3 offsets
size := uint32(12)
if fixed {
return size
}
size += ssz.SizeSliceOfStaticBytes(siz, b.Commitments)
size += ssz.SizeSliceOfStaticBytes(siz, b.Proofs)
size += ssz.SizeSliceOfStaticObjects(siz, b.Blobs)
return size
}
func (b *BlobsBundleV2) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfStaticBytesOffset(c, &b.Commitments, MaxBlobCommitmentsPerBlock)
ssz.DefineSliceOfStaticBytesOffset(c, &b.Proofs, MaxBlobCommitmentsPerBlock*CellsPerExtBlob)
ssz.DefineSliceOfStaticObjectsOffset(c, &b.Blobs, MaxBlobCommitmentsPerBlock)
ssz.DefineSliceOfStaticBytesContent(c, &b.Commitments, MaxBlobCommitmentsPerBlock)
ssz.DefineSliceOfStaticBytesContent(c, &b.Proofs, MaxBlobCommitmentsPerBlock*CellsPerExtBlob)
ssz.DefineSliceOfStaticObjectsContent(c, &b.Blobs, MaxBlobCommitmentsPerBlock)
}
// BuiltPayloadAmsterdam is the response of GET /amsterdam/payloads/{id}.
type BuiltPayloadAmsterdam struct {
Payload *ExecutionPayloadAmsterdam
BlockValue *uint256.Int
BlobsBundle *BlobsBundleV2
ExecutionRequests [][]byte
ShouldOverrideBuilder bool
}
func (p *BuiltPayloadAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
// offset(payload) + 32(value) + offset(bundle) + offset(requests) + 1(override)
size := uint32(4 + 32 + 4 + 4 + 1)
if fixed {
return size
}
size += ssz.SizeDynamicObject(siz, p.Payload)
size += ssz.SizeDynamicObject(siz, p.BlobsBundle)
size += ssz.SizeSliceOfDynamicBytes(siz, p.ExecutionRequests)
return size
}
func (p *BuiltPayloadAmsterdam) DefineSSZ(c *ssz.Codec) {
ssz.DefineDynamicObjectOffset(c, &p.Payload)
ssz.DefineUint256(c, &p.BlockValue)
ssz.DefineDynamicObjectOffset(c, &p.BlobsBundle)
ssz.DefineSliceOfDynamicBytesOffset(c, &p.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest)
ssz.DefineBool(c, &p.ShouldOverrideBuilder)
ssz.DefineDynamicObjectContent(c, &p.Payload)
ssz.DefineDynamicObjectContent(c, &p.BlobsBundle)
ssz.DefineSliceOfDynamicBytesContent(c, &p.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest)
}
// ForkchoiceUpdateAmsterdam is the request body of POST /amsterdam/forkchoice.
//
// PayloadAttributes is modelled as Optional[PayloadAttributesAmsterdam] = a slice
// of length 0 or 1. CustodyColumns is Optional[Bitvector[128]] — a slice of
// length 0 or 1 of the 16-byte Bitvector wrapper.
type ForkchoiceUpdateAmsterdam struct {
ForkchoiceState *ForkchoiceState
PayloadAttributes []*PayloadAttributesAmsterdam
CustodyColumns []*Bitvector128
}
func (f *ForkchoiceUpdateAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
// 96 (state) + 4(offset attrs) + 4(offset custody)
size := uint32(96 + 8)
if fixed {
return size
}
size += ssz.SizeSliceOfDynamicObjects(siz, f.PayloadAttributes)
size += ssz.SizeSliceOfStaticObjects(siz, f.CustodyColumns)
return size
}
func (f *ForkchoiceUpdateAmsterdam) DefineSSZ(c *ssz.Codec) {
ssz.DefineStaticObject(c, &f.ForkchoiceState)
ssz.DefineSliceOfDynamicObjectsOffset(c, &f.PayloadAttributes, 1)
ssz.DefineSliceOfStaticObjectsOffset(c, &f.CustodyColumns, 1)
ssz.DefineSliceOfDynamicObjectsContent(c, &f.PayloadAttributes, 1)
ssz.DefineSliceOfStaticObjectsContent(c, &f.CustodyColumns, 1)
}
// Validate enforces the Optional[T] = List[T,1] length invariants.
func (f *ForkchoiceUpdateAmsterdam) Validate() error {
if err := checkOptional(f.PayloadAttributes); err != nil {
return err
}
return checkOptional(f.CustodyColumns)
}
// ForkchoiceUpdateResponseAmsterdam is the response of POST /amsterdam/forkchoice.
type ForkchoiceUpdateResponseAmsterdam struct {
PayloadStatus *PayloadStatus
PayloadID [][8]byte // Optional[Bytes8]
}
func (r *ForkchoiceUpdateResponseAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
// offset(status) + offset(id)
size := uint32(8)
if fixed {
return size
}
size += ssz.SizeDynamicObject(siz, r.PayloadStatus)
size += ssz.SizeSliceOfStaticBytes(siz, r.PayloadID)
return size
}
func (r *ForkchoiceUpdateResponseAmsterdam) DefineSSZ(c *ssz.Codec) {
ssz.DefineDynamicObjectOffset(c, &r.PayloadStatus)
ssz.DefineSliceOfStaticBytesOffset(c, &r.PayloadID, 1)
ssz.DefineDynamicObjectContent(c, &r.PayloadStatus)
ssz.DefineSliceOfStaticBytesContent(c, &r.PayloadID, 1)
}
// Validate enforces optional-list invariants and inner status invariants.
func (r *ForkchoiceUpdateResponseAmsterdam) Validate() error {
if err := checkOptional(r.PayloadID); err != nil {
return err
}
if r.PayloadStatus == nil {
return nil
}
return r.PayloadStatus.Validate()
}

View file

@ -0,0 +1,36 @@
// 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 "errors"
// The spec models `Optional[T]` as `List[T, 1]`: 0 elements = absent,
// 1 element = present. We mirror that on the Go side by representing
// optional fields as length-0-or-1 slices and validating the length
// during a post-decode pass.
// errOptionalTooLong is returned when an optional list decoded with more
// than one element.
var errOptionalTooLong = errors.New("ssz: optional list has more than 1 element")
// checkOptional validates that an optional slice has length 0 or 1.
func checkOptional[T any](s []T) error {
if len(s) > 1 {
return errOptionalTooLong
}
return nil
}

View file

@ -0,0 +1,150 @@
// 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

@ -0,0 +1,185 @@
// 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

@ -0,0 +1,135 @@
// 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 (
"reflect"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
"github.com/karalabe/ssz"
)
func encDec[T ssz.Object](t *testing.T, obj T, newEmpty func() T) {
t.Helper()
size := ssz.Size(obj)
buf := make([]byte, size)
if err := ssz.EncodeToBytes(buf, obj); err != nil {
t.Fatalf("encode: %v", err)
}
got := newEmpty()
if err := ssz.DecodeFromBytes(buf, got); err != nil {
t.Fatalf("decode: %v", err)
}
// SSZ decode of an empty list yields []T{}, while encoding either nil
// or []T{} produces the same bytes. Compare via re-encode rather than
// DeepEqual to avoid spurious nil-vs-empty failures.
rebuf := make([]byte, ssz.Size(got))
if err := ssz.EncodeToBytes(rebuf, got); 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 TestWithdrawalRoundtrip(t *testing.T) {
encDec(t, &Withdrawal{
Index: 7,
ValidatorIndex: 42,
Address: common.Address{0xaa, 0xbb},
Amount: 123456,
}, func() *Withdrawal { return new(Withdrawal) })
}
func TestForkchoiceStateRoundtrip(t *testing.T) {
encDec(t, &ForkchoiceState{
HeadBlockHash: common.Hash{0x01},
SafeBlockHash: common.Hash{0x02},
FinalizedBlockHash: common.Hash{0x03},
}, func() *ForkchoiceState { return new(ForkchoiceState) })
}
func TestPayloadStatusRoundtrip(t *testing.T) {
h := common.Hash{0xab}
encDec(t, &PayloadStatus{
Status: StatusValid,
LatestValidHash: []common.Hash{h},
ValidationError: [][]byte{[]byte("err detail")},
}, func() *PayloadStatus { return new(PayloadStatus) })
// Absent optionals.
encDec(t, &PayloadStatus{Status: StatusSyncing}, func() *PayloadStatus { return new(PayloadStatus) })
}
func TestExecutionPayloadAmsterdamRoundtrip(t *testing.T) {
p := &ExecutionPayloadAmsterdam{
ParentHash: common.Hash{0x11},
FeeRecipient: common.Address{0x22},
StateRoot: common.Hash{0x33},
ReceiptsRoot: common.Hash{0x44},
PrevRandao: common.Hash{0x55},
BlockNumber: 100,
GasLimit: 30000000,
GasUsed: 21000,
Timestamp: 1700000000,
ExtraData: []byte("ext"),
BaseFeePerGas: uint256.NewInt(7e9),
BlockHash: common.Hash{0x66},
Transactions: [][]byte{{0x02, 0x03}, {0x04}},
Withdrawals: []*Withdrawal{{Index: 1, ValidatorIndex: 2, Amount: 3}},
BlobGasUsed: 131072,
ExcessBlobGas: 0,
BlockAccessList: []byte{0xde, 0xad, 0xbe, 0xef},
SlotNumber: 200,
}
encDec(t, p, func() *ExecutionPayloadAmsterdam { return new(ExecutionPayloadAmsterdam) })
}
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{
Timestamp: 1700000000,
PrevRandao: common.Hash{0x55},
SuggestedFeeRecipient: common.Address{0x66},
Withdrawals: []*Withdrawal{},
ParentBeaconBlockRoot: common.Hash{0x77},
SlotNumber: 42,
TargetGasLimit: 30000000,
}
fcu := &ForkchoiceUpdateAmsterdam{
ForkchoiceState: &ForkchoiceState{
HeadBlockHash: common.Hash{0xaa},
SafeBlockHash: common.Hash{0xbb},
FinalizedBlockHash: common.Hash{0xcc},
},
PayloadAttributes: []*PayloadAttributesAmsterdam{attrs},
CustodyColumns: []*Bitvector128{bits},
}
encDec(t, fcu, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) })
// Without optionals.
bare := &ForkchoiceUpdateAmsterdam{
ForkchoiceState: &ForkchoiceState{
HeadBlockHash: common.Hash{0xdd},
},
}
encDec(t, bare, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) })
}

164
beacon/engine/ssz/shared.go Normal file
View file

@ -0,0 +1,164 @@
// 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"
)
// Blob wraps a single 131072-byte blob. karalabe/ssz's generic
// constraint doesn't list 131072 as a "common" fixed length, so we
// wrap it as a static-object containing a length-checked []byte.
type Blob struct {
Bytes []byte
}
func (*Blob) SizeSSZ(*ssz.Sizer) uint32 { return BytesPerBlob }
func (b *Blob) DefineSSZ(c *ssz.Codec) {
ssz.DefineCheckedStaticBytes(c, &b.Bytes, BytesPerBlob)
}
// BlobCell wraps a single BYTES_PER_CELL = 1024 byte cell. Same wrapper
// rationale as Blob.
type BlobCell struct {
Bytes []byte
}
func (*BlobCell) SizeSSZ(*ssz.Sizer) uint32 { return BytesPerCell }
func (b *BlobCell) DefineSSZ(c *ssz.Codec) {
ssz.DefineCheckedStaticBytes(c, &b.Bytes, BytesPerCell)
}
// Bitvector128 wraps a packed bitvector of length CELLS_PER_EXT_BLOB = 128
// (16 bytes). Same wrapper rationale as Blob.
type Bitvector128 struct {
Bytes []byte
}
func (*Bitvector128) SizeSSZ(*ssz.Sizer) uint32 { return CellsPerExtBlob / 8 }
func (b *Bitvector128) DefineSSZ(c *ssz.Codec) {
ssz.DefineCheckedStaticBytes(c, &b.Bytes, CellsPerExtBlob/8)
}
// OptionalBlobCell models `Optional[ByteVector[BYTES_PER_CELL]]` =
// `List[BlobCell, 1]`. Length 0 = absent, length 1 = present (1024 bytes).
type OptionalBlobCell struct {
Cells []*BlobCell
}
func (o *OptionalBlobCell) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
if fixed {
return 0
}
return ssz.SizeSliceOfStaticObjects(siz, o.Cells)
}
func (o *OptionalBlobCell) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfStaticObjectsContent(c, &o.Cells, 1)
}
func (o *OptionalBlobCell) Validate() error { return checkOptional(o.Cells) }
// OptionalProof models `Optional[Bytes48]` = `List[Bytes48, 1]`.
type OptionalProof struct {
Proofs [][48]byte
}
func (o *OptionalProof) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
if fixed {
return 0
}
return ssz.SizeSliceOfStaticBytes(siz, o.Proofs)
}
func (o *OptionalProof) DefineSSZ(c *ssz.Codec) {
ssz.DefineSliceOfStaticBytesContent(c, &o.Proofs, 1)
}
func (o *OptionalProof) Validate() error { return checkOptional(o.Proofs) }
// Withdrawal mirrors the consensus-specs Withdrawal container.
type Withdrawal struct {
Index uint64
ValidatorIndex uint64
Address common.Address
Amount uint64
}
func (*Withdrawal) SizeSSZ(*ssz.Sizer) uint32 { return 44 }
func (w *Withdrawal) DefineSSZ(c *ssz.Codec) {
ssz.DefineUint64(c, &w.Index)
ssz.DefineUint64(c, &w.ValidatorIndex)
ssz.DefineStaticBytes(c, &w.Address)
ssz.DefineUint64(c, &w.Amount)
}
// ForkchoiceState carries head/safe/finalized block hashes.
type ForkchoiceState struct {
HeadBlockHash common.Hash
SafeBlockHash common.Hash
FinalizedBlockHash common.Hash
}
func (*ForkchoiceState) SizeSSZ(*ssz.Sizer) uint32 { return 96 }
func (f *ForkchoiceState) DefineSSZ(c *ssz.Codec) {
ssz.DefineStaticBytes(c, &f.HeadBlockHash)
ssz.DefineStaticBytes(c, &f.SafeBlockHash)
ssz.DefineStaticBytes(c, &f.FinalizedBlockHash)
}
// PayloadStatus is the response shape for /payloads and the inner status of
// /forkchoice. Status values are defined as constants in constants.go.
type PayloadStatus struct {
Status uint8
LatestValidHash []common.Hash // Optional[Hash32] — length 0 or 1
ValidationError [][]byte // Optional[String] — length 0 or 1
}
func (p *PayloadStatus) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
// status(1) + offset(4) + offset(4)
size := uint32(9)
if fixed {
return size
}
size += ssz.SizeSliceOfStaticBytes(siz, p.LatestValidHash)
size += ssz.SizeSliceOfDynamicBytes(siz, p.ValidationError)
return size
}
func (p *PayloadStatus) DefineSSZ(c *ssz.Codec) {
ssz.DefineUint8(c, &p.Status)
ssz.DefineSliceOfStaticBytesOffset(c, &p.LatestValidHash, 1)
ssz.DefineSliceOfDynamicBytesOffset(c, &p.ValidationError, 1, MaxErrorBytes)
ssz.DefineSliceOfStaticBytesContent(c, &p.LatestValidHash, 1)
ssz.DefineSliceOfDynamicBytesContent(c, &p.ValidationError, 1, MaxErrorBytes)
}
// Validate enforces the Optional[T] = List[T,1] length invariants.
func (p *PayloadStatus) Validate() error {
if err := checkOptional(p.LatestValidHash); err != nil {
return err
}
return checkOptional(p.ValidationError)
}

View file

@ -21,6 +21,7 @@ import (
"context"
"errors"
"fmt"
"net/http"
"reflect"
"strconv"
"sync"
@ -35,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/engineapi"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/internal/telemetry"
"github.com/ethereum/go-ethereum/internal/version"
@ -49,14 +51,20 @@ import (
// Register adds the engine API and related APIs to the full node.
func Register(stack *node.Node, backend *eth.Ethereum) error {
api := NewConsensusAPI(backend)
stack.RegisterAPIs([]rpc.API{
newTestingAPI(backend),
{
Namespace: "engine",
Service: NewConsensusAPI(backend),
Service: api,
Authenticated: true,
},
})
// Mount the REST + SSZ Engine API (execution-apis #793) under
// /engine/v2/ on the authenticated HTTP server. The router shares
// catalyst's business logic via the restBackend adapter.
stack.RegisterAuthHandler("engine-rest", engineapi.BasePath+"/",
http.StripPrefix(engineapi.BasePath, engineapi.NewRouter(newRESTBackend(api))))
return nil
}

68
eth/engineapi/backend.go Normal file
View file

@ -0,0 +1,68 @@
// 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 implements the REST + SSZ Engine API (execution-apis #793).
//
// The package is deliberately decoupled from eth/catalyst: the JSON-RPC engine
// surface and this REST surface share the same backing logic through the
// Backend interface defined here, but neither imports the other. The catalyst
// package supplies a Backend impl at node-startup time.
package engineapi
import (
"context"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params/forks"
)
// Backend is the contract a host node implements to drive the REST handlers.
type Backend interface {
// ForkchoiceUpdated applies a forkchoice update and optionally starts
// a payload build. version selects the JSON-RPC PayloadVersion the
// catalyst-layer expects.
ForkchoiceUpdated(ctx context.Context, state engine.ForkchoiceStateV1, attrs *engine.PayloadAttributes, version engine.PayloadVersion) (engine.ForkChoiceResponse, error)
// NewPayload validates and imports a new payload.
NewPayload(ctx context.Context, data engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte) (engine.PayloadStatusV1, error)
// GetPayload returns a previously-built payload. allowedForks filters
// to the fork the URL selected; an empty slice disables the check.
GetPayload(id engine.PayloadID, allowedForks []forks.Fork) (*engine.ExecutionPayloadEnvelope, error)
// GetBlobs returns blob bundle entries indexed by versioned hash.
// cellProofs=false matches /blobs/v1 semantics (single proof per blob);
// true matches /blobs/v2,v3 (cell proofs).
GetBlobs(hashes []common.Hash, cellProofs bool) ([]*engine.BlobAndProofV2, []*engine.BlobAndProofV1, error)
// BodiesByHash returns block bodies for the given hashes. Order matches
// the input; an entry is nil for unknown/pruned blocks. The second slice
// returns the timestamp of each known block (zero for unknown) so the
// router can enforce the URL fork's era window.
BodiesByHash(hashes []common.Hash) ([]*types.Body, []uint64)
// BodiesByRange returns block bodies for [from, from+count). Same shape
// as BodiesByHash.
BodiesByRange(from, count uint64) ([]*types.Body, []uint64)
// ForkFromTimestamp returns the fork active at timestamp ts.
ForkFromTimestamp(ts uint64) forks.Fork
// ClientVersion returns the EL client identity for /identity responses.
ClientVersion() engine.ClientVersionV1
}

149
eth/engineapi/blobs.go Normal file
View file

@ -0,0 +1,149 @@
// 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"
"github.com/ethereum/go-ethereum/beacon/engine"
sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz"
)
// handleBlobs dispatches POST /engine/v2/blobs/v{1,2,3,4}.
// suffix is the path remainder after "/blobs/".
func (rt *Router) handleBlobs(w http.ResponseWriter, r *http.Request, suffix string) {
if r.Method != http.MethodPost {
writeProblem(w, http.StatusNotFound, ErrMethodNotFound, "")
return
}
switch suffix {
case "v1":
rt.handleBlobsV1(w, r)
case "v2":
rt.handleBlobsV2(w, r, false)
case "v3":
rt.handleBlobsV2(w, r, true)
case "v4":
rt.handleBlobsV4(w, r)
default:
writeProblem(w, http.StatusNotFound, ErrMethodNotFound, "")
}
}
func (rt *Router) handleBlobsV1(w http.ResponseWriter, r *http.Request) {
req := new(sszt.BlobsVersionedHashesRequest)
if !readSSZRequest(w, r, req, maxPayloadBytes) {
return
}
if len(req.VersionedHashes) > sszt.MaxBlobsRequest {
writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "")
return
}
_, v1, err := rt.backend.GetBlobs(req.VersionedHashes, false)
if err != nil {
mapBackendErr(w, err)
return
}
// Backend returns nil entries for misses; map to available=false.
resp := &sszt.BlobsV1Response{Entries: make([]*sszt.BlobV1Entry, len(v1))}
for i, bp := range v1 {
e := &sszt.BlobV1Entry{Contents: emptyBlobAndProofV1()}
if bp != nil {
e.Available = true
e.Contents = &sszt.BlobAndProofV1{Blob: &sszt.Blob{Bytes: bp.Blob}}
copy(e.Contents.Proof[:], bp.Proof)
}
resp.Entries[i] = e
}
writeSSZResponse(w, resp)
}
// 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) {
return
}
if len(req.VersionedHashes) > sszt.MaxBlobsRequest {
writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "")
return
}
v2, _, err := rt.backend.GetBlobs(req.VersionedHashes, true)
if err != nil {
mapBackendErr(w, err)
return
}
if !allowPartial {
// V2 is all-or-nothing: missing entries collapse the response to 204.
for _, bp := range v2 {
if bp == nil {
w.WriteHeader(http.StatusNoContent)
return
}
}
}
resp := &sszt.BlobsV2Response{Entries: make([]*sszt.BlobV2Entry, len(v2))}
for i, bp := range v2 {
e := &sszt.BlobV2Entry{Contents: emptyBlobAndProofV2()}
if bp != nil {
e.Available = true
e.Contents = blobAndProofV2FromEngine(bp)
}
resp.Entries[i] = e
}
writeSSZResponse(w, resp)
}
// handleBlobsV4 implements POST /engine/v2/blobs/v4 (cell-range selection).
// The custody/cell-range logic is not yet plumbed into the txpool; we return
// 204 to signal the EL cannot serve it. The handler still validates the
// 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) {
return
}
if req.IndicesBitarray == nil || len(req.IndicesBitarray.Bytes) != sszt.CellsPerExtBlob/8 {
writeProblem(w, http.StatusBadRequest, ErrInvalidBody, "indices_bitarray length")
return
}
if len(req.VersionedHashes) > sszt.MaxBlobsRequest {
writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "")
return
}
w.WriteHeader(http.StatusNoContent)
}
func emptyBlobAndProofV1() *sszt.BlobAndProofV1 {
return &sszt.BlobAndProofV1{Blob: &sszt.Blob{Bytes: make([]byte, sszt.BytesPerBlob)}}
}
func emptyBlobAndProofV2() *sszt.BlobAndProofV2 {
return &sszt.BlobAndProofV2{Blob: &sszt.Blob{Bytes: make([]byte, sszt.BytesPerBlob)}}
}
func blobAndProofV2FromEngine(bp *engine.BlobAndProofV2) *sszt.BlobAndProofV2 {
out := &sszt.BlobAndProofV2{
Blob: &sszt.Blob{Bytes: append([]byte(nil), bp.Blob...)},
Proofs: make([][48]byte, len(bp.CellProofs)),
}
for i, p := range bp.CellProofs {
copy(out.Proofs[i][:], p)
}
return out
}

107
eth/engineapi/bodies.go Normal file
View file

@ -0,0 +1,107 @@
// 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"
"strconv"
sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params/forks"
)
// 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, "")
return
}
req := new(sszt.BodiesByHashRequest)
if !readSSZRequest(w, r, req, maxPayloadBytes) {
return
}
if len(req.BlockHashes) > sszt.MaxBodiesRequest {
writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "")
return
}
bodies, timestamps := rt.backend.BodiesByHash(req.BlockHashes)
writeSSZResponse(w, buildBodiesResponseAmsterdam(rt.backend, fork, bodies, timestamps))
}
// 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, "")
return
}
q := r.URL.Query()
from, err := strconv.ParseUint(q.Get("from"), 10, 64)
if err != nil {
writeProblem(w, http.StatusBadRequest, ErrInvalidRequest, "missing or bad from")
return
}
count, err := strconv.ParseUint(q.Get("count"), 10, 64)
if err != nil || count == 0 {
writeProblem(w, http.StatusBadRequest, ErrInvalidRequest, "missing or bad count")
return
}
if count > sszt.MaxBodiesRequest {
writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "")
return
}
bodies, timestamps := rt.backend.BodiesByRange(from, count)
writeSSZResponse(w, buildBodiesResponseAmsterdam(rt.backend, fork, bodies, timestamps))
}
// 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)),
}
for i, body := range bodies {
entry := &sszt.BodyEntryAmsterdam{Body: new(sszt.ExecutionPayloadBodyAmsterdam)}
if body != nil && b.ForkFromTimestamp(ts[i]) == fork {
entry.Available = true
entry.Body = bodyToAmsterdamSSZ(body)
}
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{
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,
}
}
// BlockAccessList is not yet on types.Body; once wired through, copy here.
return out
}

View file

@ -0,0 +1,57 @@
// 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 (
"encoding/json"
"net/http"
sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz"
)
// capabilitiesResponse mirrors the structured shape in refactor.md §
// "Capabilities format".
type capabilitiesResponse struct {
SupportedForks []string `json:"supported_forks"`
ForkScopedEndpoints []string `json:"fork_scoped_endpoints"`
IndependentlyVersioned map[string][]string `json:"independently_versioned"`
UnscopedEndpoints []string `json:"unscoped_endpoints"`
Limits map[string]uint64 `json:"limits"`
}
// handleCapabilities implements GET /engine/v2/capabilities.
func (rt *Router) handleCapabilities(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeProblem(w, http.StatusNotFound, ErrMethodNotFound, "")
return
}
resp := capabilitiesResponse{
SupportedForks: []string{"amsterdam"},
ForkScopedEndpoints: []string{"payloads", "forkchoice", "bodies"},
IndependentlyVersioned: map[string][]string{
"blobs": {"v1", "v2", "v3", "v4"},
},
UnscopedEndpoints: []string{"capabilities", "identity"},
Limits: map[string]uint64{
"bodies.max_count": sszt.MaxBodiesRequest,
"blobs.max_versioned_hashes": sszt.MaxBlobsRequest,
"payload.max_bytes": maxPayloadBytes,
},
}
w.Header().Set("Content-Type", jsonContentType)
_ = json.NewEncoder(w).Encode(resp)
}

55
eth/engineapi/errors.go Normal file
View file

@ -0,0 +1,55 @@
// 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 (
"encoding/json"
"net/http"
)
// Error type URIs from refactor.md § Error model.
const (
ErrParseError = "/engine-api/errors/parse-error"
ErrInvalidRequest = "/engine-api/errors/invalid-request"
ErrSSZDecode = "/engine-api/errors/ssz-decode-error"
ErrUnsupportedFork = "/engine-api/errors/unsupported-fork"
ErrMethodNotFound = "/engine-api/errors/method-not-found"
ErrUnknownPayload = "/engine-api/errors/unknown-payload"
ErrInvalidForkchoice = "/engine-api/errors/invalid-forkchoice"
ErrReorgTooDeep = "/engine-api/errors/reorg-too-deep"
ErrRequestTooLarge = "/engine-api/errors/request-too-large"
ErrUnsupportedMedia = "/engine-api/errors/unsupported-media-type"
ErrInvalidBody = "/engine-api/errors/invalid-body"
ErrInvalidAttributes = "/engine-api/errors/invalid-attributes"
ErrInternal = "/engine-api/errors/internal"
)
const problemJSONContentType = "application/problem+json"
// problem is the RFC 7807 body shape. Only type and detail are populated,
// matching the spec's two-field profile.
type problem struct {
Type string `json:"type"`
Detail string `json:"detail,omitempty"`
}
// writeProblem emits an RFC 7807 error body. detail is optional and may be empty.
func writeProblem(w http.ResponseWriter, status int, typeURI, detail string) {
w.Header().Set("Content-Type", problemJSONContentType)
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(problem{Type: typeURI, Detail: detail})
}

View file

@ -0,0 +1,77 @@
// 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"
"github.com/ethereum/go-ethereum/beacon/engine"
sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz"
"github.com/ethereum/go-ethereum/params/forks"
)
// 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, "")
return
}
fcu := new(sszt.ForkchoiceUpdateAmsterdam)
if !readSSZRequest(w, r, fcu, maxPayloadBytes) {
return
}
if err := fcu.Validate(); err != nil {
writeProblem(w, http.StatusBadRequest, ErrInvalidBody, err.Error())
return
}
state := sszt.ForkchoiceStateToV1(fcu.ForkchoiceState)
var attrs *engine.PayloadAttributes
if len(fcu.PayloadAttributes) == 1 {
ssz := fcu.PayloadAttributes[0]
// 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 {
writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork,
"payload_attributes timestamp does not match URL fork")
return
}
attrs = sszt.PayloadAttributesAmsterdamToEngine(ssz)
// 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
// underlying miner gains the corresponding setting.
}
resp, err := rt.backend.ForkchoiceUpdated(r.Context(), state, attrs, engine.PayloadV4)
if err != nil {
mapBackendErr(w, err)
return
}
// /forkchoice MUST NOT return ACCEPTED.
if resp.PayloadStatus.Status == engine.ACCEPTED {
resp.PayloadStatus.Status = engine.INVALID
}
out := &sszt.ForkchoiceUpdateResponseAmsterdam{
PayloadStatus: sszt.PayloadStatusFromV1(&resp.PayloadStatus),
}
if resp.PayloadID != nil {
out.PayloadID = [][8]byte{[8]byte(*resp.PayloadID)}
}
writeSSZResponse(w, out)
}

View file

@ -0,0 +1,93 @@
// 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"
"github.com/ethereum/go-ethereum/beacon/engine"
sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/params/forks"
"github.com/holiman/uint256"
)
// 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, "")
return
}
raw, err := hexutil.Decode(idHex)
if err != nil || len(raw) != 8 {
writeProblem(w, http.StatusBadRequest, ErrInvalidRequest, "malformed payloadId")
return
}
var id engine.PayloadID
copy(id[:], raw)
env, err := rt.backend.GetPayload(id, []forks.Fork{forks.Amsterdam})
if err != nil {
mapBackendErr(w, err)
return
}
out := buildBuiltPayloadAmsterdam(env)
w.Header().Set("Cache-Control", "no-store")
writeSSZResponse(w, out)
}
// 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 {
out := &sszt.BuiltPayloadAmsterdam{
Payload: sszt.ExecutionPayloadAmsterdamFromEngine(env.ExecutionPayload),
BlockValue: new(uint256.Int),
ExecutionRequests: env.Requests,
ShouldOverrideBuilder: env.Override,
}
if env.BlockValue != nil {
out.BlockValue.SetFromBig(env.BlockValue)
}
if env.BlobsBundle != nil {
out.BlobsBundle = convertBlobsBundle(env.BlobsBundle)
} else {
out.BlobsBundle = new(sszt.BlobsBundleV2)
}
return out
}
// convertBlobsBundle copies the JSON BlobsBundle into the SSZ V2 layout.
// Inputs are length-validated by the caller's miner pipeline.
func convertBlobsBundle(b *engine.BlobsBundle) *sszt.BlobsBundleV2 {
out := &sszt.BlobsBundleV2{
Commitments: make([][48]byte, len(b.Commitments)),
Proofs: make([][48]byte, len(b.Proofs)),
Blobs: make([]*sszt.Blob, len(b.Blobs)),
}
for i, c := range b.Commitments {
copy(out.Commitments[i][:], c)
}
for i, p := range b.Proofs {
copy(out.Proofs[i][:], p)
}
for i, blob := range b.Blobs {
out.Blobs[i] = &sszt.Blob{Bytes: append([]byte(nil), blob...)}
}
return out
}

39
eth/engineapi/identity.go Normal file
View file

@ -0,0 +1,39 @@
// 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 (
"encoding/json"
"net/http"
"github.com/ethereum/go-ethereum/beacon/engine"
)
// handleIdentity implements GET /engine/v2/identity.
// The CL surfaces itself via the X-Engine-Client-Version request header; the
// EL responds with its own ClientVersion in JSON.
func (rt *Router) handleIdentity(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeProblem(w, http.StatusNotFound, ErrMethodNotFound, "")
return
}
cv := rt.backend.ClientVersion()
w.Header().Set("Content-Type", jsonContentType)
_ = json.NewEncoder(w).Encode(struct {
Versions []engine.ClientVersionV1 `json:"versions"`
}{Versions: []engine.ClientVersionV1{cv}})
}

76
eth/engineapi/media.go Normal file
View file

@ -0,0 +1,76 @@
// 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 (
"io"
"net/http"
"strings"
"github.com/karalabe/ssz"
)
const (
sszContentType = "application/octet-stream"
jsonContentType = "application/json"
// maxPayloadBytes mirrors the capabilities advertisement
// limits.payload.max_bytes (64 MiB). Per-endpoint helpers may lower it.
maxPayloadBytes = 64 * 1024 * 1024
)
// 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 {
if !strings.EqualFold(r.Header.Get("Content-Type"), sszContentType) {
writeProblem(w, http.StatusUnsupportedMediaType, ErrUnsupportedMedia, "expected "+sszContentType)
return false
}
if r.ContentLength > max {
writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "")
return false
}
body, err := io.ReadAll(io.LimitReader(r.Body, max+1))
if err != nil {
writeProblem(w, http.StatusBadRequest, ErrParseError, err.Error())
return false
}
if int64(len(body)) > max {
writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "")
return false
}
if err := ssz.DecodeFromBytes(body, obj); 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 {
writeProblem(w, http.StatusInternalServerError, ErrInternal, err.Error())
return false
}
w.Header().Set("Content-Type", sszContentType)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(buf)
return true
}

89
eth/engineapi/payloads.go Normal file
View file

@ -0,0 +1,89 @@
// 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"
"github.com/ethereum/go-ethereum/beacon/engine"
sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params/forks"
)
// 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, "")
return
}
env := new(sszt.ExecutionPayloadEnvelopeAmsterdam)
if !readSSZRequest(w, r, env, maxPayloadBytes) {
return
}
data := sszt.ExecutionPayloadAmsterdamToEngine(env.Payload)
// The spec drops expectedBlobVersionedHashes; we recompute them from the
// payload's transactions before passing to the EL.
versionedHashes, err := versionedHashesFromTxs(data.Transactions)
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, ErrInvalidBody, err.Error())
return
}
root := env.ParentBeaconBlockRoot
status, err := rt.backend.NewPayload(r.Context(), *data, versionedHashes, &root, env.ExecutionRequests)
if err != nil {
mapBackendErr(w, err)
return
}
writeSSZResponse(w, sszt.PayloadStatusFromV1(&status))
}
// versionedHashesFromTxs extracts the blob versioned-hash list from the
// payload's RLP-encoded transactions. The CL no longer sends it; the EL
// recomputes for the block-hash check.
func versionedHashesFromTxs(raw [][]byte) ([]common.Hash, error) {
txs, err := engine.DecodeTransactions(raw)
if err != nil {
return nil, err
}
var hashes []common.Hash
for _, tx := range txs {
hashes = append(hashes, tx.BlobHashes()...)
}
return hashes, nil
}
// mapBackendErr translates engine-level errors into the spec's error model.
func mapBackendErr(w http.ResponseWriter, err error) {
switch err {
case engine.UnknownPayload:
writeProblem(w, http.StatusNotFound, ErrUnknownPayload, "")
case engine.InvalidForkChoiceState:
writeProblem(w, http.StatusConflict, ErrInvalidForkchoice, "")
case engine.InvalidPayloadAttributes:
writeProblem(w, http.StatusUnprocessableEntity, ErrInvalidAttributes, "")
case engine.UnsupportedFork:
writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "")
case engine.TooLargeRequest:
writeProblem(w, http.StatusRequestEntityTooLarge, ErrRequestTooLarge, "")
case engine.InvalidParams:
writeProblem(w, http.StatusBadRequest, ErrInvalidRequest, "")
default:
writeProblem(w, http.StatusInternalServerError, ErrInternal, err.Error())
}
}

106
eth/engineapi/router.go Normal file
View file

@ -0,0 +1,106 @@
// 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"
"strings"
"github.com/ethereum/go-ethereum/params/forks"
)
// BasePath is the mount prefix for the REST Engine API. Handlers below assume
// requests have already been routed under this prefix.
const BasePath = "/engine/v2"
// supportedForks lists the fork URL segments the router recognises. Order is
// chronological; the router does an exact prefix match so /amsterdam/payloads
// is not confused with /amsterdam-foo/payloads.
var supportedForks = map[string]forks.Fork{
"paris": forks.Paris,
"shanghai": forks.Shanghai,
"cancun": forks.Cancun,
"prague": forks.Prague,
"osaka": forks.Osaka,
"amsterdam": forks.Amsterdam,
}
// Router is the http.Handler implementing the REST Engine API.
type Router struct {
backend Backend
}
// NewRouter constructs a Router backed by b.
func NewRouter(b Backend) *Router {
return &Router{backend: b}
}
// ServeHTTP dispatches to the per-endpoint handler. The caller is expected to
// have stripped the BasePath prefix and applied authentication.
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// The spec forbids trailing slashes; net/http's ServeMux would redirect,
// so police it here.
if len(path) > 1 && strings.HasSuffix(path, "/") {
writeProblem(w, http.StatusNotFound, ErrMethodNotFound, "")
return
}
switch {
case path == "/capabilities":
rt.handleCapabilities(w, r)
case path == "/identity":
rt.handleIdentity(w, r)
case strings.HasPrefix(path, "/blobs/"):
rt.handleBlobs(w, r, path[len("/blobs/"):])
default:
rt.handleForkScoped(w, r, path)
}
}
// handleForkScoped routes /{fork}/<endpoint>... paths.
func (rt *Router) handleForkScoped(w http.ResponseWriter, r *http.Request, path string) {
if len(path) < 2 || path[0] != '/' {
writeProblem(w, http.StatusNotFound, ErrMethodNotFound, "")
return
}
rest := path[1:]
slash := strings.IndexByte(rest, '/')
if slash < 0 {
writeProblem(w, http.StatusNotFound, ErrMethodNotFound, "")
return
}
forkName, sub := rest[:slash], rest[slash:]
fork, ok := supportedForks[forkName]
if !ok {
writeProblem(w, http.StatusBadRequest, ErrUnsupportedFork, "unknown fork "+forkName)
return
}
switch {
case sub == "/payloads" && r.Method == http.MethodPost:
rt.handleNewPayload(w, r, fork)
case sub == "/forkchoice" && r.Method == http.MethodPost:
rt.handleForkchoice(w, r, fork)
case strings.HasPrefix(sub, "/payloads/") && r.Method == http.MethodGet:
rt.handleGetPayload(w, r, fork, sub[len("/payloads/"):])
case sub == "/bodies/hash" && r.Method == http.MethodPost:
rt.handleBodiesByHash(w, r, fork)
case sub == "/bodies" && r.Method == http.MethodGet:
rt.handleBodiesByRange(w, r, fork)
default:
writeProblem(w, http.StatusNotFound, ErrMethodNotFound, "")
}
}

View file

@ -0,0 +1,373 @@
// 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 (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/ethereum/go-ethereum/beacon/engine"
sszt "github.com/ethereum/go-ethereum/beacon/engine/ssz"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params/forks"
"github.com/holiman/uint256"
"github.com/karalabe/ssz"
)
// stubBackend captures requests to verify the router routes correctly.
type stubBackend struct {
fcuStatus engine.PayloadStatusV1
fcuID *engine.PayloadID
npStatus engine.PayloadStatusV1
npErr error
lastFCUState engine.ForkchoiceStateV1
lastFCUAttrs *engine.PayloadAttributes
lastNPData engine.ExecutableData
lastNPHashes []common.Hash
lastNPRequest [][]byte
envelope *engine.ExecutionPayloadEnvelope
getErr error
bodies []*types.Body
bodyTimes []uint64
v1Blobs []*engine.BlobAndProofV1
v2Blobs []*engine.BlobAndProofV2
forkAtTime func(uint64) forks.Fork
}
func (s *stubBackend) ForkchoiceUpdated(_ context.Context, state engine.ForkchoiceStateV1, attrs *engine.PayloadAttributes, _ engine.PayloadVersion) (engine.ForkChoiceResponse, error) {
s.lastFCUState = state
s.lastFCUAttrs = attrs
return engine.ForkChoiceResponse{PayloadStatus: s.fcuStatus, PayloadID: s.fcuID}, nil
}
func (s *stubBackend) NewPayload(_ context.Context, data engine.ExecutableData, hashes []common.Hash, _ *common.Hash, requests [][]byte) (engine.PayloadStatusV1, error) {
s.lastNPData = data
s.lastNPHashes = hashes
s.lastNPRequest = requests
return s.npStatus, s.npErr
}
func (s *stubBackend) GetPayload(engine.PayloadID, []forks.Fork) (*engine.ExecutionPayloadEnvelope, error) {
return s.envelope, s.getErr
}
func (s *stubBackend) GetBlobs([]common.Hash, bool) ([]*engine.BlobAndProofV2, []*engine.BlobAndProofV1, error) {
return s.v2Blobs, s.v1Blobs, nil
}
func (s *stubBackend) BodiesByHash(hashes []common.Hash) ([]*types.Body, []uint64) {
return s.bodies, s.bodyTimes
}
func (s *stubBackend) BodiesByRange(from, count uint64) ([]*types.Body, []uint64) {
return s.bodies, s.bodyTimes
}
func (s *stubBackend) ForkFromTimestamp(ts uint64) forks.Fork {
if s.forkAtTime != nil {
return s.forkAtTime(ts)
}
return forks.Amsterdam
}
func (s *stubBackend) ClientVersion() engine.ClientVersionV1 {
return engine.ClientVersionV1{Code: "GE", Name: "geth", Version: "test", Commit: "0xdeadbeef"}
}
func newTestServer(t *testing.T, b Backend) *httptest.Server {
t.Helper()
return httptest.NewServer(http.StripPrefix(BasePath, NewRouter(b)))
}
func sszPost(t *testing.T, srv *httptest.Server, path string, body ssz.Object) *http.Response {
t.Helper()
buf := make([]byte, ssz.Size(body))
if err := ssz.EncodeToBytes(buf, body); err != nil {
t.Fatalf("encode: %v", err)
}
req, _ := http.NewRequest(http.MethodPost, srv.URL+BasePath+path, bytes.NewReader(buf))
req.Header.Set("Content-Type", sszContentType)
resp, err := srv.Client().Do(req)
if err != nil {
t.Fatalf("post %s: %v", path, err)
}
return resp
}
func decodeSSZ[T ssz.Object](t *testing.T, resp *http.Response, obj T) {
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 {
t.Fatalf("decode response: %v\nbody: %x", err, body)
}
}
func TestRouterNewPayload(t *testing.T) {
id := engine.PayloadID{1, 2, 3}
b := &stubBackend{
npStatus: engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &common.Hash{0xaa}},
fcuID: &id,
}
srv := newTestServer(t, b)
defer srv.Close()
env := &sszt.ExecutionPayloadEnvelopeAmsterdam{
Payload: &sszt.ExecutionPayloadAmsterdam{
BaseFeePerGas: uint256.NewInt(7e9),
LogsBloom: [256]byte{},
},
ParentBeaconBlockRoot: common.Hash{0x55},
}
resp := sszPost(t, srv, "/amsterdam/payloads", env)
if resp.StatusCode != 200 {
t.Fatalf("want 200, got %d", resp.StatusCode)
}
got := new(sszt.PayloadStatus)
decodeSSZ(t, resp, got)
if got.Status != sszt.StatusValid {
t.Errorf("status=%d want VALID(%d)", got.Status, sszt.StatusValid)
}
if len(got.LatestValidHash) != 1 || got.LatestValidHash[0] != (common.Hash{0xaa}) {
t.Errorf("LatestValidHash=%v", got.LatestValidHash)
}
}
func TestRouterForkchoice(t *testing.T) {
id := engine.PayloadID{9, 9, 9}
b := &stubBackend{
fcuStatus: engine.PayloadStatusV1{Status: engine.VALID},
fcuID: &id,
}
srv := newTestServer(t, b)
defer srv.Close()
fcu := &sszt.ForkchoiceUpdateAmsterdam{
ForkchoiceState: &sszt.ForkchoiceState{HeadBlockHash: common.Hash{0xaa}},
}
resp := sszPost(t, srv, "/amsterdam/forkchoice", fcu)
if resp.StatusCode != 200 {
t.Fatalf("want 200, got %d", resp.StatusCode)
}
got := new(sszt.ForkchoiceUpdateResponseAmsterdam)
decodeSSZ(t, resp, got)
if got.PayloadStatus == nil || got.PayloadStatus.Status != sszt.StatusValid {
t.Errorf("status: %#v", got.PayloadStatus)
}
if len(got.PayloadID) != 1 || got.PayloadID[0] != [8]byte(id) {
t.Errorf("PayloadID=%v want %v", got.PayloadID, id)
}
}
func TestRouterUnsupportedFork(t *testing.T) {
srv := newTestServer(t, &stubBackend{})
defer srv.Close()
// Bogus fork in URL.
resp, err := srv.Client().Post(srv.URL+BasePath+"/bogus/payloads", sszContentType, bytes.NewReader(nil))
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != 400 {
t.Fatalf("want 400, got %d", resp.StatusCode)
}
// Real fork but not Amsterdam.
resp, err = srv.Client().Post(srv.URL+BasePath+"/cancun/payloads", sszContentType, bytes.NewReader(nil))
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != 400 {
t.Fatalf("want 400 for cancun, got %d", resp.StatusCode)
}
}
func TestRouterUnsupportedMediaType(t *testing.T) {
srv := newTestServer(t, &stubBackend{})
defer srv.Close()
resp, err := srv.Client().Post(srv.URL+BasePath+"/amsterdam/payloads", "application/json", strings.NewReader("{}"))
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != 415 {
t.Fatalf("want 415, got %d", resp.StatusCode)
}
}
func TestRouterCapabilities(t *testing.T) {
srv := newTestServer(t, &stubBackend{})
defer srv.Close()
resp, err := srv.Client().Get(srv.URL + BasePath + "/capabilities")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatalf("want 200, got %d", resp.StatusCode)
}
var c capabilitiesResponse
if err := json.NewDecoder(resp.Body).Decode(&c); err != nil {
t.Fatal(err)
}
if len(c.SupportedForks) == 0 || c.SupportedForks[0] != "amsterdam" {
t.Errorf("supported forks: %v", c.SupportedForks)
}
if got := c.Limits["blobs.max_versioned_hashes"]; got != sszt.MaxBlobsRequest {
t.Errorf("blobs limit: %d", got)
}
}
func TestRouterIdentity(t *testing.T) {
srv := newTestServer(t, &stubBackend{})
defer srv.Close()
resp, err := srv.Client().Get(srv.URL + BasePath + "/identity")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatalf("want 200, got %d", resp.StatusCode)
}
var body struct {
Versions []engine.ClientVersionV1 `json:"versions"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if len(body.Versions) != 1 || body.Versions[0].Code != "GE" {
t.Errorf("identity: %#v", body)
}
}
func TestRouterTrailingSlash404(t *testing.T) {
srv := newTestServer(t, &stubBackend{})
defer srv.Close()
resp, err := srv.Client().Get(srv.URL + BasePath + "/capabilities/")
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != 404 {
t.Fatalf("want 404 for trailing slash, got %d", resp.StatusCode)
}
}
func TestRouterSSZDecodeError(t *testing.T) {
srv := newTestServer(t, &stubBackend{})
defer srv.Close()
req, _ := http.NewRequest(http.MethodPost, srv.URL+BasePath+"/amsterdam/payloads", bytes.NewReader([]byte{0xff}))
req.Header.Set("Content-Type", sszContentType)
resp, err := srv.Client().Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 400 {
t.Fatalf("want 400 ssz-decode-error, got %d", resp.StatusCode)
}
var p problem
if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
t.Fatal(err)
}
if p.Type != ErrSSZDecode {
t.Errorf("type=%s want %s", p.Type, ErrSSZDecode)
}
}
func TestRouterBlobsV2AllOrNothing(t *testing.T) {
// One requested hash, but backend returns a nil entry -> /v2 should 204.
b := &stubBackend{v2Blobs: []*engine.BlobAndProofV2{nil}}
srv := newTestServer(t, b)
defer srv.Close()
req := &sszt.BlobsVersionedHashesRequest{VersionedHashes: []common.Hash{{0x01}}}
resp := sszPost(t, srv, "/blobs/v2", req)
defer resp.Body.Close()
if resp.StatusCode != 204 {
t.Fatalf("want 204, got %d", resp.StatusCode)
}
}
func TestRouterBlobsV3PartialOK(t *testing.T) {
b := &stubBackend{v2Blobs: []*engine.BlobAndProofV2{nil, {Blob: make([]byte, sszt.BytesPerBlob)}}}
srv := newTestServer(t, b)
defer srv.Close()
req := &sszt.BlobsVersionedHashesRequest{VersionedHashes: []common.Hash{{0x01}, {0x02}}}
resp := sszPost(t, srv, "/blobs/v3", req)
if resp.StatusCode != 200 {
t.Fatalf("want 200, got %d", resp.StatusCode)
}
got := new(sszt.BlobsV2Response)
decodeSSZ(t, resp, got)
if len(got.Entries) != 2 {
t.Fatalf("entries=%d", len(got.Entries))
}
if got.Entries[0].Available {
t.Error("entry 0 should be unavailable")
}
if !got.Entries[1].Available {
t.Error("entry 1 should be available")
}
}
func TestRouterGetPayloadCacheControl(t *testing.T) {
env := &engine.ExecutionPayloadEnvelope{
ExecutionPayload: &engine.ExecutableData{LogsBloom: make([]byte, 256)},
BlockValue: uint256.NewInt(0).ToBig(),
}
srv := newTestServer(t, &stubBackend{envelope: env})
defer srv.Close()
resp, err := srv.Client().Get(srv.URL + BasePath + "/amsterdam/payloads/0x0102030405060708")
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatalf("want 200, got %d", resp.StatusCode)
}
if cc := resp.Header.Get("Cache-Control"); cc != "no-store" {
t.Errorf("Cache-Control=%q want no-store", cc)
}
}
func TestRouterGetPayloadUnknown(t *testing.T) {
srv := newTestServer(t, &stubBackend{getErr: engine.UnknownPayload})
defer srv.Close()
resp, err := srv.Client().Get(srv.URL + BasePath + "/amsterdam/payloads/0x0102030405060708")
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != 404 {
t.Fatalf("want 404, got %d", resp.StatusCode)
}
}

3
go.mod
View file

@ -44,6 +44,7 @@ require (
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
github.com/jackpal/go-nat-pmp v1.0.2
github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267
github.com/karalabe/ssz v0.3.0
github.com/klauspost/compress v1.17.8
github.com/kylelemons/godebug v1.1.0
github.com/mattn/go-colorable v0.1.13
@ -86,6 +87,8 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 // indirect
github.com/prysmaticlabs/gohashtree v0.0.4-beta // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect

4
go.sum
View file

@ -227,6 +227,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/karalabe/ssz v0.3.0 h1:wSiAszX7Gr7TVheCebrDgRKEUnfHuD0WxQ2uDcbI4uU=
github.com/karalabe/ssz v0.3.0/go.mod h1:7BZG/jckt43eKw7sl/AF6gTcL0oxgFPme39m54v8rDI=
github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4=
github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -329,6 +331,8 @@ github.com/protolambda/zrnt v0.34.1 h1:qW55rnhZJDnOb3TwFiFRJZi3yTXFrJdGOFQM7vCwY
github.com/protolambda/zrnt v0.34.1/go.mod h1:A0fezkp9Tt3GBLATSPIbuY4ywYESyAuc/FFmPKg8Lqs=
github.com/protolambda/ztyp v0.2.2 h1:rVcL3vBu9W/aV646zF6caLS/dyn9BN8NYiuJzicLNyY=
github.com/protolambda/ztyp v0.2.2/go.mod h1:9bYgKGqg3wJqT9ac1gI2hnVb0STQq7p/1lapqrqY1dU=
github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 h1:lC8kiphgdOBTcbTvo8MwkvpKjO0SlAgjv4xIK5FGJ94=
github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15/go.mod h1:8svFBIKKu31YriBG/pNizo9N0Jr9i5PQ+dFkxWg3x5k=
github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4=
github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=

View file

@ -148,8 +148,9 @@ func New(conf *Config) (*Node, error) {
// Configure RPC servers.
node.http = newHTTPServer(node.log, conf.HTTPTimeouts)
// The REST + SSZ Engine API (execution-apis #793) requires HTTP/2,
// so the authenticated HTTP server speaks h2c alongside HTTP/1.1.
node.httpAuth = newHTTPServer(node.log, conf.HTTPTimeouts)
node.httpAuth.disableHTTP2 = true // Engine API does not need HTTP/2
node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
node.wsAuth = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
node.wsAuth.disableHTTP2 = true
@ -434,6 +435,7 @@ func (n *Node) startRPC() error {
if err := server.setListenAddr(n.config.AuthAddr, port); err != nil {
return err
}
server.authSecret = secret
sharedConfig := rpcEndpointConfig{
jwtSecret: secret,
batchItemLimit: engineAPIBatchItemLimit,
@ -605,6 +607,20 @@ func (n *Node) RegisterHandler(name, path string, handler http.Handler) {
n.http.handlerNames[path] = name
}
// RegisterAuthHandler mounts a handler on the authenticated HTTP server's mux.
// JWT auth is applied to it via the same middleware that protects the
// JSON-RPC engine namespace. Used by the REST + SSZ Engine API.
func (n *Node) RegisterAuthHandler(name, path string, handler http.Handler) {
n.lock.Lock()
defer n.lock.Unlock()
if n.state != initializingState {
panic("can't register HTTP handler on running/stopped node")
}
n.httpAuth.mux.Handle(path, handler)
n.httpAuth.handlerNames[path] = name
}
// Attach creates an RPC client attached to an in-process API handler.
func (n *Node) Attach() *rpc.Client {
return rpc.DialInProc(n.inprocHandler)

View file

@ -93,6 +93,11 @@ type httpServer struct {
// disableHTTP2 disables HTTP/2 support on this server when set to true.
disableHTTP2 bool
// authSecret, when set, causes the server to apply JWT authentication
// to mux-routed handlers. Used by the auth server to protect REST
// endpoints registered via Node.RegisterAuthHandler.
authSecret []byte
}
const (
@ -221,7 +226,11 @@ func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// These are made available when RPC is enabled.
muxHandler, pattern := h.mux.Handler(r)
if pattern != "" {
muxHandler.ServeHTTP(w, r)
if len(h.authSecret) != 0 {
newJWTHandler(h.authSecret, muxHandler).ServeHTTP(w, r)
} else {
muxHandler.ServeHTTP(w, r)
}
return
}