diff --git a/beacon/blsync/client.go b/beacon/blsync/client.go
index 744f4691240..636126b5f0f 100644
--- a/beacon/blsync/client.go
+++ b/beacon/blsync/client.go
@@ -24,10 +24,12 @@ import (
"github.com/ethereum/go-ethereum/beacon/params"
"github.com/ethereum/go-ethereum/beacon/types"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/ethdb/memorydb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/restapi"
"github.com/ethereum/go-ethereum/rpc"
)
@@ -38,6 +40,8 @@ type Client struct {
scheduler *request.Scheduler
blockSync *beaconBlockSync
engineRPC *rpc.Client
+ apiServer *api.BeaconApiServer
+ recentBlocks *lru.Cache[common.Hash, []byte]
chainHeadSub event.Subscription
engineClient *engineClient
@@ -55,12 +59,13 @@ func NewClient(config params.ClientConfig) *Client {
log.Error("Failed to save beacon checkpoint", "file", config.CheckpointFile, "checkpoint", checkpoint, "error", err)
}
})
+ checkpointStore = light.NewCheckpointStore(db, committeeChain)
)
headSync := sync.NewHeadSync(headTracker, committeeChain)
// set up scheduler and sync modules
scheduler := request.NewScheduler()
- checkpointInit := sync.NewCheckpointInit(committeeChain, config.Checkpoint)
+ checkpointInit := sync.NewCheckpointInit(committeeChain, checkpointStore, config.Checkpoint)
forwardSync := sync.NewForwardUpdateSync(committeeChain)
beaconBlockSync := newBeaconBlockSync(headTracker)
scheduler.RegisterTarget(headTracker)
@@ -69,6 +74,8 @@ func NewClient(config params.ClientConfig) *Client {
scheduler.RegisterModule(forwardSync, "forwardSync")
scheduler.RegisterModule(headSync, "headSync")
scheduler.RegisterModule(beaconBlockSync, "beaconBlockSync")
+ recentBlocks := lru.NewCache[common.Hash, []byte](4)
+ apiServer := api.NewBeaconApiServer(checkpointStore, committeeChain, headTracker, recentBlocks)
return &Client{
scheduler: scheduler,
@@ -76,6 +83,8 @@ func NewClient(config params.ClientConfig) *Client {
customHeader: config.CustomHeader,
config: &config,
blockSync: beaconBlockSync,
+ apiServer: apiServer,
+ recentBlocks: recentBlocks,
}
}
@@ -83,6 +92,10 @@ func (c *Client) SetEngineRPC(engine *rpc.Client) {
c.engineRPC = engine
}
+func (c *Client) RestAPI(server *restapi.Server) restapi.API {
+ return c.apiServer.RestAPI(server)
+}
+
func (c *Client) Start() error {
headCh := make(chan types.ChainHeadEvent, 16)
c.chainHeadSub = c.blockSync.SubscribeChainHead(headCh)
@@ -90,8 +103,8 @@ func (c *Client) Start() error {
c.scheduler.Start()
for _, url := range c.urls {
- beaconApi := api.NewBeaconLightApi(url, c.customHeader)
- c.scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{}))
+ beaconApi := api.NewBeaconApiClient(url, c.customHeader, c.recentBlocks)
+ c.scheduler.RegisterServer(request.NewServer(api.NewSyncServer(beaconApi), &mclock.System{}))
}
return nil
}
diff --git a/beacon/light/api/light_api.go b/beacon/light/api/client.go
similarity index 66%
rename from beacon/light/api/light_api.go
rename to beacon/light/api/client.go
index f9a5aae1532..0aeabbedd3e 100755
--- a/beacon/light/api/light_api.go
+++ b/beacon/light/api/client.go
@@ -30,98 +30,39 @@ import (
"github.com/donovanhide/eventsource"
"github.com/ethereum/go-ethereum/beacon/merkle"
- "github.com/ethereum/go-ethereum/beacon/params"
"github.com/ethereum/go-ethereum/beacon/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/log"
)
-var (
- ErrNotFound = errors.New("404 Not Found")
- ErrInternal = errors.New("500 Internal Server Error")
-)
-
-type CommitteeUpdate struct {
- Update types.LightClientUpdate
- NextSyncCommittee types.SerializedSyncCommittee
-}
-
-// See data structure definition here:
-// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientupdate
-type committeeUpdateJson struct {
- Version string `json:"version"`
- Data committeeUpdateData `json:"data"`
-}
-
-type committeeUpdateData struct {
- Header jsonBeaconHeader `json:"attested_header"`
- NextSyncCommittee types.SerializedSyncCommittee `json:"next_sync_committee"`
- NextSyncCommitteeBranch merkle.Values `json:"next_sync_committee_branch"`
- FinalizedHeader *jsonBeaconHeader `json:"finalized_header,omitempty"`
- FinalityBranch merkle.Values `json:"finality_branch,omitempty"`
- SyncAggregate types.SyncAggregate `json:"sync_aggregate"`
- SignatureSlot common.Decimal `json:"signature_slot"`
-}
-
-type jsonBeaconHeader struct {
- Beacon types.Header `json:"beacon"`
-}
-
-type jsonHeaderWithExecProof struct {
- Beacon types.Header `json:"beacon"`
- Execution json.RawMessage `json:"execution"`
- ExecutionBranch merkle.Values `json:"execution_branch"`
-}
-
-// UnmarshalJSON unmarshals from JSON.
-func (u *CommitteeUpdate) UnmarshalJSON(input []byte) error {
- var dec committeeUpdateJson
- if err := json.Unmarshal(input, &dec); err != nil {
- return err
- }
- u.NextSyncCommittee = dec.Data.NextSyncCommittee
- u.Update = types.LightClientUpdate{
- Version: dec.Version,
- AttestedHeader: types.SignedHeader{
- Header: dec.Data.Header.Beacon,
- Signature: dec.Data.SyncAggregate,
- SignatureSlot: uint64(dec.Data.SignatureSlot),
- },
- NextSyncCommitteeRoot: u.NextSyncCommittee.Root(),
- NextSyncCommitteeBranch: dec.Data.NextSyncCommitteeBranch,
- FinalityBranch: dec.Data.FinalityBranch,
- }
- if dec.Data.FinalizedHeader != nil {
- u.Update.FinalizedHeader = &dec.Data.FinalizedHeader.Beacon
- }
- return nil
-}
-
// fetcher is an interface useful for debug-harnessing the http api.
type fetcher interface {
Do(req *http.Request) (*http.Response, error)
}
-// BeaconLightApi requests light client information from a beacon node REST API.
+// BeaconApiClient requests light client information from a beacon node REST API.
// Note: all required API endpoints are currently only implemented by Lodestar.
-type BeaconLightApi struct {
+type BeaconApiClient struct {
url string
client fetcher
customHeaders map[string]string
+ recentBlocks *lru.Cache[common.Hash, []byte]
}
-func NewBeaconLightApi(url string, customHeaders map[string]string) *BeaconLightApi {
- return &BeaconLightApi{
+func NewBeaconApiClient(url string, customHeaders map[string]string, recentBlocks *lru.Cache[common.Hash, []byte]) *BeaconApiClient {
+ return &BeaconApiClient{
url: url,
client: &http.Client{
Timeout: time.Second * 10,
},
customHeaders: customHeaders,
+ recentBlocks: recentBlocks,
}
}
-func (api *BeaconLightApi) httpGet(path string, params url.Values) ([]byte, error) {
+func (api *BeaconApiClient) httpGet(path string, params url.Values) ([]byte, error) {
uri, err := api.buildURL(path, params)
if err != nil {
return nil, err
@@ -155,7 +96,7 @@ func (api *BeaconLightApi) httpGet(path string, params url.Values) ([]byte, erro
// equals update.NextSyncCommitteeRoot).
// Note that the results are validated but the update signature should be verified
// by the caller as its validity depends on the update chain.
-func (api *BeaconLightApi) GetBestUpdatesAndCommittees(firstPeriod, count uint64) ([]*types.LightClientUpdate, []*types.SerializedSyncCommittee, error) {
+func (api *BeaconApiClient) GetBestUpdatesAndCommittees(firstPeriod, count uint64) ([]*types.LightClientUpdate, []*types.SerializedSyncCommittee, error) {
resp, err := api.httpGet("/eth/v1/beacon/light_client/updates", map[string][]string{
"start_period": {strconv.FormatUint(firstPeriod, 10)},
"count": {strconv.FormatUint(count, 10)},
@@ -195,7 +136,7 @@ func (api *BeaconLightApi) GetBestUpdatesAndCommittees(firstPeriod, count uint64
//
// See data structure definition here:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
-func (api *BeaconLightApi) GetOptimisticUpdate() (types.OptimisticUpdate, error) {
+func (api *BeaconApiClient) GetOptimisticUpdate() (types.OptimisticUpdate, error) {
resp, err := api.httpGet("/eth/v1/beacon/light_client/optimistic_update", nil)
if err != nil {
return types.OptimisticUpdate{}, err
@@ -203,52 +144,11 @@ func (api *BeaconLightApi) GetOptimisticUpdate() (types.OptimisticUpdate, error)
return decodeOptimisticUpdate(resp)
}
-func decodeOptimisticUpdate(enc []byte) (types.OptimisticUpdate, error) {
- var data struct {
- Version string `json:"version"`
- Data struct {
- Attested jsonHeaderWithExecProof `json:"attested_header"`
- Aggregate types.SyncAggregate `json:"sync_aggregate"`
- SignatureSlot common.Decimal `json:"signature_slot"`
- } `json:"data"`
- }
- if err := json.Unmarshal(enc, &data); err != nil {
- return types.OptimisticUpdate{}, err
- }
- // Decode the execution payload headers.
- attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution)
- if err != nil {
- return types.OptimisticUpdate{}, fmt.Errorf("invalid attested header: %v", err)
- }
- if data.Data.Attested.Beacon.StateRoot == (common.Hash{}) {
- // workaround for different event encoding format in Lodestar
- if err := json.Unmarshal(enc, &data.Data); err != nil {
- return types.OptimisticUpdate{}, err
- }
- }
-
- if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize {
- return types.OptimisticUpdate{}, errors.New("invalid sync_committee_bits length")
- }
- if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize {
- return types.OptimisticUpdate{}, errors.New("invalid sync_committee_signature length")
- }
- return types.OptimisticUpdate{
- Attested: types.HeaderWithExecProof{
- Header: data.Data.Attested.Beacon,
- PayloadHeader: attestedExecHeader,
- PayloadBranch: data.Data.Attested.ExecutionBranch,
- },
- Signature: data.Data.Aggregate,
- SignatureSlot: uint64(data.Data.SignatureSlot),
- }, nil
-}
-
// GetFinalityUpdate fetches the latest available finality update.
//
// See data structure definition here:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientfinalityupdate
-func (api *BeaconLightApi) GetFinalityUpdate() (types.FinalityUpdate, error) {
+func (api *BeaconApiClient) GetFinalityUpdate() (types.FinalityUpdate, error) {
resp, err := api.httpGet("/eth/v1/beacon/light_client/finality_update", nil)
if err != nil {
return types.FinalityUpdate{}, err
@@ -256,60 +156,11 @@ func (api *BeaconLightApi) GetFinalityUpdate() (types.FinalityUpdate, error) {
return decodeFinalityUpdate(resp)
}
-func decodeFinalityUpdate(enc []byte) (types.FinalityUpdate, error) {
- var data struct {
- Version string `json:"version"`
- Data struct {
- Attested jsonHeaderWithExecProof `json:"attested_header"`
- Finalized jsonHeaderWithExecProof `json:"finalized_header"`
- FinalityBranch merkle.Values `json:"finality_branch"`
- Aggregate types.SyncAggregate `json:"sync_aggregate"`
- SignatureSlot common.Decimal `json:"signature_slot"`
- }
- }
- if err := json.Unmarshal(enc, &data); err != nil {
- return types.FinalityUpdate{}, err
- }
- // Decode the execution payload headers.
- attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution)
- if err != nil {
- return types.FinalityUpdate{}, fmt.Errorf("invalid attested header: %v", err)
- }
- finalizedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Finalized.Execution)
- if err != nil {
- return types.FinalityUpdate{}, fmt.Errorf("invalid finalized header: %v", err)
- }
- // Perform sanity checks.
- if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize {
- return types.FinalityUpdate{}, errors.New("invalid sync_committee_bits length")
- }
- if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize {
- return types.FinalityUpdate{}, errors.New("invalid sync_committee_signature length")
- }
-
- return types.FinalityUpdate{
- Version: data.Version,
- Attested: types.HeaderWithExecProof{
- Header: data.Data.Attested.Beacon,
- PayloadHeader: attestedExecHeader,
- PayloadBranch: data.Data.Attested.ExecutionBranch,
- },
- Finalized: types.HeaderWithExecProof{
- Header: data.Data.Finalized.Beacon,
- PayloadHeader: finalizedExecHeader,
- PayloadBranch: data.Data.Finalized.ExecutionBranch,
- },
- FinalityBranch: data.Data.FinalityBranch,
- Signature: data.Data.Aggregate,
- SignatureSlot: uint64(data.Data.SignatureSlot),
- }, nil
-}
-
// GetHeader fetches and validates the beacon header with the given blockRoot.
// If blockRoot is null hash then the latest head header is fetched.
// The values of the canonical and finalized flags are also returned. Note that
// these flags are not validated.
-func (api *BeaconLightApi) GetHeader(blockRoot common.Hash) (types.Header, bool, bool, error) {
+func (api *BeaconApiClient) GetHeader(blockRoot common.Hash) (types.Header, bool, bool, error) {
var blockId string
if blockRoot == (common.Hash{}) {
blockId = "head"
@@ -346,7 +197,7 @@ func (api *BeaconLightApi) GetHeader(blockRoot common.Hash) (types.Header, bool,
}
// GetCheckpointData fetches and validates bootstrap data belonging to the given checkpoint.
-func (api *BeaconLightApi) GetCheckpointData(checkpointHash common.Hash) (*types.BootstrapData, error) {
+func (api *BeaconApiClient) GetCheckpointData(checkpointHash common.Hash) (*types.BootstrapData, error) {
resp, err := api.httpGet(fmt.Sprintf("/eth/v1/beacon/light_client/bootstrap/0x%x", checkpointHash[:]), nil)
if err != nil {
return nil, err
@@ -390,7 +241,7 @@ func (api *BeaconLightApi) GetCheckpointData(checkpointHash common.Hash) (*types
return checkpoint, nil
}
-func (api *BeaconLightApi) GetBeaconBlock(blockRoot common.Hash) (*types.BeaconBlock, error) {
+func (api *BeaconApiClient) GetBeaconBlock(blockRoot common.Hash) (*types.BeaconBlock, error) {
resp, err := api.httpGet(fmt.Sprintf("/eth/v2/beacon/blocks/0x%x", blockRoot), nil)
if err != nil {
return nil, err
@@ -413,6 +264,9 @@ func (api *BeaconLightApi) GetBeaconBlock(blockRoot common.Hash) (*types.BeaconB
if computedRoot != blockRoot {
return nil, fmt.Errorf("Beacon block root hash mismatch (expected: %x, got: %x)", blockRoot, computedRoot)
}
+ if api.recentBlocks != nil {
+ api.recentBlocks.Add(blockRoot, resp)
+ }
return block, nil
}
@@ -438,7 +292,7 @@ type HeadEventListener struct {
// head updates and calls the specified callback functions when they are received.
// The callbacks are also called for the current head and optimistic head at startup.
// They are never called concurrently.
-func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func() {
+func (api *BeaconApiClient) StartHeadListener(listener HeadEventListener) func() {
var (
ctx, closeCtx = context.WithCancel(context.Background())
streamCh = make(chan *eventsource.Stream, 1)
@@ -550,7 +404,7 @@ func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func()
// startEventStream establishes an event stream. This will keep retrying until the stream has been
// established. It can only return nil when the context is canceled.
-func (api *BeaconLightApi) startEventStream(ctx context.Context, listener *HeadEventListener) *eventsource.Stream {
+func (api *BeaconApiClient) startEventStream(ctx context.Context, listener *HeadEventListener) *eventsource.Stream {
for retry := true; retry; retry = ctxSleep(ctx, 5*time.Second) {
log.Trace("Sending event subscription request")
uri, err := api.buildURL("/eth/v1/events", map[string][]string{"topics": {"head", "light_client_finality_update", "light_client_optimistic_update"}})
@@ -588,7 +442,7 @@ func ctxSleep(ctx context.Context, timeout time.Duration) (ok bool) {
}
}
-func (api *BeaconLightApi) buildURL(path string, params url.Values) (string, error) {
+func (api *BeaconApiClient) buildURL(path string, params url.Values) (string, error) {
uri, err := url.Parse(api.url)
if err != nil {
return "", err
diff --git a/beacon/light/api/server.go b/beacon/light/api/server.go
new file mode 100644
index 00000000000..033a18a2783
--- /dev/null
+++ b/beacon/light/api/server.go
@@ -0,0 +1,127 @@
+// Copyright 2025 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 detaiapi.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "net/url"
+ "strconv"
+ "sync/atomic"
+
+ "github.com/donovanhide/eventsource"
+ "github.com/ethereum/go-ethereum/beacon/light"
+ "github.com/ethereum/go-ethereum/beacon/types"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/lru"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/restapi"
+ "github.com/gorilla/mux"
+)
+
+type BeaconApiServer struct {
+ checkpointStore *light.CheckpointStore
+ committeeChain *light.CommitteeChain
+ headTracker *light.HeadTracker
+ recentBlocks *lru.Cache[common.Hash, []byte]
+ //headValidator *light.HeadValidator
+ eventServer *eventsource.Server
+ lastEventId uint64
+}
+
+func NewBeaconApiServer(
+ checkpointStore *light.CheckpointStore,
+ committeeChain *light.CommitteeChain,
+ headTracker *light.HeadTracker,
+ recentBlocks *lru.Cache[common.Hash, []byte]) *BeaconApiServer {
+
+ eventServer := eventsource.NewServer()
+ eventServer.Register("headEvent", eventsource.NewSliceRepository())
+ return &BeaconApiServer{
+ checkpointStore: checkpointStore,
+ committeeChain: committeeChain,
+ headTracker: headTracker,
+ recentBlocks: recentBlocks,
+ eventServer: eventServer,
+ }
+}
+
+func (s *BeaconApiServer) RestAPI(server *restapi.Server) restapi.API {
+ return func(router *mux.Router) {
+ router.HandleFunc("/eth/v1/beacon/light_client/updates", server.WrapHandler(s.handleUpdates, false, false, false)).Methods("GET")
+ router.HandleFunc("/eth/v1/beacon/light_client/optimistic_update", server.WrapHandler(s.handleOptimisticUpdate, false, false, false)).Methods("GET")
+ router.HandleFunc("/eth/v1/beacon/light_client/finality_update", server.WrapHandler(s.handleFinalityUpdate, false, false, false)).Methods("GET")
+ router.HandleFunc("/eth/v1/beacon/headers/head", server.WrapHandler(s.handleHeadHeader, false, false, false)).Methods("GET")
+ router.HandleFunc("/eth/v1/beacon/light_client/bootstrap/{checkpointhash}", server.WrapHandler(s.handleBootstrap, false, false, false)).Methods("GET")
+ router.HandleFunc("/eth/v1/beacon/blocks/{blockid}", server.WrapHandler(s.handleBlocks, false, false, false)).Methods("GET")
+ router.HandleFunc("/eth/v1/events", s.eventServer.Handler("headEvent"))
+ }
+}
+
+func (s *BeaconApiServer) PublishHeadEvent(slot uint64, blockRoot common.Hash) {
+ enc, err := json.Marshal(&jsonHeadEvent{Slot: common.Decimal(slot), Block: blockRoot})
+ if err != nil {
+ log.Error("Error encoding head event", "error", err)
+ return
+ }
+ s.publishEvent("head", string(enc))
+}
+
+func (s *BeaconApiServer) PublishOptimisticHeadUpdate(head types.OptimisticUpdate) {
+ enc, err := encodeOptimisticUpdate(head)
+ if err != nil {
+ log.Error("Error encoding optimistic head update", "error", err)
+ return
+ }
+ s.publishEvent("light_client_optimistic_update", string(enc))
+}
+
+type serverEvent struct {
+ id, event, data string
+}
+
+func (e *serverEvent) Id() string { return e.id }
+func (e *serverEvent) Event() string { return e.event }
+func (e *serverEvent) Data() string { return e.data }
+
+func (s *BeaconApiServer) publishEvent(event, data string) {
+ id := atomic.AddUint64(&s.lastEventId, 1)
+ s.eventServer.Publish([]string{"headEvent"}, &serverEvent{
+ id: strconv.FormatUint(id, 10),
+ event: event,
+ data: data,
+ })
+}
+
+func (s *BeaconApiServer) handleUpdates(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *BeaconApiServer) handleOptimisticUpdate(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *BeaconApiServer) handleFinalityUpdate(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *BeaconApiServer) handleHeadHeader(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *BeaconApiServer) handleBootstrap(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *BeaconApiServer) handleBlocks(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
diff --git a/beacon/light/api/api_server.go b/beacon/light/api/sync_server.go
similarity index 89%
rename from beacon/light/api/api_server.go
rename to beacon/light/api/sync_server.go
index 2579854d82c..9a9ca659902 100755
--- a/beacon/light/api/api_server.go
+++ b/beacon/light/api/sync_server.go
@@ -26,20 +26,20 @@ import (
"github.com/ethereum/go-ethereum/log"
)
-// ApiServer is a wrapper around BeaconLightApi that implements request.requestServer.
-type ApiServer struct {
- api *BeaconLightApi
+// SyncServer is a wrapper around BeaconApiClient that implements request.requestServer.
+type SyncServer struct {
+ api *BeaconApiClient
eventCallback func(event request.Event)
unsubscribe func()
}
-// NewApiServer creates a new ApiServer.
-func NewApiServer(api *BeaconLightApi) *ApiServer {
- return &ApiServer{api: api}
+// NewSyncServer creates a new SyncServer.
+func NewSyncServer(api *BeaconApiClient) *SyncServer {
+ return &SyncServer{api: api}
}
// Subscribe implements request.requestServer.
-func (s *ApiServer) Subscribe(eventCallback func(event request.Event)) {
+func (s *SyncServer) Subscribe(eventCallback func(event request.Event)) {
s.eventCallback = eventCallback
listener := HeadEventListener{
OnNewHead: func(slot uint64, blockRoot common.Hash) {
@@ -62,7 +62,7 @@ func (s *ApiServer) Subscribe(eventCallback func(event request.Event)) {
}
// SendRequest implements request.requestServer.
-func (s *ApiServer) SendRequest(id request.ID, req request.Request) {
+func (s *SyncServer) SendRequest(id request.ID, req request.Request) {
go func() {
var resp request.Response
var err error
@@ -101,7 +101,7 @@ func (s *ApiServer) SendRequest(id request.ID, req request.Request) {
// Unsubscribe implements request.requestServer.
// Note: Unsubscribe should not be called concurrently with Subscribe.
-func (s *ApiServer) Unsubscribe() {
+func (s *SyncServer) Unsubscribe() {
if s.unsubscribe != nil {
s.unsubscribe()
s.unsubscribe = nil
@@ -109,6 +109,6 @@ func (s *ApiServer) Unsubscribe() {
}
// Name implements request.Server
-func (s *ApiServer) Name() string {
+func (s *SyncServer) Name() string {
return s.api.url
}
diff --git a/beacon/light/api/types.go b/beacon/light/api/types.go
new file mode 100644
index 00000000000..095c07a23aa
--- /dev/null
+++ b/beacon/light/api/types.go
@@ -0,0 +1,297 @@
+// Copyright 2023 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 detaiapi.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package api
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/beacon/merkle"
+ "github.com/ethereum/go-ethereum/beacon/params"
+ "github.com/ethereum/go-ethereum/beacon/types"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/protolambda/zrnt/eth2/beacon/capella"
+)
+
+var (
+ ErrNotFound = errors.New("404 Not Found")
+ ErrInternal = errors.New("500 Internal Server Error")
+)
+
+type CommitteeUpdate struct {
+ Version string
+ Update types.LightClientUpdate
+ NextSyncCommittee types.SerializedSyncCommittee
+}
+
+type jsonBeaconHeader struct {
+ Beacon types.Header `json:"beacon"`
+}
+
+type jsonHeaderWithExecProof struct {
+ Beacon types.Header `json:"beacon"`
+ Execution json.RawMessage `json:"execution"`
+ ExecutionBranch merkle.Values `json:"execution_branch"`
+}
+
+// See data structure definition here:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientupdate
+type committeeUpdateJson struct {
+ Version string `json:"version"`
+ Data committeeUpdateData `json:"data"`
+}
+
+type committeeUpdateData struct {
+ Header jsonBeaconHeader `json:"attested_header"`
+ NextSyncCommittee types.SerializedSyncCommittee `json:"next_sync_committee"`
+ NextSyncCommitteeBranch merkle.Values `json:"next_sync_committee_branch"`
+ FinalizedHeader *jsonBeaconHeader `json:"finalized_header,omitempty"`
+ FinalityBranch merkle.Values `json:"finality_branch,omitempty"`
+ SyncAggregate types.SyncAggregate `json:"sync_aggregate"`
+ SignatureSlot common.Decimal `json:"signature_slot"`
+}
+
+func (u *CommitteeUpdate) MarshalJSON() ([]byte, error) {
+ enc := committeeUpdateJson{
+ Version: u.Version,
+ Data: committeeUpdateData{
+ Header: jsonBeaconHeader{Beacon: u.Update.AttestedHeader.Header},
+ NextSyncCommittee: u.NextSyncCommittee,
+ NextSyncCommitteeBranch: u.Update.NextSyncCommitteeBranch,
+ //FinalizedHeader *jsonBeaconHeader `json:"finalized_header,omitempty"`
+ //FinalityBranch merkle.Values `json:"finality_branch,omitempty"`
+ SyncAggregate: u.Update.AttestedHeader.Signature,
+ SignatureSlot: common.Decimal(u.Update.AttestedHeader.SignatureSlot),
+ },
+ }
+ if u.Update.FinalizedHeader != nil {
+ enc.Data.FinalizedHeader = &jsonBeaconHeader{Beacon: *u.Update.FinalizedHeader}
+ enc.Data.FinalityBranch = u.Update.FinalityBranch
+ }
+ return json.Marshal(&enc)
+}
+
+// UnmarshalJSON unmarshals from JSON.
+func (u *CommitteeUpdate) UnmarshalJSON(input []byte) error {
+ var dec committeeUpdateJson
+ if err := json.Unmarshal(input, &dec); err != nil {
+ return err
+ }
+ u.Version = dec.Version
+ u.NextSyncCommittee = dec.Data.NextSyncCommittee
+ u.Update = types.LightClientUpdate{
+ AttestedHeader: types.SignedHeader{
+ Header: dec.Data.Header.Beacon,
+ Signature: dec.Data.SyncAggregate,
+ SignatureSlot: uint64(dec.Data.SignatureSlot),
+ },
+ NextSyncCommitteeRoot: u.NextSyncCommittee.Root(),
+ NextSyncCommitteeBranch: dec.Data.NextSyncCommitteeBranch,
+ FinalityBranch: dec.Data.FinalityBranch,
+ }
+ if dec.Data.FinalizedHeader != nil {
+ u.Update.FinalizedHeader = &dec.Data.FinalizedHeader.Beacon
+ }
+ return nil
+}
+
+type jsonOptimisticUpdate struct {
+ Version string `json:"version"`
+ Data struct {
+ Attested jsonHeaderWithExecProof `json:"attested_header"`
+ Aggregate types.SyncAggregate `json:"sync_aggregate"`
+ SignatureSlot common.Decimal `json:"signature_slot"`
+ } `json:"data"`
+}
+
+func encodeOptimisticUpdate(update types.OptimisticUpdate) ([]byte, error) {
+ data, err := toJsonOptimisticUpdate(update)
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(&data)
+}
+
+func toJsonOptimisticUpdate(update types.OptimisticUpdate) (jsonOptimisticUpdate, error) {
+ var data jsonOptimisticUpdate
+ data.Version = update.Version
+ attestedHeader, err := types.ExecutionHeaderToJSON(update.Version, update.Attested.PayloadHeader)
+ if err != nil {
+ return jsonOptimisticUpdate{}, err
+ }
+ data.Data.Attested = jsonHeaderWithExecProof{
+ Beacon: update.Attested.Header,
+ Execution: attestedHeader,
+ ExecutionBranch: update.Attested.PayloadBranch,
+ }
+ data.Data.Aggregate = update.Signature
+ data.Data.SignatureSlot = common.Decimal(update.SignatureSlot)
+ return data, nil
+}
+
+func decodeOptimisticUpdate(enc []byte) (types.OptimisticUpdate, error) {
+ var data jsonOptimisticUpdate
+ if err := json.Unmarshal(enc, &data); err != nil {
+ return types.OptimisticUpdate{}, err
+ }
+ // Decode the execution payload headers.
+ attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution)
+ if err != nil {
+ return types.OptimisticUpdate{}, fmt.Errorf("invalid attested header: %v", err)
+ }
+ if data.Data.Attested.Beacon.StateRoot == (common.Hash{}) {
+ // workaround for different event encoding format in Lodestar
+ if err := json.Unmarshal(enc, &data.Data); err != nil {
+ return types.OptimisticUpdate{}, err
+ }
+ }
+
+ if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize {
+ return types.OptimisticUpdate{}, errors.New("invalid sync_committee_bits length")
+ }
+ if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize {
+ return types.OptimisticUpdate{}, errors.New("invalid sync_committee_signature length")
+ }
+ return types.OptimisticUpdate{
+ Version: data.Version,
+ Attested: types.HeaderWithExecProof{
+ Header: data.Data.Attested.Beacon,
+ PayloadHeader: attestedExecHeader,
+ PayloadBranch: data.Data.Attested.ExecutionBranch,
+ },
+ Signature: data.Data.Aggregate,
+ SignatureSlot: uint64(data.Data.SignatureSlot),
+ }, nil
+}
+
+type jsonFinalityUpdate struct {
+ Version string `json:"version"`
+ Data struct {
+ Attested jsonHeaderWithExecProof `json:"attested_header"`
+ Finalized jsonHeaderWithExecProof `json:"finalized_header"`
+ FinalityBranch merkle.Values `json:"finality_branch"`
+ Aggregate types.SyncAggregate `json:"sync_aggregate"`
+ SignatureSlot common.Decimal `json:"signature_slot"`
+ }
+}
+
+func encodeFinalityUpdate(update types.FinalityUpdate) ([]byte, error) {
+ data, err := toJsonFinalityUpdate(update)
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(&data)
+}
+
+func toJsonFinalityUpdate(update types.FinalityUpdate) (jsonFinalityUpdate, error) {
+ var data jsonFinalityUpdate
+ data.Version = update.Version
+ attestedHeader, err := types.ExecutionHeaderToJSON(update.Version, update.Attested.PayloadHeader)
+ if err != nil {
+ return jsonFinalityUpdate{}, err
+ }
+ finalizedHeader, err := types.ExecutionHeaderToJSON(update.Version, update.Finalized.PayloadHeader)
+ if err != nil {
+ return jsonFinalityUpdate{}, err
+ }
+ data.Data.Attested = jsonHeaderWithExecProof{
+ Beacon: update.Attested.Header,
+ Execution: attestedHeader,
+ ExecutionBranch: update.Attested.PayloadBranch,
+ }
+ data.Data.Finalized = jsonHeaderWithExecProof{
+ Beacon: update.Finalized.Header,
+ Execution: finalizedHeader,
+ ExecutionBranch: update.Finalized.PayloadBranch,
+ }
+ data.Data.FinalityBranch = update.FinalityBranch
+ data.Data.Aggregate = update.Signature
+ data.Data.SignatureSlot = common.Decimal(update.SignatureSlot)
+ return data, nil
+}
+
+func decodeFinalityUpdate(enc []byte) (types.FinalityUpdate, error) {
+ var data jsonFinalityUpdate
+ if err := json.Unmarshal(enc, &data); err != nil {
+ return types.FinalityUpdate{}, err
+ }
+ // Decode the execution payload headers.
+ attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution)
+ if err != nil {
+ return types.FinalityUpdate{}, fmt.Errorf("invalid attested header: %v", err)
+ }
+ finalizedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Finalized.Execution)
+ if err != nil {
+ return types.FinalityUpdate{}, fmt.Errorf("invalid finalized header: %v", err)
+ }
+ // Perform sanity checks.
+ if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize {
+ return types.FinalityUpdate{}, errors.New("invalid sync_committee_bits length")
+ }
+ if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize {
+ return types.FinalityUpdate{}, errors.New("invalid sync_committee_signature length")
+ }
+
+ return types.FinalityUpdate{
+ Version: data.Version,
+ Attested: types.HeaderWithExecProof{
+ Header: data.Data.Attested.Beacon,
+ PayloadHeader: attestedExecHeader,
+ PayloadBranch: data.Data.Attested.ExecutionBranch,
+ },
+ Finalized: types.HeaderWithExecProof{
+ Header: data.Data.Finalized.Beacon,
+ PayloadHeader: finalizedExecHeader,
+ PayloadBranch: data.Data.Finalized.ExecutionBranch,
+ },
+ FinalityBranch: data.Data.FinalityBranch,
+ Signature: data.Data.Aggregate,
+ SignatureSlot: uint64(data.Data.SignatureSlot),
+ }, nil
+}
+
+type jsonHeadEvent struct {
+ Slot common.Decimal `json:"slot"`
+ Block common.Hash `json:"block"`
+}
+
+type jsonBeaconBlock struct {
+ Data struct {
+ Message capella.BeaconBlock `json:"message"`
+ } `json:"data"`
+}
+
+type jsonBootstrapData struct {
+ Data struct {
+ Header jsonBeaconHeader `json:"header"`
+ Committee *types.SerializedSyncCommittee `json:"current_sync_committee"`
+ CommitteeBranch merkle.Values `json:"current_sync_committee_branch"`
+ } `json:"data"`
+}
+
+type jsonHeaderData struct {
+ Data struct {
+ Root common.Hash `json:"root"`
+ Canonical bool `json:"canonical"`
+ Header struct {
+ Message types.Header `json:"message"`
+ Signature hexutil.Bytes `json:"signature"`
+ } `json:"header"`
+ } `json:"data"`
+}
diff --git a/beacon/light/committee_chain.go b/beacon/light/committee_chain.go
index 4fa87785c08..40ff19c7065 100644
--- a/beacon/light/committee_chain.go
+++ b/beacon/light/committee_chain.go
@@ -334,6 +334,16 @@ func (s *CommitteeChain) addCommittee(period uint64, committee *types.Serialized
return nil
}
+func (s *CommitteeChain) GetCommittee(period uint64) *types.SerializedSyncCommittee {
+ committee, _ := s.committees.get(s.db, period)
+ return committee
+}
+
+func (s *CommitteeChain) GetUpdate(period uint64) *types.LightClientUpdate {
+ update, _ := s.updates.get(s.db, period)
+ return update
+}
+
// InsertUpdate adds a new update if possible.
func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error {
s.chainmu.Lock()
diff --git a/beacon/light/sync/update_sync.go b/beacon/light/sync/update_sync.go
index 9549ee59921..064fe675c14 100644
--- a/beacon/light/sync/update_sync.go
+++ b/beacon/light/sync/update_sync.go
@@ -39,10 +39,11 @@ type committeeChain interface {
// data belonging to the given checkpoint hash and initializes the committee chain
// if successful.
type CheckpointInit struct {
- chain committeeChain
- checkpointHash common.Hash
- locked request.ServerAndID
- initialized bool
+ chain committeeChain
+ checkpointHash common.Hash
+ checkpointStore *light.CheckpointStore
+ locked request.ServerAndID
+ initialized bool
// per-server state is used to track the state of requesting checkpoint header
// info. Part of this info (canonical and finalized state) is not validated
// and therefore it is requested from each server separately after it has
@@ -71,11 +72,12 @@ type serverState struct {
}
// NewCheckpointInit creates a new CheckpointInit.
-func NewCheckpointInit(chain committeeChain, checkpointHash common.Hash) *CheckpointInit {
+func NewCheckpointInit(chain committeeChain, checkpointStore *light.CheckpointStore, checkpointHash common.Hash) *CheckpointInit {
return &CheckpointInit{
- chain: chain,
- checkpointHash: checkpointHash,
- serverState: make(map[request.Server]serverState),
+ chain: chain,
+ checkpointHash: checkpointHash,
+ checkpointStore: checkpointStore,
+ serverState: make(map[request.Server]serverState),
}
}
@@ -100,6 +102,7 @@ func (s *CheckpointInit) Process(requester request.Requester, events []request.E
if resp != nil {
if checkpoint := resp.(*types.BootstrapData); checkpoint.Header.Hash() == common.Hash(req.(ReqCheckpointData)) {
s.chain.CheckpointInit(*checkpoint)
+ s.checkpointStore.Store(checkpoint)
s.initialized = true
return
}
diff --git a/beacon/types/exec_header.go b/beacon/types/exec_header.go
index ae79b008419..9dfdfa33976 100644
--- a/beacon/types/exec_header.go
+++ b/beacon/types/exec_header.go
@@ -56,6 +56,17 @@ func ExecutionHeaderFromJSON(forkName string, data []byte) (*ExecutionHeader, er
return &ExecutionHeader{obj: obj}, nil
}
+func ExecutionHeaderToJSON(forkName string, header *ExecutionHeader) ([]byte, error) {
+ switch forkName {
+ case "capella":
+ return json.Marshal(header.obj.(*capella.ExecutionPayloadHeader))
+ case "deneb", "electra": // note: the payload type was not changed in electra
+ return json.Marshal(header.obj.(*deneb.ExecutionPayloadHeader))
+ default:
+ return nil, fmt.Errorf("unsupported fork: %s", forkName)
+ }
+}
+
func NewExecutionHeader(obj headerObject) *ExecutionHeader {
switch obj.(type) {
case *capella.ExecutionPayloadHeader:
diff --git a/beacon/types/light_sync.go b/beacon/types/light_sync.go
index 128ee77f1ba..f2f18dcba93 100644
--- a/beacon/types/light_sync.go
+++ b/beacon/types/light_sync.go
@@ -163,6 +163,7 @@ func (h *HeaderWithExecProof) Validate() error {
// See data structure definition here:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
type OptimisticUpdate struct {
+ Version string
Attested HeaderWithExecProof
// Sync committee BLS signature aggregate
Signature SyncAggregate
diff --git a/cmd/geth/config.go b/cmd/geth/config.go
index fcb315af979..4a364cb9218 100644
--- a/cmd/geth/config.go
+++ b/cmd/geth/config.go
@@ -44,6 +44,7 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/node"
+ "github.com/ethereum/go-ethereum/restapi"
"github.com/ethereum/go-ethereum/rpc"
"github.com/naoina/toml"
"github.com/urfave/cli/v2"
@@ -259,6 +260,10 @@ func makeFullNode(ctx *cli.Context) *node.Node {
})
}
+ // Register REST API.
+ restServer := restapi.NewServer(stack)
+ restServer.Register(restapi.ExecutionAPI(restServer, backend))
+
// Configure log filter RPC API.
filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)
@@ -300,6 +305,7 @@ func makeFullNode(ctx *cli.Context) *node.Node {
srv.RegisterName("engine", catalyst.NewConsensusAPI(eth))
blsyncer := blsync.NewClient(utils.MakeBeaconLightConfig(ctx))
blsyncer.SetEngineRPC(rpc.DialInProc(srv))
+ restServer.Register(blsyncer.RestAPI(restServer))
stack.RegisterLifecycle(blsyncer)
} else {
// Launch the engine API for interacting with external consensus client.
diff --git a/go.mod b/go.mod
index c91cc81d21c..2c75b96275a 100644
--- a/go.mod
+++ b/go.mod
@@ -102,6 +102,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/deepmap/oapi-codegen v1.6.0 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
+ github.com/elnormous/contenttype v1.0.4 // indirect
github.com/emicklei/dot v1.6.2 // indirect
github.com/fjl/gencodec v0.1.0 // indirect
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect
@@ -113,6 +114,7 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
+ github.com/gorilla/mux v1.8.1 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kilic/bls12-381 v0.1.0 // indirect
diff --git a/go.sum b/go.sum
index 779bcde8467..bf751e101c8 100644
--- a/go.sum
+++ b/go.sum
@@ -109,6 +109,8 @@ github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 h1:+3HCtB74++ClLy8GgjU
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
+github.com/elnormous/contenttype v1.0.4 h1:FjmVNkvQOGqSX70yvocph7keC8DtmJaLzTTq6ZOQCI8=
+github.com/elnormous/contenttype v1.0.4/go.mod h1:5KTOW8m1kdX1dLMiUJeN9szzR2xkngiv2K+RVZwWBbI=
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum/c-kzg-4844/v2 v2.1.3 h1:DQ21UU0VSsuGy8+pcMJHDS0CV1bKmJmxsJYK8l3MiLU=
@@ -183,6 +185,8 @@ github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8q
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0=
diff --git a/restapi/exec_api.go b/restapi/exec_api.go
new file mode 100644
index 00000000000..8777f40e2c6
--- /dev/null
+++ b/restapi/exec_api.go
@@ -0,0 +1,197 @@
+// Copyright 2025 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 restapi
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+
+ "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/params/forks"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/gorilla/mux"
+)
+
+type execApiServer struct {
+ apiBackend backend
+}
+
+func ExecutionAPI(server *Server, backend backend) API {
+ api := execApiServer{apiBackend: backend}
+ return func(router *mux.Router) {
+ router.HandleFunc("/eth/v1/exec/headers/{blockid}", server.WrapHandler(api.handleHeaders, false, false, true)).Methods("GET")
+ router.HandleFunc("/eth/v1/exec/blocks", server.WrapHandler(api.handleBlocks, false, false, true)).Methods("GET")
+ router.HandleFunc("/eth/v1/exec/block_receipts", server.WrapHandler(api.handleBlockReceipts, false, false, true)).Methods("GET")
+ router.HandleFunc("/eth/v1/exec/transaction", server.WrapHandler(api.handleTransaction, false, false, true)).Methods("GET")
+ router.HandleFunc("/eth/v1/exec/transaction_by_index", server.WrapHandler(api.handleTxByIndex, false, false, true)).Methods("GET")
+ router.HandleFunc("/eth/v1/exec/receipt_by_index", server.WrapHandler(api.handleReceiptByIndex, false, false, true)).Methods("GET")
+ router.HandleFunc("/eth/v1/exec/state", server.WrapHandler(api.handleState, true, true, true)).Methods("POST")
+ router.HandleFunc("/eth/v1/exec/call", server.WrapHandler(api.handleCall, true, true, true)).Methods("POST")
+ router.HandleFunc("/eth/v1/exec/send_transaction", server.WrapHandler(api.handleSendTransaction, true, true, true)).Methods("POST")
+ router.HandleFunc("/eth/v1/exec/history", server.WrapHandler(api.handleHistory, false, false, true)).Methods("GET")
+ router.HandleFunc("/eth/v1/exec/transaction_position", server.WrapHandler(api.handleTxPosition, false, false, true)).Methods("GET")
+ router.HandleFunc("/eth/v1/exec/logs", server.WrapHandler(api.handleLogs, false, false, true)).Methods("GET")
+ }
+}
+
+type blockId struct {
+ hash common.Hash
+ number uint64
+}
+
+func (b *blockId) isHash() bool {
+ return b.hash != (common.Hash{})
+}
+
+func getBlockId(id string) (blockId, bool) {
+ if hex, err := hexutil.Decode(id); err == nil {
+ if len(hex) != common.HashLength {
+ return blockId{}, false
+ }
+ var b blockId
+ copy(b.hash[:], hex)
+ return b, true
+ }
+ if number, err := strconv.ParseUint(id, 10, 64); err == nil {
+ return blockId{number: number}, true
+ }
+ return blockId{}, false
+}
+
+// forkId returns the fork corresponding to the given header.
+// Note that frontier thawing and difficulty bomb adjustments are ignored according
+// to the API specification as they do not affect the interpretation of the
+// returned data structures.
+func (s *execApiServer) forkId(header *types.Header) forks.Fork {
+ c := s.apiBackend.ChainConfig()
+ switch {
+ case header.Difficulty.Sign() == 0:
+ return c.LatestFork(header.Time)
+ case c.IsLondon(header.Number):
+ return forks.London
+ case c.IsBerlin(header.Number):
+ return forks.Berlin
+ case c.IsIstanbul(header.Number):
+ return forks.Istanbul
+ case c.IsPetersburg(header.Number):
+ return forks.Petersburg
+ case c.IsConstantinople(header.Number):
+ return forks.Constantinople
+ case c.IsByzantium(header.Number):
+ return forks.Byzantium
+ case c.IsEIP155(header.Number):
+ return forks.SpuriousDragon
+ case c.IsEIP150(header.Number):
+ return forks.TangerineWhistle
+ case c.IsDAOFork(header.Number):
+ return forks.DAO
+ case c.IsHomestead(header.Number):
+ return forks.Homestead
+ default:
+ return forks.Frontier
+ }
+}
+
+func (s *execApiServer) forkName(header *types.Header) string {
+ return strings.ToLower(s.forkId(header).String())
+}
+
+func (s *execApiServer) handleHeaders(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ type headerResponse struct {
+ Version string `json:"version"`
+ Data *types.Header `json:"data"`
+ }
+ var (
+ amount int
+ response []headerResponse
+ err error
+ )
+ id, ok := getBlockId(vars["blockid"])
+ if !ok {
+ return nil, "invalid block id", http.StatusBadRequest
+ }
+ if s := values.Get("amount"); s != "" {
+ amount, err = strconv.Atoi(s)
+ if err != nil || amount <= 0 {
+ return nil, "invalid amount", http.StatusBadRequest
+ }
+ } else {
+ amount = 1
+ }
+
+ response = make([]headerResponse, amount)
+ for i := amount - 1; i >= 0; i-- {
+ if id.isHash() {
+ response[i].Data, err = s.apiBackend.HeaderByHash(ctx, id.hash)
+ } else {
+ response[i].Data, err = s.apiBackend.HeaderByNumber(ctx, rpc.BlockNumber(id.number))
+ }
+ if errors.Is(err, context.Canceled) {
+ return nil, "request timeout", http.StatusRequestTimeout
+ }
+ if response[i].Data == nil {
+ return nil, "not available", http.StatusNotFound
+ }
+ response[i].Version = s.forkName(response[i].Data)
+ if response[i].Data.Number.Uint64() == 0 {
+ response = response[i:]
+ break
+ }
+ id = blockId{hash: response[i].Data.ParentHash}
+ }
+ return response, "", 0
+}
+
+func (s *execApiServer) handleBlocks(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *execApiServer) handleBlockReceipts(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *execApiServer) handleTransaction(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *execApiServer) handleTxByIndex(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *execApiServer) handleReceiptByIndex(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *execApiServer) handleState(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *execApiServer) handleCall(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
+func (s *execApiServer) handleHistory(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+} // Requires EIP-7745
+func (s *execApiServer) handleTxPosition(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+} // Requires EIP-7745
+func (s *execApiServer) handleLogs(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+} // Requires EIP-7745
+func (s *execApiServer) handleSendTransaction(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ panic("TODO")
+}
diff --git a/restapi/server.go b/restapi/server.go
new file mode 100644
index 00000000000..5423b11f931
--- /dev/null
+++ b/restapi/server.go
@@ -0,0 +1,151 @@
+// Copyright 2025 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 restapi
+
+import (
+ "context"
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+
+ "github.com/elnormous/contenttype"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/node"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/gorilla/mux"
+)
+
+type Server struct {
+ router *mux.Router
+}
+
+type API func(*mux.Router)
+
+type backend interface {
+ HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
+ HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error)
+ ChainConfig() *params.ChainConfig
+}
+
+type WrappedHandler func(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int)
+
+func NewServer(node *node.Node) *Server {
+ s := &Server{
+ router: mux.NewRouter(),
+ }
+ node.RegisterHandler("REST API", "/eth/", s.router)
+ return s
+}
+
+func (s *Server) Register(regAPI API) {
+ regAPI(s.router)
+}
+
+func mediaType(mt contenttype.MediaType, allowBinary bool) (binary, valid bool) {
+ switch {
+ case mt.Type == "" && mt.Subtype == "":
+ return false, true // if content type is not specified then assume JSON
+ case mt.Type == "application" && mt.Subtype == "json":
+ return false, true
+ case mt.Type == "application" && mt.Subtype == "octet-stream":
+ return allowBinary, allowBinary
+ default:
+ return false, false
+ }
+}
+
+var allAvailableMediaTypes = []contenttype.MediaType{
+ contenttype.NewMediaType("application/json"),
+ contenttype.NewMediaType("application/octet-stream"),
+}
+
+func (s *Server) WrapHandler(handler WrappedHandler, expectBody, allowRlpBody, allowRlpResponse bool) func(resp http.ResponseWriter, req *http.Request) {
+ return func(resp http.ResponseWriter, req *http.Request) {
+ var decodeBody func(*any) error
+ if expectBody {
+ contentType, err := contenttype.GetMediaType(req)
+ if err != nil {
+ http.Error(resp, "invalid content type", http.StatusUnsupportedMediaType)
+ return
+ }
+ binary, valid := mediaType(contentType, allowRlpBody)
+ if !valid {
+ http.Error(resp, "invalid content type", http.StatusUnsupportedMediaType)
+ return
+ }
+ if req.Body == nil {
+ http.Error(resp, "missing request body", http.StatusBadRequest)
+ return
+ }
+ data, err := ioutil.ReadAll(req.Body)
+ if err != nil {
+ http.Error(resp, "could not read request body", http.StatusInternalServerError)
+ return
+ }
+ if binary {
+ decodeBody = func(body *any) error {
+ return rlp.DecodeBytes(data, body)
+ }
+ } else {
+ decodeBody = func(body *any) error {
+ return json.Unmarshal(data, body)
+ }
+ }
+ }
+
+ availableMediaTypes := allAvailableMediaTypes
+ if !allowRlpResponse {
+ availableMediaTypes = availableMediaTypes[:1]
+ }
+ acceptType, _, err := contenttype.GetAcceptableMediaType(req, availableMediaTypes)
+ if err != nil {
+ http.Error(resp, "invalid accepted media type", http.StatusNotAcceptable)
+ return
+ }
+ binary, valid := mediaType(acceptType, allowRlpResponse)
+ if !valid {
+ http.Error(resp, "invalid accepted media type", http.StatusNotAcceptable)
+ return
+ }
+ response, errorStr, errorCode := handler(req.Context(), req.URL.Query(), mux.Vars(req), decodeBody)
+ if errorCode != 0 {
+ http.Error(resp, errorStr, errorCode)
+ return
+ }
+ if binary {
+ respRlp, err := rlp.EncodeToBytes(response)
+ if err != nil {
+ http.Error(resp, "response encoding error", http.StatusInternalServerError)
+ return
+ }
+ resp.Header().Set("content-type", "application/octet-stream")
+ resp.Write(respRlp)
+ } else {
+ respJson, err := json.Marshal(response)
+ if err != nil {
+ http.Error(resp, "response encoding error", http.StatusInternalServerError)
+ return
+ }
+ resp.Header().Set("content-type", "application/json")
+ resp.Write(respJson)
+ }
+ }
+}