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) + } + } +}