mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-22 04:36:42 +00:00
Merge 06c623b5c7 into d3679c2f2e
This commit is contained in:
commit
09f49bb76c
21 changed files with 1388 additions and 250 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
289
beacon/light/api/server.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
313
beacon/light/api/types.go
Normal 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"`
|
||||
}
|
||||
76
beacon/light/checkpoint_store.go
Normal file
76
beacon/light/checkpoint_store.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ type committeeChain interface {
|
|||
type CheckpointInit struct {
|
||||
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
|
||||
|
|
@ -71,10 +72,11 @@ 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,
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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[:])
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
2
go.mod
|
|
@ -102,6 +102,7 @@ require (
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/elnormous/contenttype v1.0.4 // indirect
|
||||
github.com/emicklei/dot v1.6.2 // indirect
|
||||
github.com/fjl/gencodec v0.1.0 // indirect
|
||||
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect
|
||||
|
|
@ -113,6 +114,7 @@ require (
|
|||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kilic/bls12-381 v0.1.0 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -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
378
restapi/exec_api.go
Normal 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
156
restapi/server.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue