beacon/light/api: implement blsync REST API server

This commit is contained in:
zsfelfoldi 2025-10-11 12:34:08 +02:00
parent 8708c2e1cc
commit 17173d0014
17 changed files with 842 additions and 252 deletions

View file

@ -162,3 +162,8 @@ func (s *beaconBlockSync) updateEventFeed() {
Finalized: finalizedHash, Finalized: finalizedHash,
}) })
} }
func (s *beaconBlockSync) getBlock(blockRoot common.Hash) *types.BeaconBlock {
block, _ := s.recentBlocks.Get(blockRoot)
return block
}

View file

@ -30,7 +30,6 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/restapi" "github.com/ethereum/go-ethereum/restapi"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/gorilla/mux"
) )
type Client struct { type Client struct {
@ -40,12 +39,13 @@ type Client struct {
scheduler *request.Scheduler scheduler *request.Scheduler
blockSync *beaconBlockSync blockSync *beaconBlockSync
engineRPC *rpc.Client engineRPC *rpc.Client
apiServer *api.BeaconApiServer
chainHeadSub event.Subscription chainHeadSub event.Subscription
engineClient *engineClient engineClient *engineClient
} }
func NewClient(config params.ClientConfig) *Client { func NewClient(config params.ClientConfig, execChain api.ExecChain) *Client {
// create data structures // create data structures
var ( var (
db = memorydb.New() db = memorydb.New()
@ -57,12 +57,13 @@ func NewClient(config params.ClientConfig) *Client {
log.Error("Failed to save beacon checkpoint", "file", config.CheckpointFile, "checkpoint", checkpoint, "error", err) log.Error("Failed to save beacon checkpoint", "file", config.CheckpointFile, "checkpoint", checkpoint, "error", err)
} }
}) })
checkpointStore = light.NewCheckpointStore(db, committeeChain)
) )
headSync := sync.NewHeadSync(headTracker, committeeChain) headSync := sync.NewHeadSync(headTracker, committeeChain)
// set up scheduler and sync modules // set up scheduler and sync modules
scheduler := request.NewScheduler() scheduler := request.NewScheduler()
checkpointInit := sync.NewCheckpointInit(committeeChain, config.Checkpoint) checkpointInit := sync.NewCheckpointInit(committeeChain, checkpointStore, config.Checkpoint)
forwardSync := sync.NewForwardUpdateSync(committeeChain) forwardSync := sync.NewForwardUpdateSync(committeeChain)
beaconBlockSync := newBeaconBlockSync(headTracker) beaconBlockSync := newBeaconBlockSync(headTracker)
scheduler.RegisterTarget(headTracker) scheduler.RegisterTarget(headTracker)
@ -71,6 +72,8 @@ func NewClient(config params.ClientConfig) *Client {
scheduler.RegisterModule(forwardSync, "forwardSync") scheduler.RegisterModule(forwardSync, "forwardSync")
scheduler.RegisterModule(headSync, "headSync") scheduler.RegisterModule(headSync, "headSync")
scheduler.RegisterModule(beaconBlockSync, "beaconBlockSync") scheduler.RegisterModule(beaconBlockSync, "beaconBlockSync")
apiServer := api.NewBeaconApiServer(scheduler, checkpointStore, committeeChain, headTracker, beaconBlockSync.getBlock, execChain)
scheduler.RegisterModule(apiServer, "apiServer")
return &Client{ return &Client{
scheduler: scheduler, scheduler: scheduler,
@ -78,6 +81,7 @@ func NewClient(config params.ClientConfig) *Client {
customHeader: config.CustomHeader, customHeader: config.CustomHeader,
config: &config, config: &config,
blockSync: beaconBlockSync, blockSync: beaconBlockSync,
apiServer: apiServer,
} }
} }
@ -85,6 +89,10 @@ func (c *Client) SetEngineRPC(engine *rpc.Client) {
c.engineRPC = engine c.engineRPC = engine
} }
func (c *Client) RestAPI(server *restapi.Server) restapi.API {
return c.apiServer.RestAPI(server)
}
func (c *Client) Start() error { func (c *Client) Start() error {
headCh := make(chan types.ChainHeadEvent, 16) headCh := make(chan types.ChainHeadEvent, 16)
c.chainHeadSub = c.blockSync.SubscribeChainHead(headCh) c.chainHeadSub = c.blockSync.SubscribeChainHead(headCh)
@ -92,13 +100,14 @@ func (c *Client) Start() error {
c.scheduler.Start() c.scheduler.Start()
for _, url := range c.urls { for _, url := range c.urls {
beaconApi := api.NewBeaconLightApi(url, c.customHeader) beaconApi := api.NewBeaconApiClient(url, c.customHeader)
c.scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{})) c.scheduler.RegisterServer(request.NewServer(api.NewSyncServer(beaconApi), &mclock.System{}))
} }
return nil return nil
} }
func (c *Client) Stop() error { func (c *Client) Stop() error {
c.apiServer.Stop()
c.engineClient.stop() c.engineClient.stop()
c.chainHeadSub.Unsubscribe() c.chainHeadSub.Unsubscribe()
c.scheduler.Stop() c.scheduler.Stop()

View file

@ -29,90 +29,27 @@ import (
"time" "time"
"github.com/donovanhide/eventsource" "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/beacon/types"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log" "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. // fetcher is an interface useful for debug-harnessing the http api.
type fetcher interface { type fetcher interface {
Do(req *http.Request) (*http.Response, error) 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. // Note: all required API endpoints are currently only implemented by Lodestar.
type BeaconLightApi struct { type BeaconApiClient struct {
url string url string
client fetcher client fetcher
customHeaders map[string]string customHeaders map[string]string
} }
func NewBeaconLightApi(url string, customHeaders map[string]string) *BeaconLightApi { func NewBeaconApiClient(url string, customHeaders map[string]string) *BeaconApiClient {
return &BeaconLightApi{ return &BeaconApiClient{
url: url, url: url,
client: &http.Client{ client: &http.Client{
Timeout: time.Second * 10, 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) uri, err := api.buildURL(path, params)
if err != nil { if err != nil {
return nil, err return nil, err
@ -155,7 +92,7 @@ func (api *BeaconLightApi) httpGet(path string, params url.Values) ([]byte, erro
// equals update.NextSyncCommitteeRoot). // equals update.NextSyncCommitteeRoot).
// Note that the results are validated but the update signature should be verified // Note that the results are validated but the update signature should be verified
// by the caller as its validity depends on the update chain. // 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{ resp, err := api.httpGet("/eth/v1/beacon/light_client/updates", map[string][]string{
"start_period": {strconv.FormatUint(firstPeriod, 10)}, "start_period": {strconv.FormatUint(firstPeriod, 10)},
"count": {strconv.FormatUint(count, 10)}, "count": {strconv.FormatUint(count, 10)},
@ -195,7 +132,7 @@ func (api *BeaconLightApi) GetBestUpdatesAndCommittees(firstPeriod, count uint64
// //
// See data structure definition here: // See data structure definition here:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate // 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) resp, err := api.httpGet("/eth/v1/beacon/light_client/optimistic_update", nil)
if err != nil { if err != nil {
return types.OptimisticUpdate{}, err return types.OptimisticUpdate{}, err
@ -203,52 +140,11 @@ func (api *BeaconLightApi) GetOptimisticUpdate() (types.OptimisticUpdate, error)
return decodeOptimisticUpdate(resp) 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. // GetFinalityUpdate fetches the latest available finality update.
// //
// See data structure definition here: // See data structure definition here:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientfinalityupdate // 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) resp, err := api.httpGet("/eth/v1/beacon/light_client/finality_update", nil)
if err != nil { if err != nil {
return types.FinalityUpdate{}, err return types.FinalityUpdate{}, err
@ -256,60 +152,11 @@ func (api *BeaconLightApi) GetFinalityUpdate() (types.FinalityUpdate, error) {
return decodeFinalityUpdate(resp) 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. // GetHeader fetches and validates the beacon header with the given blockRoot.
// If blockRoot is null hash then the latest head header is fetched. // 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 // The values of the canonical and finalized flags are also returned. Note that
// these flags are not validated. // 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 var blockId string
if blockRoot == (common.Hash{}) { if blockRoot == (common.Hash{}) {
blockId = "head" 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. // 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) resp, err := api.httpGet(fmt.Sprintf("/eth/v1/beacon/light_client/bootstrap/0x%x", checkpointHash[:]), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// See data structure definition here: var data jsonBootstrapData
// 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
if err := json.Unmarshal(resp, &data); err != nil { if err := json.Unmarshal(resp, &data); err != nil {
return nil, err return nil, err
} }
@ -390,25 +226,16 @@ func (api *BeaconLightApi) GetCheckpointData(checkpointHash common.Hash) (*types
return checkpoint, nil 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) resp, err := api.httpGet(fmt.Sprintf("/eth/v2/beacon/blocks/0x%x", blockRoot), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var beaconBlockMessage struct { block := new(types.BeaconBlock)
Version string `json:"version"` if err := json.Unmarshal(resp, block); err != nil {
Data struct {
Message json.RawMessage `json:"message"`
}
}
if err := json.Unmarshal(resp, &beaconBlockMessage); err != nil {
return nil, fmt.Errorf("invalid block json data: %v", err) 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() computedRoot := block.Root()
if computedRoot != blockRoot { if computedRoot != blockRoot {
return nil, fmt.Errorf("Beacon block root hash mismatch (expected: %x, got: %x)", blockRoot, computedRoot) 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. // 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. // The callbacks are also called for the current head and optimistic head at startup.
// They are never called concurrently. // They are never called concurrently.
func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func() { func (api *BeaconApiClient) StartHeadListener(listener HeadEventListener) func() {
var ( var (
ctx, closeCtx = context.WithCancel(context.Background()) ctx, closeCtx = context.WithCancel(context.Background())
streamCh = make(chan *eventsource.Stream, 1) 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 // 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. // 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) { for retry := true; retry; retry = ctxSleep(ctx, 5*time.Second) {
log.Trace("Sending event subscription request") 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"}}) 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) uri, err := url.Parse(api.url)
if err != nil { if err != nil {
return "", err 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", 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" "github.com/ethereum/go-ethereum/log"
) )
// ApiServer is a wrapper around BeaconLightApi that implements request.requestServer. // SyncServer is a wrapper around BeaconApiClient that implements request.requestServer.
type ApiServer struct { type SyncServer struct {
api *BeaconLightApi api *BeaconApiClient
eventCallback func(event request.Event) eventCallback func(event request.Event)
unsubscribe func() unsubscribe func()
} }
// NewApiServer creates a new ApiServer. // NewSyncServer creates a new SyncServer.
func NewApiServer(api *BeaconLightApi) *ApiServer { func NewSyncServer(api *BeaconApiClient) *SyncServer {
return &ApiServer{api: api} return &SyncServer{api: api}
} }
// Subscribe implements request.requestServer. // 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 s.eventCallback = eventCallback
listener := HeadEventListener{ listener := HeadEventListener{
OnNewHead: func(slot uint64, blockRoot common.Hash) { OnNewHead: func(slot uint64, blockRoot common.Hash) {
@ -62,7 +62,7 @@ func (s *ApiServer) Subscribe(eventCallback func(event request.Event)) {
} }
// SendRequest implements request.requestServer. // 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() { go func() {
var resp request.Response var resp request.Response
var err error var err error
@ -101,7 +101,7 @@ func (s *ApiServer) SendRequest(id request.ID, req request.Request) {
// Unsubscribe implements request.requestServer. // Unsubscribe implements request.requestServer.
// Note: Unsubscribe should not be called concurrently with Subscribe. // Note: Unsubscribe should not be called concurrently with Subscribe.
func (s *ApiServer) Unsubscribe() { func (s *SyncServer) Unsubscribe() {
if s.unsubscribe != nil { if s.unsubscribe != nil {
s.unsubscribe() s.unsubscribe()
s.unsubscribe = nil s.unsubscribe = nil
@ -109,6 +109,6 @@ func (s *ApiServer) Unsubscribe() {
} }
// Name implements request.Server // Name implements request.Server
func (s *ApiServer) Name() string { func (s *SyncServer) Name() string {
return s.api.url 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 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. // InsertUpdate adds a new update if possible.
func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error { func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error {
s.chainmu.Lock() 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, // 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 // 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. // 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 { if age < 0 {
log.Warn("Future committee update received", "age", age) log.Warn("Future committee update received", "age", age)
} }

View file

@ -41,6 +41,7 @@ type committeeChain interface {
type CheckpointInit struct { type CheckpointInit struct {
chain committeeChain chain committeeChain
checkpointHash common.Hash checkpointHash common.Hash
checkpointStore *light.CheckpointStore
locked request.ServerAndID locked request.ServerAndID
initialized bool initialized bool
// per-server state is used to track the state of requesting checkpoint header // per-server state is used to track the state of requesting checkpoint header
@ -71,10 +72,11 @@ type serverState struct {
} }
// NewCheckpointInit creates a new CheckpointInit. // 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{ return &CheckpointInit{
chain: chain, chain: chain,
checkpointHash: checkpointHash, checkpointHash: checkpointHash,
checkpointStore: checkpointStore,
serverState: make(map[request.Server]serverState), serverState: make(map[request.Server]serverState),
} }
} }
@ -100,6 +102,7 @@ func (s *CheckpointInit) Process(requester request.Requester, events []request.E
if resp != nil { if resp != nil {
if checkpoint := resp.(*types.BootstrapData); checkpoint.Header.Hash() == common.Hash(req.(ReqCheckpointData)) { if checkpoint := resp.(*types.BootstrapData); checkpoint.Header.Hash() == common.Hash(req.(ReqCheckpointData)) {
s.chain.CheckpointInit(*checkpoint) s.chain.CheckpointInit(*checkpoint)
s.checkpointStore.Store(checkpoint)
s.initialized = true s.initialized = true
return 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 { func GenerateTestUpdate(config *params.ChainConfig, period uint64, committee, nextCommittee *types.SerializedSyncCommittee, signerCount int, finalizedHeader bool) *types.LightClientUpdate {
update := new(types.LightClientUpdate) update := new(types.LightClientUpdate)
update.NextSyncCommitteeRoot = nextCommittee.Root() update.NextSyncCommitteeRoot = nextCommittee.Root()
var attestedHeader types.Header
if finalizedHeader { 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)) *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 { } 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 return update
} }
@ -65,7 +66,7 @@ func GenerateTestSignedHeader(header types.Header, config *params.ChainConfig, c
func GenerateTestCheckpoint(period uint64, committee *types.SerializedSyncCommittee) *types.BootstrapData { func GenerateTestCheckpoint(period uint64, committee *types.SerializedSyncCommittee) *types.BootstrapData {
header, branch := makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+200, params.StateIndexSyncCommittee(""), merkle.Value(committee.Root())) header, branch := makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+200, params.StateIndexSyncCommittee(""), merkle.Value(committee.Root()))
return &types.BootstrapData{ return &types.BootstrapData{
Header: header, Header: header.Header,
Committee: committee, Committee: committee,
CommitteeRoot: committee.Root(), CommitteeRoot: committee.Root(),
CommitteeBranch: branch, CommitteeBranch: branch,
@ -82,7 +83,7 @@ func makeBitmask(signerCount int) (bitmask [params.SyncCommitteeBitmaskSize]byte
return 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 var branch merkle.Values
hasher := sha256.New() hasher := sha256.New()
for index > 1 { for index > 1 {
@ -100,7 +101,7 @@ func makeTestHeaderWithMerkleProof(slot, index uint64, value merkle.Value) (type
index >>= 1 index >>= 1
branch = append(branch, proofHash) 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 // 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]() 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. // UnmarshalJSON parses a merkle value in hex syntax.
func (m *Value) UnmarshalJSON(input []byte) error { func (m *Value) UnmarshalJSON(input []byte) error {
return hexutil.UnmarshalFixedJSON(valueT, input, m[:]) return hexutil.UnmarshalFixedJSON(valueT, input, m[:])

View file

@ -22,6 +22,7 @@ import (
"fmt" "fmt"
"github.com/ethereum/go-ethereum/common" "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/core/types"
zrntcommon "github.com/protolambda/zrnt/eth2/beacon/common" zrntcommon "github.com/protolambda/zrnt/eth2/beacon/common"
"github.com/protolambda/zrnt/eth2/configs" "github.com/protolambda/zrnt/eth2/configs"
@ -41,37 +42,55 @@ type blockObject interface {
// BeaconBlock represents a full block in the beacon chain. // BeaconBlock represents a full block in the beacon chain.
type BeaconBlock struct { type BeaconBlock struct {
jsonBeaconBlock
blockObj blockObject blockObj blockObject
} }
// BlockFromJSON decodes a beacon block from JSON. type jsonBeaconBlock struct {
func BlockFromJSON(forkName string, data []byte) (*BeaconBlock, error) { Version string `json:"version"`
var obj blockObject ExecutionOptimistic bool `json:"execution_optimistic"`
switch forkName { Finalized bool `json:"finalized"`
case "capella": Data struct {
obj = new(capella.BeaconBlock) Message json.RawMessage `json:"message"`
case "deneb": Signature hexutil.Bytes `json:"signature"`
obj = new(deneb.BeaconBlock) } `json:"data"`
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
} }
// 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 { func NewBeaconBlock(obj blockObject) *BeaconBlock {
switch obj := obj.(type) { switch obj := obj.(type) {
case *capella.BeaconBlock: case *capella.BeaconBlock:
return &BeaconBlock{obj} return &BeaconBlock{blockObj: obj}
case *deneb.BeaconBlock: case *deneb.BeaconBlock:
return &BeaconBlock{obj} return &BeaconBlock{blockObj: obj}
case *electra.BeaconBlock: case *electra.BeaconBlock:
return &BeaconBlock{obj} return &BeaconBlock{blockObj: obj}
default: default:
panic(fmt.Errorf("unsupported block type %T", obj)) 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 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 { func NewExecutionHeader(obj headerObject) *ExecutionHeader {
switch obj.(type) { switch obj.(type) {
case *capella.ExecutionPayloadHeader: 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 // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientupdate
type LightClientUpdate struct { type LightClientUpdate struct {
Version string 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 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 NextSyncCommitteeBranch merkle.Values // Proof for the next period's sync committee
FinalizedHeader *Header `rlp:"nil"` // Optional header to announce a point of finality FinalizedHeader *HeaderWithExecProof `rlp:"nil"` // Optional header to announce a point of finality
FinalityBranch merkle.Values // Proof for the announced finality FinalityBranch merkle.Values // Proof for the announced finality
score *UpdateScore // Weight of the update to compare between competing ones 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. // Validate verifies the validity of the update.
func (update *LightClientUpdate) Validate() error { 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() 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") return errors.New("signature slot and signed header are from different periods")
} }
if update.FinalizedHeader != nil { if update.FinalizedHeader != nil {
if err := update.FinalizedHeader.Validate(); err != nil {
return fmt.Errorf("invalid finalized header: %w", err)
}
if update.FinalizedHeader.SyncPeriod() != period { if update.FinalizedHeader.SyncPeriod() != period {
return errors.New("finalized header is from different period") return errors.New("finalized header is from different period")
} }
@ -97,7 +113,7 @@ func (update *LightClientUpdate) Validate() error {
func (update *LightClientUpdate) Score() UpdateScore { func (update *LightClientUpdate) Score() UpdateScore {
if update.score == nil { if update.score == nil {
update.score = &UpdateScore{ update.score = &UpdateScore{
SignerCount: uint32(update.AttestedHeader.Signature.SignerCount()), SignerCount: uint32(update.Signature.SignerCount()),
SubPeriodIndex: uint32(update.AttestedHeader.Header.Slot & 0x1fff), SubPeriodIndex: uint32(update.AttestedHeader.Header.Slot & 0x1fff),
FinalizedHeader: update.FinalizedHeader != nil, FinalizedHeader: update.FinalizedHeader != nil,
} }
@ -163,6 +179,7 @@ func (h *HeaderWithExecProof) Validate() error {
// See data structure definition here: // See data structure definition here:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
type OptimisticUpdate struct { type OptimisticUpdate struct {
Version string
Attested HeaderWithExecProof Attested HeaderWithExecProof
// Sync committee BLS signature aggregate // Sync committee BLS signature aggregate
Signature SyncAggregate Signature SyncAggregate

View file

@ -72,7 +72,7 @@ func main() {
func sync(ctx *cli.Context) error { func sync(ctx *cli.Context) error {
// set up blsync // set up blsync
client := blsync.NewClient(utils.MakeBeaconLightConfig(ctx)) client := blsync.NewClient(utils.MakeBeaconLightConfig(ctx), nil)
client.SetEngineRPC(makeRPCClient(ctx)) client.SetEngineRPC(makeRPCClient(ctx))
client.Start() client.Start()

View file

@ -303,9 +303,9 @@ func makeFullNode(ctx *cli.Context) *node.Node {
// Start blsync mode. // Start blsync mode.
srv := rpc.NewServer() srv := rpc.NewServer()
srv.RegisterName("engine", catalyst.NewConsensusAPI(eth)) 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)) blsyncer.SetEngineRPC(rpc.DialInProc(srv))
restServer.Register(blsyncer.RestAPI()) restServer.Register(blsyncer.RestAPI(restServer))
stack.RegisterLifecycle(blsyncer) stack.RegisterLifecycle(blsyncer)
} else { } else {
// Launch the engine API for interacting with external consensus client. // Launch the engine API for interacting with external consensus client.

View file

@ -463,7 +463,12 @@ func isString(input []byte) bool {
return len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' 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 { func (d *Decimal) UnmarshalJSON(input []byte) error {
if !isString(input) { if !isString(input) {
return &json.UnmarshalTypeError{Value: "non-string", Type: reflect.TypeFor[uint64]()} return &json.UnmarshalTypeError{Value: "non-string", Type: reflect.TypeFor[uint64]()}