From 85445ac3b6b2e279d36a1d1cef440c13b05b792a Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Sun, 14 Jun 2026 11:13:39 +0200 Subject: [PATCH] eth/catalyst: wire up rest-api and update to new spec --- beacon/engine/ssz/constants.go | 39 +++++----- eth/catalyst/rest_backend.go | 125 +++++++++++++++++++++++++++++++++ eth/engineapi/backend.go | 8 ++- eth/engineapi/errors.go | 26 +++---- 4 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 eth/catalyst/rest_backend.go diff --git a/beacon/engine/ssz/constants.go b/beacon/engine/ssz/constants.go index 1ceb71173e..6befe4b26c 100644 --- a/beacon/engine/ssz/constants.go +++ b/beacon/engine/ssz/constants.go @@ -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. diff --git a/eth/catalyst/rest_backend.go b/eth/catalyst/rest_backend.go new file mode 100644 index 0000000000..d2ea1e4edd --- /dev/null +++ b/eth/catalyst/rest_backend.go @@ -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 . + +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), + } +} diff --git a/eth/engineapi/backend.go b/eth/engineapi/backend.go index 8132c826fa..36dee977bf 100644 --- a/eth/engineapi/backend.go +++ b/eth/engineapi/backend.go @@ -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. diff --git a/eth/engineapi/errors.go b/eth/engineapi/errors.go index b02ef82492..44346da507 100644 --- a/eth/engineapi/errors.go +++ b/eth/engineapi/errors.go @@ -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"