eth/catalyst: wire up rest-api and update to new spec

This commit is contained in:
MariusVanDerWijden 2026-06-14 11:13:39 +02:00
parent 8685b80f23
commit 85445ac3b6
No known key found for this signature in database
4 changed files with 166 additions and 32 deletions

View file

@ -22,23 +22,28 @@ 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
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
FieldElementsPerCell = 64 // EIP-7594
// A cell spans FIELD_ELEMENTS_PER_CELL field elements of the *extended*
// blob, so BytesPerCell = 64 * 32 = 2048. (Not BytesPerBlob/CellsPerExtBlob,
// which divides the original-blob byte count over the extended-blob cell
// count and halves the true size — see execution-apis refactor-ssz.md.)
BytesPerCell = FieldElementsPerCell * BytesPerFieldElement
MaxBalBytes = MaxBytesPerTx // EIP-7928 placeholder
MaxExecutionRequestsPerPayload = 1 << 8 // 256, EIP-7685
MaxBytesPerExecutionRequest = MaxBytesPerTx // placeholder
MaxVersionedHashesPerRequest = 128 // Osaka
MaxBlobsRequest = MaxVersionedHashesPerRequest
MaxBodiesRequest = 1 << 5 // 32, Shanghai
MaxErrorBytes = 1024
)
// Status enum values for PayloadStatus.

View file

@ -0,0 +1,125 @@
// 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 catalyst
import (
"context"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/engineapi"
"github.com/ethereum/go-ethereum/internal/version"
"github.com/ethereum/go-ethereum/params/forks"
)
// restBackend adapts the JSON-RPC ConsensusAPI to the engineapi.Backend
// contract that drives the REST + SSZ Engine API (execution-apis #793). Both
// surfaces share the same underlying logic; this adapter only translates
// shapes, it holds no state of its own.
type restBackend struct {
api *ConsensusAPI
}
// newRESTBackend wraps a ConsensusAPI as an engineapi.Backend.
func newRESTBackend(api *ConsensusAPI) engineapi.Backend {
return &restBackend{api: api}
}
func (b *restBackend) ForkchoiceUpdated(ctx context.Context, state engine.ForkchoiceStateV1, attrs *engine.PayloadAttributes, version engine.PayloadVersion) (engine.ForkChoiceResponse, error) {
return b.api.forkchoiceUpdated(ctx, state, attrs, version, false)
}
func (b *restBackend) NewPayload(ctx context.Context, data engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte) (engine.PayloadStatusV1, error) {
return b.api.newPayload(ctx, data, versionedHashes, beaconRoot, requests, false)
}
func (b *restBackend) GetPayload(id engine.PayloadID, allowedForks []forks.Fork) (*engine.ExecutionPayloadEnvelope, error) {
// full=true so the envelope carries the complete payload; versions=nil
// because the REST router selects the fork from the URL, not the payload
// id's embedded version. allowedForks enforces the URL fork's era.
return b.api.getPayload(id, true, nil, allowedForks)
}
func (b *restBackend) GetBlobs(hashes []common.Hash, cellProofs bool) ([]*engine.BlobAndProofV2, []*engine.BlobAndProofV1, error) {
if cellProofs {
v2, err := b.api.getBlobs(hashes, true)
return v2, nil, err
}
v1, err := b.api.GetBlobsV1(hashes)
return nil, v1, err
}
func (b *restBackend) BodiesByHash(hashes []common.Hash) ([]*types.Body, []uint64) {
chain := b.api.eth.BlockChain()
bodies := make([]*types.Body, len(hashes))
timestamps := make([]uint64, len(hashes))
for i, h := range hashes {
block := chain.GetBlockByHash(h)
if block == nil {
continue
}
bodies[i] = block.Body()
timestamps[i] = block.Time()
}
return bodies, timestamps
}
func (b *restBackend) BodiesByRange(from, count uint64) ([]*types.Body, []uint64) {
chain := b.api.eth.BlockChain()
// Truncate at head: blocks past the latest known block are omitted, never
// padded with nil (the spec's "no trailing nulls" rule for range queries).
head := chain.CurrentBlock().Number.Uint64()
if from > head {
return nil, nil
}
last := min(from+count-1, head)
n := last - from + 1
bodies := make([]*types.Body, 0, n)
timestamps := make([]uint64, 0, n)
for num := from; num <= last; num++ {
block := chain.GetBlockByNumber(num)
if block == nil {
// In-range but pruned: keep the slot so the entry reports
// available=false; only past-head blocks are dropped (above).
bodies = append(bodies, nil)
timestamps = append(timestamps, 0)
continue
}
bodies = append(bodies, block.Body())
timestamps = append(timestamps, block.Time())
}
return bodies, timestamps
}
func (b *restBackend) ForkFromTimestamp(ts uint64) forks.Fork {
return b.api.config().LatestFork(ts)
}
func (b *restBackend) ClientVersion() engine.ClientVersionV1 {
commit := make([]byte, 4)
if vcs, ok := version.VCS(); ok {
commit = common.FromHex(vcs.Commit)[0:4]
}
return engine.ClientVersionV1{
Code: engine.ClientCode,
Name: engine.ClientName,
Version: version.WithMeta,
Commit: hexutil.Encode(commit),
}
}

View file

@ -56,8 +56,12 @@ type Backend interface {
// 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 returns block bodies for [from, from+count), but the
// result MUST be truncated at the latest known block: blocks past head are
// omitted, not padded with nil. The spec's "no trailing nulls" rule means a
// range response has length min(count, head-from+1) for from <= head, and
// is empty for from > head. In-range-but-pruned blocks are still returned as
// nil entries (-> available=false); only past-head blocks are dropped.
BodiesByRange(from, count uint64) ([]*types.Body, []uint64)
// ForkFromTimestamp returns the fork active at timestamp ts.

View file

@ -23,19 +23,19 @@ import (
// 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"
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"