mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-22 12:46:44 +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,
|
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/ethdb/memorydb"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/restapi"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -38,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()
|
||||||
|
|
@ -55,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)
|
||||||
|
|
@ -69,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,
|
||||||
|
|
@ -76,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,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)
|
||||||
|
|
@ -90,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()
|
||||||
|
|
|
||||||
|
|
@ -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
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"
|
"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
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
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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[:])
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
"github.com/ethereum/go-ethereum/node"
|
"github.com/ethereum/go-ethereum/node"
|
||||||
|
"github.com/ethereum/go-ethereum/restapi"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
"github.com/naoina/toml"
|
"github.com/naoina/toml"
|
||||||
"github.com/urfave/cli/v2"
|
"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.
|
// Configure log filter RPC API.
|
||||||
filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)
|
filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)
|
||||||
|
|
||||||
|
|
@ -297,8 +302,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))
|
||||||
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.
|
||||||
|
|
|
||||||
|
|
@ -472,7 +472,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]()}
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -102,6 +102,7 @@ require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
github.com/deepmap/oapi-codegen v1.6.0 // indirect
|
github.com/deepmap/oapi-codegen v1.6.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.7.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/emicklei/dot v1.6.2 // indirect
|
||||||
github.com/fjl/gencodec v0.1.0 // indirect
|
github.com/fjl/gencodec v0.1.0 // indirect
|
||||||
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // 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/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // 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/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/kilic/bls12-381 v0.1.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 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-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
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 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
|
||||||
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
|
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=
|
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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.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 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
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=
|
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