mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-24 21:56:43 +00:00
eth/catalyst: added rest-ssz engine api
This commit is contained in:
parent
1149f76dca
commit
b694357889
28 changed files with 3013 additions and 3 deletions
136
beacon/engine/ssz/attributes_forks.go
Normal file
136
beacon/engine/ssz/attributes_forks.go
Normal 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
238
beacon/engine/ssz/blobs.go
Normal 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
162
beacon/engine/ssz/bodies.go
Normal 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)
|
||||
}
|
||||
50
beacon/engine/ssz/constants.go
Normal file
50
beacon/engine/ssz/constants.go
Normal 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
|
||||
)
|
||||
236
beacon/engine/ssz/convert.go
Normal file
236
beacon/engine/ssz/convert.go
Normal 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
|
||||
}
|
||||
189
beacon/engine/ssz/envelopes.go
Normal file
189
beacon/engine/ssz/envelopes.go
Normal 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()
|
||||
}
|
||||
36
beacon/engine/ssz/optional.go
Normal file
36
beacon/engine/ssz/optional.go
Normal 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
|
||||
}
|
||||
150
beacon/engine/ssz/payload_amsterdam.go
Normal file
150
beacon/engine/ssz/payload_amsterdam.go
Normal 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)
|
||||
}
|
||||
185
beacon/engine/ssz/payload_forks.go
Normal file
185
beacon/engine/ssz/payload_forks.go
Normal 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)
|
||||
}
|
||||
135
beacon/engine/ssz/roundtrip_test.go
Normal file
135
beacon/engine/ssz/roundtrip_test.go
Normal 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
164
beacon/engine/ssz/shared.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
68
eth/engineapi/backend.go
Normal 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
149
eth/engineapi/blobs.go
Normal 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
107
eth/engineapi/bodies.go
Normal 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
|
||||
}
|
||||
57
eth/engineapi/capabilities.go
Normal file
57
eth/engineapi/capabilities.go
Normal 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
55
eth/engineapi/errors.go
Normal 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})
|
||||
}
|
||||
77
eth/engineapi/forkchoice.go
Normal file
77
eth/engineapi/forkchoice.go
Normal 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)
|
||||
}
|
||||
93
eth/engineapi/get_payload.go
Normal file
93
eth/engineapi/get_payload.go
Normal 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
39
eth/engineapi/identity.go
Normal 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
76
eth/engineapi/media.go
Normal 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
89
eth/engineapi/payloads.go
Normal 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
106
eth/engineapi/router.go
Normal 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, "")
|
||||
}
|
||||
}
|
||||
373
eth/engineapi/router_test.go
Normal file
373
eth/engineapi/router_test.go
Normal 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
3
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
18
node/node.go
18
node/node.go
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 != "" {
|
||||
if len(h.authSecret) != 0 {
|
||||
newJWTHandler(h.authSecret, muxHandler).ServeHTTP(w, r)
|
||||
} else {
|
||||
muxHandler.ServeHTTP(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue