diff --git a/beacon/blsync/block_sync.go b/beacon/blsync/block_sync.go
index a6252a55f14..d6ec9453fa7 100755
--- a/beacon/blsync/block_sync.go
+++ b/beacon/blsync/block_sync.go
@@ -162,3 +162,8 @@ func (s *beaconBlockSync) updateEventFeed() {
Finalized: finalizedHash,
})
}
+
+func (s *beaconBlockSync) getBlock(blockRoot common.Hash) *types.BeaconBlock {
+ block, _ := s.recentBlocks.Get(blockRoot)
+ return block
+}
diff --git a/beacon/blsync/client.go b/beacon/blsync/client.go
index 744f4691240..d1682bae449 100644
--- a/beacon/blsync/client.go
+++ b/beacon/blsync/client.go
@@ -28,6 +28,7 @@ import (
"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,12 +39,13 @@ type Client struct {
scheduler *request.Scheduler
blockSync *beaconBlockSync
engineRPC *rpc.Client
+ apiServer *api.BeaconApiServer
chainHeadSub event.Subscription
engineClient *engineClient
}
-func NewClient(config params.ClientConfig) *Client {
+func NewClient(config params.ClientConfig, execChain api.ExecChain) *Client {
// create data structures
var (
db = memorydb.New()
@@ -55,12 +57,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 +72,7 @@ func NewClient(config params.ClientConfig) *Client {
scheduler.RegisterModule(forwardSync, "forwardSync")
scheduler.RegisterModule(headSync, "headSync")
scheduler.RegisterModule(beaconBlockSync, "beaconBlockSync")
+ apiServer := api.NewBeaconApiServer(scheduler, checkpointStore, committeeChain, headTracker, beaconBlockSync.getBlock, execChain)
return &Client{
scheduler: scheduler,
@@ -76,6 +80,7 @@ func NewClient(config params.ClientConfig) *Client {
customHeader: config.CustomHeader,
config: &config,
blockSync: beaconBlockSync,
+ apiServer: apiServer,
}
}
@@ -83,6 +88,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,13 +99,14 @@ 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.scheduler.RegisterServer(request.NewServer(api.NewSyncServer(beaconApi), &mclock.System{}))
}
return nil
}
func (c *Client) Stop() error {
+ c.apiServer.Stop()
c.engineClient.stop()
c.chainHeadSub.Unsubscribe()
c.scheduler.Stop()
diff --git a/beacon/light/api/light_api.go b/beacon/light/api/client.go
similarity index 62%
rename from beacon/light/api/light_api.go
rename to beacon/light/api/client.go
index f9a5aae1532..2fca58f6182 100755
--- a/beacon/light/api/light_api.go
+++ b/beacon/light/api/client.go
@@ -29,90 +29,27 @@ import (
"time"
"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/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
}
-func NewBeaconLightApi(url string, customHeaders map[string]string) *BeaconLightApi {
- return &BeaconLightApi{
+func NewBeaconApiClient(url string, customHeaders map[string]string) *BeaconApiClient {
+ return &BeaconApiClient{
url: url,
client: &http.Client{
Timeout: time.Second * 10,
@@ -121,7 +58,7 @@ func NewBeaconLightApi(url string, customHeaders map[string]string) *BeaconLight
}
}
-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 +92,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 +132,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 +140,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 +152,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,24 +193,13 @@ 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
}
- // See data structure definition here:
- // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientbootstrap
- type bootstrapData struct {
- Version string `json:"version"`
- Data struct {
- Header jsonBeaconHeader `json:"header"`
- Committee *types.SerializedSyncCommittee `json:"current_sync_committee"`
- CommitteeBranch merkle.Values `json:"current_sync_committee_branch"`
- } `json:"data"`
- }
-
- var data bootstrapData
+ var data jsonBootstrapData
if err := json.Unmarshal(resp, &data); err != nil {
return nil, err
}
@@ -390,25 +226,16 @@ 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
}
- var beaconBlockMessage struct {
- Version string `json:"version"`
- Data struct {
- Message json.RawMessage `json:"message"`
- }
- }
- if err := json.Unmarshal(resp, &beaconBlockMessage); err != nil {
+ block := new(types.BeaconBlock)
+ if err := json.Unmarshal(resp, block); err != nil {
return nil, fmt.Errorf("invalid block json data: %v", err)
}
- block, err := types.BlockFromJSON(beaconBlockMessage.Version, beaconBlockMessage.Data.Message)
- if err != nil {
- return nil, err
- }
computedRoot := block.Root()
if computedRoot != blockRoot {
return nil, fmt.Errorf("Beacon block root hash mismatch (expected: %x, got: %x)", blockRoot, computedRoot)
@@ -438,7 +265,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 +377,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 +415,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..c5f838975d1
--- /dev/null
+++ b/beacon/light/api/server.go
@@ -0,0 +1,282 @@
+// 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/http"
+ "net/url"
+ "strconv"
+ "sync/atomic"
+
+ "github.com/donovanhide/eventsource"
+ "github.com/ethereum/go-ethereum/beacon/light"
+ "github.com/ethereum/go-ethereum/beacon/light/request"
+ "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/core"
+ "github.com/ethereum/go-ethereum/event"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/restapi"
+ "github.com/gorilla/mux"
+)
+
+type BeaconApiServer struct {
+ scheduler *request.Scheduler
+ checkpointStore *light.CheckpointStore
+ committeeChain *light.CommitteeChain
+ headTracker *light.HeadTracker
+ getBeaconBlock func(common.Hash) *types.BeaconBlock
+ execBlocks *lru.Cache[common.Hash, struct{}] // execution block root -> processed flag
+ eventServer *eventsource.Server
+ closeCh chan struct{}
+
+ lastEventId uint64
+ lastHeadInfo types.HeadInfo
+ lastOptimistic types.OptimisticUpdate
+ lastFinality types.FinalityUpdate
+}
+
+type ExecChain interface {
+ SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription
+}
+
+func NewBeaconApiServer(
+ scheduler *request.Scheduler,
+ checkpointStore *light.CheckpointStore,
+ committeeChain *light.CommitteeChain,
+ headTracker *light.HeadTracker,
+ getBeaconBlock func(common.Hash) *types.BeaconBlock,
+ execChain ExecChain) *BeaconApiServer {
+
+ eventServer := eventsource.NewServer()
+ eventServer.Register("headEvent", eventsource.NewSliceRepository())
+ s := &BeaconApiServer{
+ scheduler: scheduler,
+ checkpointStore: checkpointStore,
+ committeeChain: committeeChain,
+ headTracker: headTracker,
+ getBeaconBlock: getBeaconBlock,
+ eventServer: eventServer,
+ closeCh: make(chan struct{}),
+ }
+ if execChain != nil {
+ s.execBlocks = lru.NewCache[common.Hash, struct{}](100)
+ ch := make(chan core.ChainEvent, 1)
+ sub := execChain.SubscribeChainEvent(ch)
+ go func() {
+ defer sub.Unsubscribe()
+ for {
+ select {
+ case ev := <-ch:
+ s.execBlocks.Add(ev.Header.Hash(), struct{}{})
+ s.scheduler.Trigger()
+ case <-s.closeCh:
+ return
+ }
+ }
+ }()
+ }
+ return s
+}
+
+func (s *BeaconApiServer) Stop() {
+ close(s.closeCh)
+}
+
+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/{blockhash}", server.WrapHandler(s.handleBlocks, false, false, false)).Methods("GET")
+ router.HandleFunc("/eth/v1/events", s.eventServer.Handler("headEvent"))
+ }
+}
+
+func (s *BeaconApiServer) Process(requester request.Requester, events []request.Event) {
+ if head := s.headTracker.PrefetchHead(); head != s.lastHeadInfo {
+ if head != s.lastHeadInfo && s.getBeaconBlock(head.BlockRoot) != nil {
+ s.lastHeadInfo = head
+ s.publishHeadEvent(head)
+ }
+ }
+ if vh, ok := s.headTracker.ValidatedOptimistic(); ok && vh.Attested.Header != s.lastOptimistic.Attested.Header && s.canPublish(vh.Attested) {
+ s.lastOptimistic = vh
+ s.publishOptimisticUpdate(vh)
+ }
+ if fh, ok := s.headTracker.ValidatedFinality(); ok && fh.Finalized.Header != s.lastFinality.Finalized.Header && s.canPublish(fh.Attested) {
+ s.lastFinality = fh
+ s.publishFinalityUpdate(fh)
+ }
+}
+
+func (s *BeaconApiServer) canPublish(header types.HeaderWithExecProof) bool {
+ if s.getBeaconBlock(header.Hash()) == nil {
+ return false
+ }
+ if s.execBlocks != nil {
+ if _, ok := s.execBlocks.Get(header.PayloadHeader.BlockHash()); !ok {
+ return false
+ }
+ }
+ return true
+}
+
+func (s *BeaconApiServer) publishHeadEvent(headInfo types.HeadInfo) {
+ enc, err := json.Marshal(&jsonHeadEvent{Slot: common.Decimal(headInfo.Slot), Block: headInfo.BlockRoot})
+ if err != nil {
+ log.Error("Error encoding head event", "error", err)
+ return
+ }
+ s.publishEvent("head", string(enc))
+}
+
+func (s *BeaconApiServer) publishOptimisticUpdate(update types.OptimisticUpdate) {
+ enc, err := encodeOptimisticUpdate(update)
+ if err != nil {
+ log.Error("Error encoding optimistic head update", "error", err)
+ return
+ }
+ s.publishEvent("light_client_optimistic_update", string(enc))
+}
+
+func (s *BeaconApiServer) publishFinalityUpdate(update types.FinalityUpdate) {
+ enc, err := encodeFinalityUpdate(update)
+ if err != nil {
+ log.Error("Error encoding optimistic head update", "error", err)
+ return
+ }
+ s.publishEvent("light_client_finality_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) {
+ startStr, countStr := values.Get("start_period"), values.Get("count")
+ start, err := strconv.ParseUint(startStr, 10, 64)
+ if err != nil {
+ return nil, "invalid start_period parameter", http.StatusBadRequest
+ }
+ var count uint64
+ if countStr != "" {
+ count, err = strconv.ParseUint(countStr, 10, 64)
+ if err != nil {
+ return nil, "invalid count parameter", http.StatusBadRequest
+ }
+ } else {
+ count = 1
+ }
+ var updates []CommitteeUpdate
+ for period := start; period < start+count; period++ {
+ update := s.committeeChain.GetUpdate(period)
+ if update == nil {
+ continue
+ }
+ committee := s.committeeChain.GetCommittee(period + 1)
+ if committee == nil {
+ continue
+ }
+ updates = append(updates, CommitteeUpdate{
+ Update: *update,
+ NextSyncCommittee: *committee,
+ })
+ }
+ return updates, "", 0
+}
+
+func (s *BeaconApiServer) handleOptimisticUpdate(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ update, err := toJsonOptimisticUpdate(s.lastOptimistic)
+ if err != nil {
+ return nil, "error encoding optimistic update", http.StatusInternalServerError
+ }
+ return update, "", 0
+}
+
+func (s *BeaconApiServer) handleFinalityUpdate(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ update, err := toJsonFinalityUpdate(s.lastFinality)
+ if err != nil {
+ return nil, "error encoding finality update", http.StatusInternalServerError
+ }
+ return update, "", 0
+}
+
+func (s *BeaconApiServer) handleHeadHeader(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ block := s.getBeaconBlock(s.lastHeadInfo.BlockRoot)
+ if block == nil {
+ return nil, "unknown head block", http.StatusNotFound
+ }
+ header := block.Header()
+ var headerData jsonHeaderData
+ headerData.Data.Canonical = true
+ headerData.Data.Header.Message = header
+ headerData.Data.Root = header.Hash()
+ return headerData, "", 0
+}
+
+func (s *BeaconApiServer) handleBootstrap(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ hex, err := hexutil.Decode(vars["checkpointhash"])
+ if err != nil || len(hex) != common.HashLength {
+ return nil, "invalid checkpoint hash", http.StatusBadRequest
+ }
+ var checkpointHash common.Hash
+ copy(checkpointHash[:], hex)
+ checkpoint := s.checkpointStore.Get(checkpointHash)
+ if checkpoint == nil {
+ return nil, "unknown checkpoint", http.StatusNotFound
+ }
+ var response jsonBootstrapData
+ response.Version = checkpoint.Version
+ response.Data.Header.Beacon = checkpoint.Header
+ response.Data.CommitteeBranch = checkpoint.CommitteeBranch
+ response.Data.Committee = checkpoint.Committee
+ return response, "", 0
+}
+
+func (s *BeaconApiServer) handleBlocks(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
+ hex, err := hexutil.Decode(vars["blockhash"])
+ if err != nil || len(hex) != common.HashLength {
+ return nil, "invalid block hash", http.StatusBadRequest
+ }
+ var blockHash common.Hash
+ copy(blockHash[:], hex)
+ block := s.getBeaconBlock(blockHash)
+ if block == nil {
+ return nil, "unknown beacon block", http.StatusNotFound
+ }
+ return block, "", 0
+}
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..3fd4fcbce9f
--- /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 {
+ 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.Update.Version,
+ Data: committeeUpdateData{
+ Header: jsonBeaconHeader{Beacon: u.Update.AttestedHeader.Header},
+ NextSyncCommittee: u.NextSyncCommittee,
+ NextSyncCommitteeBranch: u.Update.NextSyncCommitteeBranch,
+ 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.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
+}
+
+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"`
+}
+
+// See data structure definition here:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientbootstrap
+type jsonBootstrapData struct {
+ Version string `json:"version"`
+ 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/checkpoint_store.go b/beacon/light/checkpoint_store.go
new file mode 100644
index 00000000000..7acb1c6e819
--- /dev/null
+++ b/beacon/light/checkpoint_store.go
@@ -0,0 +1,76 @@
+// 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 light
+
+import (
+ "github.com/ethereum/go-ethereum/beacon/types"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+var checkpointKey = []byte("checkpoint-") // block root -> RLP(types.BootstrapData)
+
+// CheckpointStore stores checkpoints in a database, identified by their hash.
+type CheckpointStore struct {
+ chain *CommitteeChain
+ db ethdb.KeyValueStore
+}
+
+func NewCheckpointStore(db ethdb.KeyValueStore, chain *CommitteeChain) *CheckpointStore {
+ return &CheckpointStore{
+ db: db,
+ chain: chain,
+ }
+}
+
+func getCheckpointKey(checkpoint common.Hash) []byte {
+ var (
+ kl = len(checkpointKey)
+ key = make([]byte, kl+32)
+ )
+ copy(key[:kl], checkpointKey)
+ copy(key[kl:], checkpoint[:])
+ return key
+}
+
+func (cs *CheckpointStore) Get(checkpoint common.Hash) *types.BootstrapData {
+ if enc, err := cs.db.Get(getCheckpointKey(checkpoint)); err == nil {
+ c := new(types.BootstrapData)
+ if err := rlp.DecodeBytes(enc, c); err != nil {
+ log.Error("Error decoding stored checkpoint", "error", err)
+ return nil
+ }
+ if committee := cs.chain.GetCommittee(c.Header.SyncPeriod()); committee != nil && committee.Root() == c.CommitteeRoot {
+ c.Committee = committee
+ return c
+ }
+ log.Error("Missing committee for stored checkpoint", "period", c.Header.SyncPeriod())
+ }
+ return nil
+}
+
+func (cs *CheckpointStore) Store(c *types.BootstrapData) {
+ enc, err := rlp.EncodeToBytes(c)
+ if err != nil {
+ log.Error("Error encoding checkpoint for storage", "error", err)
+ }
+ if err := cs.db.Put(getCheckpointKey(c.Header.Hash()), enc); err != nil {
+ log.Error("Error storing checkpoint in database", "error", err)
+ }
+}
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/beacon_block.go b/beacon/types/beacon_block.go
index a2e31d5abf6..1324e6a57c7 100644
--- a/beacon/types/beacon_block.go
+++ b/beacon/types/beacon_block.go
@@ -41,37 +41,40 @@ type blockObject interface {
// BeaconBlock represents a full block in the beacon chain.
type BeaconBlock struct {
+ Version string `json:"version"`
+ Data struct {
+ Message json.RawMessage `json:"message"`
+ }
blockObj blockObject
}
-// BlockFromJSON decodes a beacon block from JSON.
-func BlockFromJSON(forkName string, data []byte) (*BeaconBlock, error) {
- var obj blockObject
- switch forkName {
+// UnmarshalJSON implements json.Marshaler.
+func (b *BeaconBlock) UnmarshalJSON(input []byte) error {
+ if err := json.Unmarshal(input, b); err != nil {
+ return err
+ }
+ switch b.Version {
case "capella":
- obj = new(capella.BeaconBlock)
+ b.blockObj = new(capella.BeaconBlock)
case "deneb":
- obj = new(deneb.BeaconBlock)
+ b.blockObj = new(deneb.BeaconBlock)
case "electra":
- obj = new(electra.BeaconBlock)
+ b.blockObj = new(electra.BeaconBlock)
default:
- return nil, fmt.Errorf("unsupported fork: %s", forkName)
- }
- if err := json.Unmarshal(data, obj); err != nil {
- return nil, err
+ return fmt.Errorf("unsupported fork: %s", b.Version)
}
- return &BeaconBlock{obj}, nil
+ return json.Unmarshal(b.Data.Message, b.blockObj)
}
-// NewBeaconBlock wraps a ZRNT block.
+// NewBeaconBlock wraps a ZRNT block (only used for testing).
func NewBeaconBlock(obj blockObject) *BeaconBlock {
switch obj := obj.(type) {
case *capella.BeaconBlock:
- return &BeaconBlock{obj}
+ return &BeaconBlock{blockObj: obj}
case *deneb.BeaconBlock:
- return &BeaconBlock{obj}
+ return &BeaconBlock{blockObj: obj}
case *electra.BeaconBlock:
- return &BeaconBlock{obj}
+ return &BeaconBlock{blockObj: obj}
default:
panic(fmt.Errorf("unsupported block type %T", obj))
}
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/blsync/main.go b/cmd/blsync/main.go
index 39a94073045..ddd0694218b 100644
--- a/cmd/blsync/main.go
+++ b/cmd/blsync/main.go
@@ -72,7 +72,7 @@ func main() {
func sync(ctx *cli.Context) error {
// set up blsync
- client := blsync.NewClient(utils.MakeBeaconLightConfig(ctx))
+ client := blsync.NewClient(utils.MakeBeaconLightConfig(ctx), nil)
client.SetEngineRPC(makeRPCClient(ctx))
client.Start()
diff --git a/cmd/geth/config.go b/cmd/geth/config.go
index fcb315af979..bcddcf172d6 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)
@@ -298,8 +303,9 @@ func makeFullNode(ctx *cli.Context) *node.Node {
// Start blsync mode.
srv := rpc.NewServer()
srv.RegisterName("engine", catalyst.NewConsensusAPI(eth))
- blsyncer := blsync.NewClient(utils.MakeBeaconLightConfig(ctx))
+ blsyncer := blsync.NewClient(utils.MakeBeaconLightConfig(ctx), eth.BlockChain())
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)
+ }
+ }
+}