This commit is contained in:
Felföldi Zsolt 2025-12-02 14:30:21 -07:00 committed by GitHub
commit 09f49bb76c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1388 additions and 250 deletions

View file

@ -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
}

View file

@ -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,8 @@ 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)
scheduler.RegisterModule(apiServer, "apiServer")
return &Client{
scheduler: scheduler,
@ -76,6 +81,7 @@ func NewClient(config params.ClientConfig) *Client {
customHeader: config.CustomHeader,
config: &config,
blockSync: beaconBlockSync,
apiServer: apiServer,
}
}
@ -83,6 +89,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 +100,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()

View file

@ -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

289
beacon/light/api/server.go Normal file
View file

@ -0,0 +1,289 @@
// 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 <http://www.gnu.org/licenses/>.
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/v2/beacon/blocks/{blockhash}", server.WrapHandler(s.handleBlocks, false, false, false)).Methods("GET")
router.HandleFunc("/eth/v1/events", server.WrapEventHandler(s.eventServer.Handler("headEvent")))
}
}
func (s *BeaconApiServer) Process(requester request.Requester, events []request.Event) {
if head := s.headTracker.PrefetchHead(); 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) {
if s.lastOptimistic.Attested.Header == (types.Header{}) {
return nil, "no optimistic update available", http.StatusNotFound
}
update, err := encodeOptimisticUpdate(s.lastOptimistic)
if err != nil {
return nil, "error encoding optimistic update", http.StatusInternalServerError
}
return json.RawMessage(update), "", 0
}
func (s *BeaconApiServer) handleFinalityUpdate(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
if s.lastFinality.Attested.Header == (types.Header{}) {
return nil, "no finality update available", http.StatusNotFound
}
update, err := encodeFinalityUpdate(s.lastFinality)
if err != nil {
return nil, "error encoding finality update", http.StatusInternalServerError
}
return json.RawMessage(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.ExecutionOptimistic = block.ExecutionOptimistic
headerData.Finalized = block.Finalized
headerData.Data.Canonical = true
headerData.Data.Header.Message = header
headerData.Data.Header.Signature = block.Data.Signature
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
}

View file

@ -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
}

313
beacon/light/api/types.go Normal file
View file

@ -0,0 +1,313 @@
// 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 <http://www.gnu.org/licenses/>.
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 jsonHeaderWithExecProof `json:"attested_header"`
NextSyncCommittee types.SerializedSyncCommittee `json:"next_sync_committee"`
NextSyncCommitteeBranch merkle.Values `json:"next_sync_committee_branch"`
FinalizedHeader *jsonHeaderWithExecProof `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) {
execHeader, err := types.ExecutionHeaderToJSON(u.Update.Version, u.Update.AttestedHeader.PayloadHeader)
if err != nil {
return nil, err
}
enc := committeeUpdateJson{
Version: u.Update.Version,
Data: committeeUpdateData{
Header: jsonHeaderWithExecProof{
Beacon: u.Update.AttestedHeader.Header,
Execution: execHeader,
ExecutionBranch: u.Update.AttestedHeader.PayloadBranch,
},
NextSyncCommittee: u.NextSyncCommittee,
NextSyncCommitteeBranch: u.Update.NextSyncCommitteeBranch,
SyncAggregate: u.Update.Signature,
SignatureSlot: common.Decimal(u.Update.SignatureSlot),
},
}
if u.Update.FinalizedHeader != nil {
execHeader, err := types.ExecutionHeaderToJSON(u.Update.Version, u.Update.FinalizedHeader.PayloadHeader)
if err != nil {
return nil, err
}
enc.Data.FinalizedHeader = &jsonHeaderWithExecProof{
Beacon: u.Update.FinalizedHeader.Header,
Execution: execHeader,
ExecutionBranch: u.Update.FinalizedHeader.PayloadBranch,
}
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
execHeader, err := types.ExecutionHeaderFromJSON(dec.Version, dec.Data.Header.Execution)
if err != nil {
return err
}
u.Update = types.LightClientUpdate{
Version: dec.Version,
AttestedHeader: types.HeaderWithExecProof{
Header: dec.Data.Header.Beacon,
PayloadHeader: execHeader,
PayloadBranch: dec.Data.Header.ExecutionBranch,
},
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 {
execHeader, err := types.ExecutionHeaderFromJSON(dec.Version, dec.Data.FinalizedHeader.Execution)
if err != nil {
return err
}
u.Update.FinalizedHeader = &types.HeaderWithExecProof{
Header: dec.Data.FinalizedHeader.Beacon,
PayloadHeader: execHeader,
PayloadBranch: dec.Data.FinalizedHeader.ExecutionBranch,
}
}
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) {
var data jsonOptimisticUpdate
data.Version = update.Version
attestedHeader, err := types.ExecutionHeaderToJSON(update.Version, update.Attested.PayloadHeader)
if err != nil {
return nil, 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 json.Marshal(&data)
}
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"`
} `json:"data"`
}
func encodeFinalityUpdate(update types.FinalityUpdate) ([]byte, error) {
var data jsonFinalityUpdate
data.Version = update.Version
attestedHeader, err := types.ExecutionHeaderToJSON(update.Version, update.Attested.PayloadHeader)
if err != nil {
return nil, err
}
finalizedHeader, err := types.ExecutionHeaderToJSON(update.Version, update.Finalized.PayloadHeader)
if err != nil {
return nil, 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 json.Marshal(&data)
}
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 {
ExecutionOptimistic bool `json:"execution_optimistic"`
Finalized bool `json:"finalized"`
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"`
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

View file

@ -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()
@ -519,7 +529,7 @@ func (s *CommitteeChain) verifyUpdate(update *types.LightClientUpdate) (bool, er
// verification. Though in reality SignatureSlot is always bigger than update.Header.Slot,
// setting them as equal here enforces the rule that they have to be in the same sync
// period in order for the light client update proof to be meaningful.
ok, age, err := s.verifySignedHeader(update.AttestedHeader)
ok, age, err := s.verifySignedHeader(update.SignedHeader())
if age < 0 {
log.Warn("Future committee update received", "age", age)
}

View file

@ -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
}

View file

@ -36,15 +36,16 @@ func GenerateTestCommittee() *types.SerializedSyncCommittee {
func GenerateTestUpdate(config *params.ChainConfig, period uint64, committee, nextCommittee *types.SerializedSyncCommittee, signerCount int, finalizedHeader bool) *types.LightClientUpdate {
update := new(types.LightClientUpdate)
update.NextSyncCommitteeRoot = nextCommittee.Root()
var attestedHeader types.Header
if finalizedHeader {
update.FinalizedHeader = new(types.Header)
update.FinalizedHeader = new(types.HeaderWithExecProof)
*update.FinalizedHeader, update.NextSyncCommitteeBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+100, params.StateIndexNextSyncCommittee(""), merkle.Value(update.NextSyncCommitteeRoot))
attestedHeader, update.FinalityBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+200, params.StateIndexFinalBlock(""), merkle.Value(update.FinalizedHeader.Hash()))
update.AttestedHeader, update.FinalityBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+200, params.StateIndexFinalBlock(""), merkle.Value(update.FinalizedHeader.Hash()))
} else {
attestedHeader, update.NextSyncCommitteeBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+2000, params.StateIndexNextSyncCommittee(""), merkle.Value(update.NextSyncCommitteeRoot))
update.AttestedHeader, update.NextSyncCommitteeBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+2000, params.StateIndexNextSyncCommittee(""), merkle.Value(update.NextSyncCommitteeRoot))
}
update.AttestedHeader = GenerateTestSignedHeader(attestedHeader, config, committee, attestedHeader.Slot+1, signerCount)
signedHeader := GenerateTestSignedHeader(update.AttestedHeader.Header, config, committee, update.AttestedHeader.Slot+1, signerCount)
update.Signature = signedHeader.Signature
update.SignatureSlot = signedHeader.SignatureSlot
return update
}
@ -65,7 +66,7 @@ func GenerateTestSignedHeader(header types.Header, config *params.ChainConfig, c
func GenerateTestCheckpoint(period uint64, committee *types.SerializedSyncCommittee) *types.BootstrapData {
header, branch := makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+200, params.StateIndexSyncCommittee(""), merkle.Value(committee.Root()))
return &types.BootstrapData{
Header: header,
Header: header.Header,
Committee: committee,
CommitteeRoot: committee.Root(),
CommitteeBranch: branch,
@ -82,7 +83,7 @@ func makeBitmask(signerCount int) (bitmask [params.SyncCommitteeBitmaskSize]byte
return
}
func makeTestHeaderWithMerkleProof(slot, index uint64, value merkle.Value) (types.Header, merkle.Values) {
func makeTestHeaderWithMerkleProof(slot, index uint64, value merkle.Value) (types.HeaderWithExecProof, merkle.Values) {
var branch merkle.Values
hasher := sha256.New()
for index > 1 {
@ -100,7 +101,7 @@ func makeTestHeaderWithMerkleProof(slot, index uint64, value merkle.Value) (type
index >>= 1
branch = append(branch, proofHash)
}
return types.Header{Slot: slot, StateRoot: common.Hash(value)}, branch
return types.HeaderWithExecProof{Header: types.Header{Slot: slot, StateRoot: common.Hash(value)}}, branch
}
// syncCommittee holds either a blsSyncCommittee or a fake dummySyncCommittee used for testing

View file

@ -34,6 +34,11 @@ type Values []Value
var valueT = reflect.TypeFor[Value]()
// MarshalJSON encodes a merkle value in hex syntax.
func (m *Value) MarshalJSON() ([]byte, error) {
return []byte("\"" + hexutil.Encode((*m)[:]) + "\""), nil
}
// UnmarshalJSON parses a merkle value in hex syntax.
func (m *Value) UnmarshalJSON(input []byte) error {
return hexutil.UnmarshalFixedJSON(valueT, input, m[:])

View file

@ -22,6 +22,7 @@ import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
zrntcommon "github.com/protolambda/zrnt/eth2/beacon/common"
"github.com/protolambda/zrnt/eth2/configs"
@ -41,37 +42,55 @@ type blockObject interface {
// BeaconBlock represents a full block in the beacon chain.
type BeaconBlock struct {
jsonBeaconBlock
blockObj blockObject
}
// BlockFromJSON decodes a beacon block from JSON.
func BlockFromJSON(forkName string, data []byte) (*BeaconBlock, error) {
var obj blockObject
switch forkName {
case "capella":
obj = new(capella.BeaconBlock)
case "deneb":
obj = new(deneb.BeaconBlock)
case "electra":
obj = new(electra.BeaconBlock)
default:
return nil, fmt.Errorf("unsupported fork: %s", forkName)
}
if err := json.Unmarshal(data, obj); err != nil {
return nil, err
}
return &BeaconBlock{obj}, nil
type jsonBeaconBlock struct {
Version string `json:"version"`
ExecutionOptimistic bool `json:"execution_optimistic"`
Finalized bool `json:"finalized"`
Data struct {
Message json.RawMessage `json:"message"`
Signature hexutil.Bytes `json:"signature"`
} `json:"data"`
}
// NewBeaconBlock wraps a ZRNT block.
// UnmarshalJSON implements json.Marshaler.
func (b *BeaconBlock) UnmarshalJSON(input []byte) error {
if err := json.Unmarshal(input, &b.jsonBeaconBlock); err != nil {
return err
}
switch b.Version {
case "capella":
b.blockObj = new(capella.BeaconBlock)
case "deneb":
b.blockObj = new(deneb.BeaconBlock)
case "electra":
b.blockObj = new(electra.BeaconBlock)
default:
return fmt.Errorf("unsupported fork: %s", b.Version)
}
return json.Unmarshal(b.Data.Message, b.blockObj)
}
// MarshalJSON implements json.Marshaler.
/*func (b *BeaconBlock) MarshalJSON() ([]byte, error) {
var bb jsonBeaconBlock
bb.Version = b.version
bb.Data.Message = b.json
return json.Marshal(&bb)
}*/
// 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))
}

View file

@ -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:

View file

@ -61,23 +61,39 @@ func (c *BootstrapData) Validate() error {
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientupdate
type LightClientUpdate struct {
Version string
AttestedHeader SignedHeader // Arbitrary header out of the period signed by the sync committee
AttestedHeader HeaderWithExecProof // Arbitrary header out of the period signed by the sync committee
Signature SyncAggregate
SignatureSlot uint64
NextSyncCommitteeRoot common.Hash // Sync committee of the next period advertised in the current one
NextSyncCommitteeBranch merkle.Values // Proof for the next period's sync committee
FinalizedHeader *Header `rlp:"nil"` // Optional header to announce a point of finality
FinalityBranch merkle.Values // Proof for the announced finality
FinalizedHeader *HeaderWithExecProof `rlp:"nil"` // Optional header to announce a point of finality
FinalityBranch merkle.Values // Proof for the announced finality
score *UpdateScore // Weight of the update to compare between competing ones
}
func (update *LightClientUpdate) SignedHeader() SignedHeader {
return SignedHeader{
Header: update.AttestedHeader.Header,
Signature: update.Signature,
SignatureSlot: update.SignatureSlot,
}
}
// Validate verifies the validity of the update.
func (update *LightClientUpdate) Validate() error {
if err := update.AttestedHeader.Validate(); err != nil {
return fmt.Errorf("invalid attested header: %w", err)
}
period := update.AttestedHeader.Header.SyncPeriod()
if SyncPeriod(update.AttestedHeader.SignatureSlot) != period {
if SyncPeriod(update.SignatureSlot) != period {
return errors.New("signature slot and signed header are from different periods")
}
if update.FinalizedHeader != nil {
if err := update.FinalizedHeader.Validate(); err != nil {
return fmt.Errorf("invalid finalized header: %w", err)
}
if update.FinalizedHeader.SyncPeriod() != period {
return errors.New("finalized header is from different period")
}
@ -97,7 +113,7 @@ func (update *LightClientUpdate) Validate() error {
func (update *LightClientUpdate) Score() UpdateScore {
if update.score == nil {
update.score = &UpdateScore{
SignerCount: uint32(update.AttestedHeader.Signature.SignerCount()),
SignerCount: uint32(update.Signature.SignerCount()),
SubPeriodIndex: uint32(update.AttestedHeader.Header.Slot & 0x1fff),
FinalizedHeader: update.FinalizedHeader != nil,
}
@ -163,6 +179,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

View file

@ -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()

View file

@ -43,6 +43,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"
@ -258,6 +259,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)
@ -297,8 +302,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.

View file

@ -472,7 +472,12 @@ func isString(input []byte) bool {
return len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"'
}
// UnmarshalJSON parses a hash in hex syntax.
// MarshalJSON encodes a decimal number into a string.
func (d *Decimal) MarshalJSON() ([]byte, error) {
return []byte("\"" + strconv.FormatUint(uint64(*d), 10) + "\""), nil
}
// UnmarshalJSON parses a decimal number encoded as a string.
func (d *Decimal) UnmarshalJSON(input []byte) error {
if !isString(input) {
return &json.UnmarshalTypeError{Value: "non-string", Type: reflect.TypeFor[uint64]()}

2
go.mod
View file

@ -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

4
go.sum
View file

@ -111,6 +111,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.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s=
@ -185,6 +187,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=

378
restapi/exec_api.go Normal file
View file

@ -0,0 +1,378 @@
// 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 <http://www.gnu.org/licenses/>.
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"
"github.com/ethereum/go-ethereum/params/forks"
"github.com/ethereum/go-ethereum/rpc"
"github.com/gorilla/mux"
)
type backend interface {
HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error)
BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error)
GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error)
GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64)
GetPoolTransaction(txHash common.Hash) *types.Transaction
ChainConfig() *params.ChainConfig
}
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/{blockid}", server.WrapHandler(api.handleBlocks, false, false, true)).Methods("GET")
router.HandleFunc("/eth/v1/exec/block_receipts/{blockid}", server.WrapHandler(api.handleBlockReceipts, false, false, true)).Methods("GET")
router.HandleFunc("/eth/v1/exec/transaction/{txhash}", server.WrapHandler(api.handleTransaction, false, false, true)).Methods("GET")
router.HandleFunc("/eth/v1/exec/transaction_by_index/{blockid}", server.WrapHandler(api.handleTxByIndex, false, false, true)).Methods("GET")
router.HandleFunc("/eth/v1/exec/receipt_by_index/{blockid}", server.WrapHandler(api.handleReceiptByIndex, false, false, true)).Methods("GET")
router.HandleFunc("/eth/v1/exec/state/{blockid}", server.WrapHandler(api.handleState, true, true, true)).Methods("POST")
router.HandleFunc("/eth/v1/exec/call/{blockid}", 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 || err != 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) {
type blockResponse struct {
Version string `json:"version"`
Data *types.Block `json:"data"`
}
var (
amount int
response []blockResponse
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([]blockResponse, amount)
for i := amount - 1; i >= 0; i-- {
if id.isHash() {
response[i].Data, err = s.apiBackend.BlockByHash(ctx, id.hash)
} else {
response[i].Data, err = s.apiBackend.BlockByNumber(ctx, rpc.BlockNumber(id.number))
}
if errors.Is(err, context.Canceled) {
return nil, "request timeout", http.StatusRequestTimeout
}
if response[i].Data == nil || err != nil {
return nil, "not available", http.StatusNotFound
}
response[i].Version = s.forkName(response[i].Data.Header())
if response[i].Data.NumberU64() == 0 {
response = response[i:]
break
}
id = blockId{hash: response[i].Data.ParentHash()}
}
return response, "", 0
}
func (s *execApiServer) handleBlockReceipts(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
type blockReceiptsResponse struct {
Version string `json:"version"`
Data types.Receipts `json:"data"`
}
var (
amount int
response []blockReceiptsResponse
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([]blockReceiptsResponse, amount)
for i := amount - 1; i >= 0; i-- {
var header *types.Header
if id.isHash() {
header, err = s.apiBackend.HeaderByHash(ctx, id.hash)
} else {
header, err = s.apiBackend.HeaderByNumber(ctx, rpc.BlockNumber(id.number))
}
if header != nil && err == nil {
response[i].Data, err = s.apiBackend.GetReceipts(ctx, header.Hash())
}
if errors.Is(err, context.Canceled) {
return nil, "request timeout", http.StatusRequestTimeout
}
if response[i].Data == nil || err != nil {
return nil, "not available", http.StatusNotFound
}
response[i].Version = s.forkName(header)
if header.Number.Uint64() == 0 {
response = response[i:]
break
}
id = blockId{hash: header.ParentHash}
}
return response, "", 0
}
func (s *execApiServer) handleTransaction(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
var txHash common.Hash
if hex, err := hexutil.Decode(vars["txhash"]); err == nil {
if len(hex) != common.HashLength {
return nil, "invalid transaction hash", http.StatusBadRequest
}
copy(txHash[:], hex)
}
_, tx, _, _, _ := s.apiBackend.GetCanonicalTransaction(txHash)
if tx == nil {
tx = s.apiBackend.GetPoolTransaction(txHash)
}
if tx == nil {
return nil, "not available", http.StatusNotFound
}
return tx, "", 0
}
func (s *execApiServer) handleTxByIndex(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
type txProof struct {
Index uint64 `json:"index"`
Transaction *types.Transaction `json:"transaction" rlp:"-"`
Proof []hexutil.Bytes `json:"proof"`
}
var response struct {
Version string `json:"version"`
Data types.Receipts `json:"data"`
}
id, ok := getBlockId(vars["blockid"])
if !ok {
return nil, "invalid block id", http.StatusBadRequest
}
indices := values["indices"]
if len(indices) == 0 {
return nil, "empty transaction index list", http.StatusBadRequest
}
response.Data = make([]txProof, len(indices))
for i, str := range indicesStr {
if number, err := strconv.ParseUint(str, 10, 64); err == nil {
indices[i] = number
} else {
return nil, "invalid transaction index", http.StatusBadRequest
}
}
var (
block *types.Block
err error
)
if id.isHash() {
block, err = s.apiBackend.BlockByHash(ctx, id.hash)
} else {
block, err = s.apiBackend.BlockByNumber(ctx, rpc.BlockNumber(id.number))
}
if errors.Is(err, context.Canceled) {
return nil, "request timeout", http.StatusRequestTimeout
}
if block == nil || err != nil {
return nil, "not available", http.StatusNotFound
}
response[i].Version = s.forkName(block.Header())
t := trie.NewStackTrie(nil)
if types.DeriveSha(block.Transactions(), t) != block.TransactionsRoot() {
log.Error("")
return nil, "transactions root mismatch", http.StatusInternalServerError
}
var indexBuf []byte
for _, txIndex := range indices {
indexBuf = rlp.AppendUint64(indexBuf[:0], txIndex)
t.Prove(indexBuf, proofWriter)
}
}
func (s *execApiServer) handleReceiptByIndex(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
id, ok := getBlockId(vars["blockid"])
if !ok {
return nil, "invalid block id", http.StatusBadRequest
}
}
func (s *execApiServer) handleState(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
id, ok := getBlockId(vars["blockid"])
if !ok {
return nil, "invalid block id", http.StatusBadRequest
}
}
func (s *execApiServer) handleCall(ctx context.Context, values url.Values, vars map[string]string, decodeBody func(*any) error) (any, string, int) {
id, ok := getBlockId(vars["blockid"])
if !ok {
return nil, "invalid block id", http.StatusBadRequest
}
}
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")
}

156
restapi/server.go Normal file
View file

@ -0,0 +1,156 @@
// 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 <http://www.gnu.org/licenses/>.
package restapi
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/elnormous/contenttype"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rlp"
"github.com/gorilla/mux"
)
type Server struct {
router *mux.Router
}
type API func(*mux.Router)
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)
}
}
}
func (s *Server) WrapEventHandler(handler func(resp http.ResponseWriter, req *http.Request)) func(resp http.ResponseWriter, req *http.Request) {
return func(resp http.ResponseWriter, req *http.Request) {
rc := http.NewResponseController(resp)
if err := rc.SetReadDeadline(time.Time{}); err != nil {
log.Error("Could not set read deadline for events request", "error", err)
}
if err := rc.SetWriteDeadline(time.Time{}); err != nil {
log.Error("Could not set read deadline for events request", "error", err)
}
handler(resp, req)
}
}