mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-22 20:56:42 +00:00
beacon/light/api: implement blsync REST API server
This commit is contained in:
parent
8708c2e1cc
commit
17173d0014
17 changed files with 842 additions and 252 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/restapi"
|
"github.com/ethereum/go-ethereum/restapi"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
|
|
@ -40,12 +39,13 @@ type Client struct {
|
||||||
scheduler *request.Scheduler
|
scheduler *request.Scheduler
|
||||||
blockSync *beaconBlockSync
|
blockSync *beaconBlockSync
|
||||||
engineRPC *rpc.Client
|
engineRPC *rpc.Client
|
||||||
|
apiServer *api.BeaconApiServer
|
||||||
|
|
||||||
chainHeadSub event.Subscription
|
chainHeadSub event.Subscription
|
||||||
engineClient *engineClient
|
engineClient *engineClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(config params.ClientConfig) *Client {
|
func NewClient(config params.ClientConfig, execChain api.ExecChain) *Client {
|
||||||
// create data structures
|
// create data structures
|
||||||
var (
|
var (
|
||||||
db = memorydb.New()
|
db = memorydb.New()
|
||||||
|
|
@ -57,12 +57,13 @@ func NewClient(config params.ClientConfig) *Client {
|
||||||
log.Error("Failed to save beacon checkpoint", "file", config.CheckpointFile, "checkpoint", checkpoint, "error", err)
|
log.Error("Failed to save beacon checkpoint", "file", config.CheckpointFile, "checkpoint", checkpoint, "error", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
checkpointStore = light.NewCheckpointStore(db, committeeChain)
|
||||||
)
|
)
|
||||||
headSync := sync.NewHeadSync(headTracker, committeeChain)
|
headSync := sync.NewHeadSync(headTracker, committeeChain)
|
||||||
|
|
||||||
// set up scheduler and sync modules
|
// set up scheduler and sync modules
|
||||||
scheduler := request.NewScheduler()
|
scheduler := request.NewScheduler()
|
||||||
checkpointInit := sync.NewCheckpointInit(committeeChain, config.Checkpoint)
|
checkpointInit := sync.NewCheckpointInit(committeeChain, checkpointStore, config.Checkpoint)
|
||||||
forwardSync := sync.NewForwardUpdateSync(committeeChain)
|
forwardSync := sync.NewForwardUpdateSync(committeeChain)
|
||||||
beaconBlockSync := newBeaconBlockSync(headTracker)
|
beaconBlockSync := newBeaconBlockSync(headTracker)
|
||||||
scheduler.RegisterTarget(headTracker)
|
scheduler.RegisterTarget(headTracker)
|
||||||
|
|
@ -71,6 +72,8 @@ func NewClient(config params.ClientConfig) *Client {
|
||||||
scheduler.RegisterModule(forwardSync, "forwardSync")
|
scheduler.RegisterModule(forwardSync, "forwardSync")
|
||||||
scheduler.RegisterModule(headSync, "headSync")
|
scheduler.RegisterModule(headSync, "headSync")
|
||||||
scheduler.RegisterModule(beaconBlockSync, "beaconBlockSync")
|
scheduler.RegisterModule(beaconBlockSync, "beaconBlockSync")
|
||||||
|
apiServer := api.NewBeaconApiServer(scheduler, checkpointStore, committeeChain, headTracker, beaconBlockSync.getBlock, execChain)
|
||||||
|
scheduler.RegisterModule(apiServer, "apiServer")
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
scheduler: scheduler,
|
scheduler: scheduler,
|
||||||
|
|
@ -78,6 +81,7 @@ func NewClient(config params.ClientConfig) *Client {
|
||||||
customHeader: config.CustomHeader,
|
customHeader: config.CustomHeader,
|
||||||
config: &config,
|
config: &config,
|
||||||
blockSync: beaconBlockSync,
|
blockSync: beaconBlockSync,
|
||||||
|
apiServer: apiServer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,6 +89,10 @@ func (c *Client) SetEngineRPC(engine *rpc.Client) {
|
||||||
c.engineRPC = engine
|
c.engineRPC = engine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) RestAPI(server *restapi.Server) restapi.API {
|
||||||
|
return c.apiServer.RestAPI(server)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) Start() error {
|
func (c *Client) Start() error {
|
||||||
headCh := make(chan types.ChainHeadEvent, 16)
|
headCh := make(chan types.ChainHeadEvent, 16)
|
||||||
c.chainHeadSub = c.blockSync.SubscribeChainHead(headCh)
|
c.chainHeadSub = c.blockSync.SubscribeChainHead(headCh)
|
||||||
|
|
@ -92,13 +100,14 @@ func (c *Client) Start() error {
|
||||||
|
|
||||||
c.scheduler.Start()
|
c.scheduler.Start()
|
||||||
for _, url := range c.urls {
|
for _, url := range c.urls {
|
||||||
beaconApi := api.NewBeaconLightApi(url, c.customHeader)
|
beaconApi := api.NewBeaconApiClient(url, c.customHeader)
|
||||||
c.scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{}))
|
c.scheduler.RegisterServer(request.NewServer(api.NewSyncServer(beaconApi), &mclock.System{}))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Stop() error {
|
func (c *Client) Stop() error {
|
||||||
|
c.apiServer.Stop()
|
||||||
c.engineClient.stop()
|
c.engineClient.stop()
|
||||||
c.chainHeadSub.Unsubscribe()
|
c.chainHeadSub.Unsubscribe()
|
||||||
c.scheduler.Stop()
|
c.scheduler.Stop()
|
||||||
|
|
|
||||||
|
|
@ -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", 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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,11 @@ type committeeChain interface {
|
||||||
// data belonging to the given checkpoint hash and initializes the committee chain
|
// data belonging to the given checkpoint hash and initializes the committee chain
|
||||||
// if successful.
|
// if successful.
|
||||||
type CheckpointInit struct {
|
type CheckpointInit struct {
|
||||||
chain committeeChain
|
chain committeeChain
|
||||||
checkpointHash common.Hash
|
checkpointHash common.Hash
|
||||||
locked request.ServerAndID
|
checkpointStore *light.CheckpointStore
|
||||||
initialized bool
|
locked request.ServerAndID
|
||||||
|
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
|
||||||
// info. Part of this info (canonical and finalized state) is not validated
|
// info. Part of this info (canonical and finalized state) is not validated
|
||||||
// and therefore it is requested from each server separately after it has
|
// and therefore it is requested from each server separately after it has
|
||||||
|
|
@ -71,11 +72,12 @@ 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,
|
||||||
serverState: make(map[request.Server]serverState),
|
checkpointStore: checkpointStore,
|
||||||
|
serverState: make(map[request.Server]serverState),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +102,7 @@ func (s *CheckpointInit) Process(requester request.Requester, events []request.E
|
||||||
if resp != nil {
|
if 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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -303,9 +303,9 @@ func makeFullNode(ctx *cli.Context) *node.Node {
|
||||||
// Start blsync mode.
|
// Start blsync mode.
|
||||||
srv := rpc.NewServer()
|
srv := rpc.NewServer()
|
||||||
srv.RegisterName("engine", catalyst.NewConsensusAPI(eth))
|
srv.RegisterName("engine", catalyst.NewConsensusAPI(eth))
|
||||||
blsyncer := blsync.NewClient(utils.MakeBeaconLightConfig(ctx))
|
blsyncer := blsync.NewClient(utils.MakeBeaconLightConfig(ctx), eth.BlockChain())
|
||||||
blsyncer.SetEngineRPC(rpc.DialInProc(srv))
|
blsyncer.SetEngineRPC(rpc.DialInProc(srv))
|
||||||
restServer.Register(blsyncer.RestAPI())
|
restServer.Register(blsyncer.RestAPI(restServer))
|
||||||
stack.RegisterLifecycle(blsyncer)
|
stack.RegisterLifecycle(blsyncer)
|
||||||
} else {
|
} else {
|
||||||
// Launch the engine API for interacting with external consensus client.
|
// Launch the engine API for interacting with external consensus client.
|
||||||
|
|
|
||||||
|
|
@ -463,7 +463,12 @@ func isString(input []byte) bool {
|
||||||
return len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"'
|
return len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"'
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON parses a hash in hex syntax.
|
// MarshalJSON encodes a decimal number into a string.
|
||||||
|
func (d *Decimal) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte("\"" + strconv.FormatUint(uint64(*d), 10) + "\""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON parses a decimal number encoded as a string.
|
||||||
func (d *Decimal) UnmarshalJSON(input []byte) error {
|
func (d *Decimal) UnmarshalJSON(input []byte) error {
|
||||||
if !isString(input) {
|
if !isString(input) {
|
||||||
return &json.UnmarshalTypeError{Value: "non-string", Type: reflect.TypeFor[uint64]()}
|
return &json.UnmarshalTypeError{Value: "non-string", Type: reflect.TypeFor[uint64]()}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue