diff --git a/go.sum b/go.sum index 19e7e16f1f0c..280aae881bb3 100644 --- a/go.sum +++ b/go.sum @@ -586,6 +586,7 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= diff --git a/light/beacon/api/rest_api.go b/light/beacon/api/rest_api.go index 76ee3fd55171..0b0f356f4098 100644 --- a/light/beacon/api/rest_api.go +++ b/light/beacon/api/rest_api.go @@ -38,11 +38,46 @@ type RestApi struct { Url string } +// GetBestUpdateAndCommittee fetches and validates LightClientUpdate for given period and full serialized +// committee for the next period (committee root hash equals update.NextSyncCommitteeRoot). +// Note that the results are validated but the update signature should be verified by the caller as its +// validity depends on the update chain. func (api *RestApi) GetBestUpdateAndCommittee(period uint64) (beacon.LightClientUpdate, []byte, error) { - c, err := api.getCommitteeUpdate(period) + periodStr := strconv.Itoa(int(period)) + resp, err := http.Get(api.Url + "/eth/v1/beacon/light_client/updates?start_period=" + periodStr + "&count=1") if err != nil { return beacon.LightClientUpdate{}, nil, err } + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return beacon.LightClientUpdate{}, nil, err + } + + type committeeUpdate struct { + Header beacon.Header `json:"attested_header"` + NextSyncCommittee syncCommitteeJson `json:"next_sync_committee"` + NextSyncCommitteeBranch beacon.MerkleValues `json:"next_sync_committee_branch"` + FinalizedHeader beacon.Header `json:"finalized_header"` + FinalityBranch beacon.MerkleValues `json:"finality_branch"` + Aggregate syncAggregate `json:"sync_aggregate"` + ForkVersion hexutil.Bytes `json:"fork_version"` + } + + var data struct { + Data []committeeUpdate `json:"data"` + } + if err := json.Unmarshal(body, &data); err != nil { + return beacon.LightClientUpdate{}, nil, err + } + if len(data.Data) != 1 { + return beacon.LightClientUpdate{}, nil, errors.New("invalid number of committee updates") + } + c := data.Data[0] + if len(c.NextSyncCommittee.Pubkeys) != 512 { + return beacon.LightClientUpdate{}, nil, errors.New("invalid number of pubkeys in next_sync_committee") + } + committee, ok := c.NextSyncCommittee.serialize() if !ok { return beacon.LightClientUpdate{}, nil, errors.New("invalid sync committee") @@ -57,14 +92,24 @@ func (api *RestApi) GetBestUpdateAndCommittee(period uint64) (beacon.LightClient SyncCommitteeSignature: c.Aggregate.Signature, ForkVersion: c.ForkVersion, } + if err := update.Validate(); err != nil { + return beacon.LightClientUpdate{}, nil, err + } + if beacon.SerializedCommitteeRoot(committee) != update.NextSyncCommitteeRoot { + return beacon.LightClientUpdate{}, nil, errors.New("sync committee root does not match") + } return update, committee, nil } +// syncAggregate represents an aggregated BLS signature with BitMask referring to a subset +// of the corresponding sync committee type syncAggregate struct { BitMask hexutil.Bytes `json:"sync_committee_bits"` Signature hexutil.Bytes `json:"sync_committee_signature"` } +// GetHeadUpdate fetches the latest available signed header. +// Note that the signature should be verified by the caller as its validity depends on the update chain. func (api *RestApi) GetHeadUpdate() (beacon.SignedHead, error) { resp, err := http.Get(api.Url + "/eth/v1/beacon/light_client/optimistic_update/") if err != nil { @@ -98,11 +143,13 @@ func (api *RestApi) GetHeadUpdate() (beacon.SignedHead, error) { }, nil } +// syncCommitteeJson is the JSON representation of a sync committee type syncCommitteeJson struct { Pubkeys []hexutil.Bytes `json:"pubkeys"` Aggregate hexutil.Bytes `json:"aggregate_pubkey"` } +// serialize returns the serialized version of the committee func (s *syncCommitteeJson) serialize() ([]byte, bool) { if len(s.Pubkeys) != 512 { return nil, false @@ -121,45 +168,8 @@ func (s *syncCommitteeJson) serialize() ([]byte, bool) { return sk, true } -type committeeUpdate struct { - Header beacon.Header `json:"attested_header"` - NextSyncCommittee syncCommitteeJson `json:"next_sync_committee"` - NextSyncCommitteeBranch beacon.MerkleValues `json:"next_sync_committee_branch"` - FinalizedHeader beacon.Header `json:"finalized_header"` - FinalityBranch beacon.MerkleValues `json:"finality_branch"` - Aggregate syncAggregate `json:"sync_aggregate"` - ForkVersion hexutil.Bytes `json:"fork_version"` -} - -func (api *RestApi) getCommitteeUpdate(period uint64) (committeeUpdate, error) { - periodStr := strconv.Itoa(int(period)) - resp, err := http.Get(api.Url + "/eth/v1/beacon/light_client/updates?start_period=" + periodStr + "&count=1") - if err != nil { - return committeeUpdate{}, err - } - body, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - return committeeUpdate{}, err - } - - var data struct { - Data []committeeUpdate `json:"data"` - } - if err := json.Unmarshal(body, &data); err != nil { - return committeeUpdate{}, err - } - if len(data.Data) != 1 { - return committeeUpdate{}, errors.New("invalid number of committee updates") - } - update := data.Data[0] - if len(update.NextSyncCommittee.Pubkeys) != 512 { - return committeeUpdate{}, errors.New("invalid number of pubkeys in next_sync_committee") - } - return update, nil -} - -// null hash -> current head +// GetHead fetches and validates the beacon header with the given blockRoot. +// If blockRoot is null hash then the latest head header is fetched. func (api *RestApi) GetHeader(blockRoot common.Hash) (beacon.Header, error) { url := api.Url + "/eth/v1/beacon/headers/" if blockRoot == (common.Hash{}) { @@ -200,6 +210,10 @@ func (api *RestApi) GetHeader(blockRoot common.Hash) (beacon.Header, error) { return header, nil } +// GetStateProof fetches and validates a Merkle proof for the specified parts of the recent +// beacon state referenced by stateRoot. If successful the returned multiproof has the format +// specified by expFormat. The state subset specified by the list of string keys (paths) should +// cover the subset specified by expFormat. func (api *RestApi) GetStateProof(stateRoot common.Hash, paths []string, expFormat beacon.ProofFormat) (beacon.MultiProof, error) { url := api.Url + "/eth/v1/beacon/light_client/proof/" + stateRoot.Hex() + "?paths=" + paths[0] for i := 1; i < len(paths); i++ { @@ -226,6 +240,7 @@ func (api *RestApi) GetStateProof(stateRoot common.Hash, paths []string, expForm return beacon.MultiProof{Format: expFormat, Values: values}, nil } +// GetCheckpointData fetches and validates bootstrap data belonging to the given checkpoint. func (api *RestApi) GetCheckpointData(ctx context.Context, checkpoint common.Hash) (beacon.Header, beacon.CheckpointData, []byte, error) { resp, err := http.Get(api.Url + "/eth/v1/beacon/light_client/bootstrap/" + checkpoint.String()) if err != nil { @@ -267,9 +282,10 @@ func (api *RestApi) GetCheckpointData(ctx context.Context, checkpoint common.Has } -// beacon block root -> exec block -func (api *RestApi) GetExecutionPayload( /*ctx context.Context, */ blockRoot, execRoot common.Hash) (*types.Block, error) { - resp, err := http.Get(api.Url + "/eth/v2/beacon/blocks/" + blockRoot.Hex()) +// GetExecutionPayload fetches the execution block belonging to the beacon block specified +// by beaconRoot and validates its block hash against the expected execRoot. +func (api *RestApi) GetExecutionPayload(beaconRoot, execRoot common.Hash) (*types.Block, error) { + resp, err := http.Get(api.Url + "/eth/v2/beacon/blocks/" + beaconRoot.Hex()) if err != nil { return nil, err } diff --git a/light/beacon/api/syncer.go b/light/beacon/api/syncer.go index e61c272915cb..e90e2f79df01 100644 --- a/light/beacon/api/syncer.go +++ b/light/beacon/api/syncer.go @@ -32,6 +32,7 @@ const ( headPollCount = 50 ) +// CommitteeSyncer syncs committee updates and signed heads from RestApi to SyncCommitteeTracker type CommitteeSyncer struct { api *RestApi @@ -44,7 +45,8 @@ type CommitteeSyncer struct { headTriggerCh, closedCh chan struct{} } -// genesisData is only needed when light syncing (using GetInitData for bootstrap) +// NewCommitteeSyncer creates a new CommitteeSyncer +// Note: genesisData is only needed when light syncing (using GetInitData for bootstrap) func NewCommitteeSyncer(api *RestApi, genesisData beacon.GenesisData) *CommitteeSyncer { updateCache, _ := lru.New(beacon.MaxCommitteeUpdateFetch) committeeCache, _ := lru.New(beacon.MaxCommitteeUpdateFetch / beacon.CommitteeCostFactor) @@ -58,28 +60,23 @@ func NewCommitteeSyncer(api *RestApi, genesisData beacon.GenesisData) *Committee } } +// Start starts the syncing of the given SyncCommitteeTracker func (cs *CommitteeSyncer) Start(sct *beacon.SyncCommitteeTracker) { cs.sct = sct sct.SyncWithPeer(cs, nil) go cs.headPollLoop() } -func (cs *CommitteeSyncer) TriggerNewHead() { - cs.updateCache.Purge() - cs.committeeCache.Purge() - - select { - case cs.headTriggerCh <- struct{}{}: - default: - } -} - +// Stop stops the syncing process func (cs *CommitteeSyncer) Stop() { cs.sct.Disconnect(cs) close(cs.headTriggerCh) <-cs.closedCh } +// headPollLoop polls the signed head update endpoint and feeds signed heads into the tracker. +// Twice each sync period (not long before the end of the period and after the end of each period) +// it queries the best available committee update and advertises it to the tracker. func (cs *CommitteeSyncer) headPollLoop() { //fmt.Println("Started headPollLoop()") timer := time.NewTimer(0) @@ -98,6 +95,8 @@ func (cs *CommitteeSyncer) headPollLoop() { if head, err := cs.api.GetHeadUpdate(); err == nil { //fmt.Println(" headPollLoop head update for slot", head.Header.Slot, nextAdvertiseSlot) if !head.Equal(&lastHead) { + cs.updateCache.Purge() + cs.committeeCache.Purge() //fmt.Println("head poll: new head", head.Header.Slot, nextAdvertiseSlot) cs.sct.AddSignedHeads(cs, []beacon.SignedHead{head}) lastHead = head @@ -134,6 +133,9 @@ func (cs *CommitteeSyncer) headPollLoop() { } } +// advertiseUpdates queries committee updates that the tracker does not have or might have +// improved since the last query and advertises them to the tracker. The tracker might then +// fetch the actual updates and committees via GetBestCommitteeProofs. func (cs *CommitteeSyncer) advertiseUpdates(lastPeriod uint64) bool { nextPeriod := cs.sct.NextPeriod() if nextPeriod < 1 { @@ -161,6 +163,7 @@ func (cs *CommitteeSyncer) advertiseUpdates(lastPeriod uint64) bool { return true } +// GetBestCommitteeProofs fetches updates and committees for the specified periods func (cs *CommitteeSyncer) GetBestCommitteeProofs(ctx context.Context, req beacon.CommitteeRequest) (beacon.CommitteeReply, error) { reply := beacon.CommitteeReply{ Updates: make([]beacon.LightClientUpdate, len(req.UpdatePeriods)), @@ -180,6 +183,7 @@ func (cs *CommitteeSyncer) GetBestCommitteeProofs(ctx context.Context, req beaco return reply, nil } +// getBestUpdate returns the best update for the given period func (cs *CommitteeSyncer) getBestUpdate(period uint64) (beacon.LightClientUpdate, error) { if c, _ := cs.updateCache.Get(period); c != nil { return c.(beacon.LightClientUpdate), nil @@ -188,7 +192,9 @@ func (cs *CommitteeSyncer) getBestUpdate(period uint64) (beacon.LightClientUpdat return update, err } +// getCommittee returns the committee for the given period func (cs *CommitteeSyncer) getCommittee(period uint64) ([]byte, error) { + //TODO period 0 (same as period 1 if available?) if cs.checkpointCommittee != nil && period == cs.checkpointPeriod { return cs.checkpointCommittee, nil } @@ -199,6 +205,8 @@ func (cs *CommitteeSyncer) getCommittee(period uint64) ([]byte, error) { return committee, err } +// getBestUpdateAndCommittee fetches the best update for period and corresponding committee +// for period+1 and caches the results until a new head is received by headPollLoop func (cs *CommitteeSyncer) getBestUpdateAndCommittee(period uint64) (beacon.LightClientUpdate, []byte, error) { update, committee, err := cs.api.GetBestUpdateAndCommittee(period) if err != nil { @@ -209,6 +217,8 @@ func (cs *CommitteeSyncer) getBestUpdateAndCommittee(period uint64) (beacon.Ligh return update, committee, nil } +// GetInitData fetches the bootstrap data and returns LightClientInitData (the corresponding +// committee is stored so that a subsequent GetBestCommitteeProofs can return it when requested) func (cs *CommitteeSyncer) GetInitData(ctx context.Context, checkpoint common.Hash) (beacon.Header, beacon.LightClientInitData, error) { if cs.genesisData == (beacon.GenesisData{}) { return beacon.Header{}, beacon.LightClientInitData{}, errors.New("missing genesis data") @@ -221,10 +231,12 @@ func (cs *CommitteeSyncer) GetInitData(ctx context.Context, checkpoint common.Ha return header, beacon.LightClientInitData{GenesisData: cs.genesisData, CheckpointData: checkpointData}, nil } +// ClosedChannel returns a channel that is closed when the syncer is stopped func (cs *CommitteeSyncer) ClosedChannel() chan struct{} { return cs.closedCh } +// WrongReply is called by the tracker when the RestApi has provided wrong committee updates or signed heads func (cs *CommitteeSyncer) WrongReply(description string) { log.Error("Beacon node API data source delivered wrong reply", "error", description) } diff --git a/light/beacon/sync_committee.go b/light/beacon/sync_committee.go index 3fedc21e429f..2089d1a341d1 100644 --- a/light/beacon/sync_committee.go +++ b/light/beacon/sync_committee.go @@ -22,6 +22,7 @@ import ( "context" "encoding/binary" + "errors" "fmt" "io" "math" @@ -252,38 +253,11 @@ func (s *SyncCommitteeTracker) deleteBestUpdate(period uint64) { s.updateInfoChanged() } -// verifyUpdate checks whether the sync committee Merkle proof and the header signature is -// correct and the update fits into the specified constraints +// verifyUpdate checks whether the header signature is correct and the update fits into the specified constraints +// (assumes that the update has been successfully validated previously) func (s *SyncCommitteeTracker) verifyUpdate(update *LightClientUpdate) bool { - //fmt.Println("verify update", update) - if update.Header.Slot&0x1fff == 0x1fff { - // last slot of each period is not suitable for an update because it is signed by the next period's sync committee, proves the same committee it is signed by - return false - } - if !s.checkForksAndConstraints(update) { - return false - } - var checkRoot common.Hash - if update.hasFinality() { - if update.FinalizedHeader.Slot>>13 != update.Header.Slot>>13 { - // finalized header is from the previous period, proves the same committee it is signed by - return false - } - if root, ok := VerifySingleProof(update.FinalityBranch, BsiFinalBlock, MerkleValue(update.FinalizedHeader.Hash()), 0); !ok || root != update.Header.StateRoot { - return false - } - checkRoot = update.FinalizedHeader.StateRoot - } else { - checkRoot = update.Header.StateRoot - } - if root, ok := VerifySingleProof(update.NextSyncCommitteeBranch, BsiNextSyncCommittee, MerkleValue(update.NextSyncCommitteeRoot), 0); !ok || root != checkRoot { - if ok && root == update.Header.StateRoot { - log.Warn("update.NextSyncCommitteeBranch rooted in update.Header.StateRoot (Lodestar bug workaround applied)") - } else { - return false - } - } - return s.verifySignature(SignedHead{Header: update.Header, Signature: update.SyncCommitteeSignature, BitMask: update.SyncCommitteeBits}) + return s.checkForksAndConstraints(update) && + s.verifySignature(SignedHead{Header: update.Header, Signature: update.SyncCommitteeSignature, BitMask: update.SyncCommitteeBits}) } const ( @@ -1026,6 +1000,34 @@ type LightClientUpdate struct { score UpdateScore } +func (update *LightClientUpdate) Validate() error { + //fmt.Println("verify update", update) + if update.Header.Slot&0x1fff == 0x1fff { + // last slot of each period is not suitable for an update because it is signed by the next period's sync committee, proves the same committee it is signed by + return errors.New("Last slot of period") + } + var checkRoot common.Hash + if update.hasFinality() { + if update.FinalizedHeader.Slot>>13 != update.Header.Slot>>13 { + return errors.New("FinalizedHeader is from previous period") // proves the same committee it is signed by + } + if root, ok := VerifySingleProof(update.FinalityBranch, BsiFinalBlock, MerkleValue(update.FinalizedHeader.Hash()), 0); !ok || root != update.Header.StateRoot { + return errors.New("Invalid FinalizedHeader merkle proof") + } + checkRoot = update.FinalizedHeader.StateRoot + } else { + checkRoot = update.Header.StateRoot + } + if root, ok := VerifySingleProof(update.NextSyncCommitteeBranch, BsiNextSyncCommittee, MerkleValue(update.NextSyncCommitteeRoot), 0); !ok || root != checkRoot { + if ok && root == update.Header.StateRoot { + log.Warn("update.NextSyncCommitteeBranch rooted in update.Header.StateRoot (Lodestar bug workaround applied)") + } else { + return errors.New("Invalid NextSyncCommittee merkle proof") + } + } + return nil +} + func (l *LightClientUpdate) hasFinality() bool { return l.FinalizedHeader.BodyRoot != (common.Hash{}) }