Skip to content

Commit

Permalink
light/beacon: added comments, moved functions
Browse files Browse the repository at this point in the history
  • Loading branch information
zsfelfoldi committed Oct 1, 2022
1 parent 8af7869 commit 224af70
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 85 deletions.
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
102 changes: 59 additions & 43 deletions light/beacon/api/rest_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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{}) {
Expand Down Expand Up @@ -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++ {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
34 changes: 23 additions & 11 deletions light/beacon/api/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
headPollCount = 50
)

// CommitteeSyncer syncs committee updates and signed heads from RestApi to SyncCommitteeTracker
type CommitteeSyncer struct {
api *RestApi

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)),
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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 {
Expand All @@ -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")
Expand All @@ -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)
}
64 changes: 33 additions & 31 deletions light/beacon/sync_committee.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"context"
"encoding/binary"

"errors"
"fmt"
"io"
"math"
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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{})
}
Expand Down

0 comments on commit 224af70

Please sign in to comment.