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