github.com/ethereum/go-ethereum@v1.16.1/beacon/light/api/light_api.go (about)

     1  // Copyright 2022 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more detaiapi.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package api
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"net/url"
    27  	"strconv"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/donovanhide/eventsource"
    32  	"github.com/ethereum/go-ethereum/beacon/merkle"
    33  	"github.com/ethereum/go-ethereum/beacon/params"
    34  	"github.com/ethereum/go-ethereum/beacon/types"
    35  	"github.com/ethereum/go-ethereum/common"
    36  	"github.com/ethereum/go-ethereum/common/hexutil"
    37  	"github.com/ethereum/go-ethereum/log"
    38  )
    39  
    40  var (
    41  	ErrNotFound = errors.New("404 Not Found")
    42  	ErrInternal = errors.New("500 Internal Server Error")
    43  )
    44  
    45  type CommitteeUpdate struct {
    46  	Update            types.LightClientUpdate
    47  	NextSyncCommittee types.SerializedSyncCommittee
    48  }
    49  
    50  // See data structure definition here:
    51  // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientupdate
    52  type committeeUpdateJson struct {
    53  	Version string              `json:"version"`
    54  	Data    committeeUpdateData `json:"data"`
    55  }
    56  
    57  type committeeUpdateData struct {
    58  	Header                  jsonBeaconHeader              `json:"attested_header"`
    59  	NextSyncCommittee       types.SerializedSyncCommittee `json:"next_sync_committee"`
    60  	NextSyncCommitteeBranch merkle.Values                 `json:"next_sync_committee_branch"`
    61  	FinalizedHeader         *jsonBeaconHeader             `json:"finalized_header,omitempty"`
    62  	FinalityBranch          merkle.Values                 `json:"finality_branch,omitempty"`
    63  	SyncAggregate           types.SyncAggregate           `json:"sync_aggregate"`
    64  	SignatureSlot           common.Decimal                `json:"signature_slot"`
    65  }
    66  
    67  type jsonBeaconHeader struct {
    68  	Beacon types.Header `json:"beacon"`
    69  }
    70  
    71  type jsonHeaderWithExecProof struct {
    72  	Beacon          types.Header    `json:"beacon"`
    73  	Execution       json.RawMessage `json:"execution"`
    74  	ExecutionBranch merkle.Values   `json:"execution_branch"`
    75  }
    76  
    77  // UnmarshalJSON unmarshals from JSON.
    78  func (u *CommitteeUpdate) UnmarshalJSON(input []byte) error {
    79  	var dec committeeUpdateJson
    80  	if err := json.Unmarshal(input, &dec); err != nil {
    81  		return err
    82  	}
    83  	u.NextSyncCommittee = dec.Data.NextSyncCommittee
    84  	u.Update = types.LightClientUpdate{
    85  		Version: dec.Version,
    86  		AttestedHeader: types.SignedHeader{
    87  			Header:        dec.Data.Header.Beacon,
    88  			Signature:     dec.Data.SyncAggregate,
    89  			SignatureSlot: uint64(dec.Data.SignatureSlot),
    90  		},
    91  		NextSyncCommitteeRoot:   u.NextSyncCommittee.Root(),
    92  		NextSyncCommitteeBranch: dec.Data.NextSyncCommitteeBranch,
    93  		FinalityBranch:          dec.Data.FinalityBranch,
    94  	}
    95  	if dec.Data.FinalizedHeader != nil {
    96  		u.Update.FinalizedHeader = &dec.Data.FinalizedHeader.Beacon
    97  	}
    98  	return nil
    99  }
   100  
   101  // fetcher is an interface useful for debug-harnessing the http api.
   102  type fetcher interface {
   103  	Do(req *http.Request) (*http.Response, error)
   104  }
   105  
   106  // BeaconLightApi requests light client information from a beacon node REST API.
   107  // Note: all required API endpoints are currently only implemented by Lodestar.
   108  type BeaconLightApi struct {
   109  	url           string
   110  	client        fetcher
   111  	customHeaders map[string]string
   112  }
   113  
   114  func NewBeaconLightApi(url string, customHeaders map[string]string) *BeaconLightApi {
   115  	return &BeaconLightApi{
   116  		url: url,
   117  		client: &http.Client{
   118  			Timeout: time.Second * 10,
   119  		},
   120  		customHeaders: customHeaders,
   121  	}
   122  }
   123  
   124  func (api *BeaconLightApi) httpGet(path string, params url.Values) ([]byte, error) {
   125  	uri, err := api.buildURL(path, params)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	req, err := http.NewRequest("GET", uri, nil)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	for k, v := range api.customHeaders {
   134  		req.Header.Set(k, v)
   135  	}
   136  	resp, err := api.client.Do(req)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	defer resp.Body.Close()
   141  	switch resp.StatusCode {
   142  	case 200:
   143  		return io.ReadAll(resp.Body)
   144  	case 404:
   145  		return nil, ErrNotFound
   146  	case 500:
   147  		return nil, ErrInternal
   148  	default:
   149  		return nil, fmt.Errorf("unexpected error from API endpoint \"%s\": status code %d", path, resp.StatusCode)
   150  	}
   151  }
   152  
   153  // GetBestUpdatesAndCommittees fetches and validates LightClientUpdate for given
   154  // period and full serialized committee for the next period (committee root hash
   155  // equals update.NextSyncCommitteeRoot).
   156  // Note that the results are validated but the update signature should be verified
   157  // by the caller as its validity depends on the update chain.
   158  func (api *BeaconLightApi) GetBestUpdatesAndCommittees(firstPeriod, count uint64) ([]*types.LightClientUpdate, []*types.SerializedSyncCommittee, error) {
   159  	resp, err := api.httpGet("/eth/v1/beacon/light_client/updates", map[string][]string{
   160  		"start_period": {strconv.FormatUint(firstPeriod, 10)},
   161  		"count":        {strconv.FormatUint(count, 10)},
   162  	})
   163  	if err != nil {
   164  		return nil, nil, err
   165  	}
   166  
   167  	var data []CommitteeUpdate
   168  	if err := json.Unmarshal(resp, &data); err != nil {
   169  		return nil, nil, err
   170  	}
   171  	if len(data) != int(count) {
   172  		return nil, nil, errors.New("invalid number of committee updates")
   173  	}
   174  	updates := make([]*types.LightClientUpdate, int(count))
   175  	committees := make([]*types.SerializedSyncCommittee, int(count))
   176  	for i, d := range data {
   177  		if d.Update.AttestedHeader.Header.SyncPeriod() != firstPeriod+uint64(i) {
   178  			return nil, nil, errors.New("wrong committee update header period")
   179  		}
   180  		if err := d.Update.Validate(); err != nil {
   181  			return nil, nil, err
   182  		}
   183  		if d.NextSyncCommittee.Root() != d.Update.NextSyncCommitteeRoot {
   184  			return nil, nil, errors.New("wrong sync committee root")
   185  		}
   186  		updates[i], committees[i] = new(types.LightClientUpdate), new(types.SerializedSyncCommittee)
   187  		*updates[i], *committees[i] = d.Update, d.NextSyncCommittee
   188  	}
   189  	return updates, committees, nil
   190  }
   191  
   192  // GetOptimisticUpdate fetches the latest available optimistic update.
   193  // Note that the signature should be verified by the caller as its validity
   194  // depends on the update chain.
   195  //
   196  // See data structure definition here:
   197  // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
   198  func (api *BeaconLightApi) GetOptimisticUpdate() (types.OptimisticUpdate, error) {
   199  	resp, err := api.httpGet("/eth/v1/beacon/light_client/optimistic_update", nil)
   200  	if err != nil {
   201  		return types.OptimisticUpdate{}, err
   202  	}
   203  	return decodeOptimisticUpdate(resp)
   204  }
   205  
   206  func decodeOptimisticUpdate(enc []byte) (types.OptimisticUpdate, error) {
   207  	var data struct {
   208  		Version string `json:"version"`
   209  		Data    struct {
   210  			Attested      jsonHeaderWithExecProof `json:"attested_header"`
   211  			Aggregate     types.SyncAggregate     `json:"sync_aggregate"`
   212  			SignatureSlot common.Decimal          `json:"signature_slot"`
   213  		} `json:"data"`
   214  	}
   215  	if err := json.Unmarshal(enc, &data); err != nil {
   216  		return types.OptimisticUpdate{}, err
   217  	}
   218  	// Decode the execution payload headers.
   219  	attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution)
   220  	if err != nil {
   221  		return types.OptimisticUpdate{}, fmt.Errorf("invalid attested header: %v", err)
   222  	}
   223  	if data.Data.Attested.Beacon.StateRoot == (common.Hash{}) {
   224  		// workaround for different event encoding format in Lodestar
   225  		if err := json.Unmarshal(enc, &data.Data); err != nil {
   226  			return types.OptimisticUpdate{}, err
   227  		}
   228  	}
   229  
   230  	if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize {
   231  		return types.OptimisticUpdate{}, errors.New("invalid sync_committee_bits length")
   232  	}
   233  	if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize {
   234  		return types.OptimisticUpdate{}, errors.New("invalid sync_committee_signature length")
   235  	}
   236  	return types.OptimisticUpdate{
   237  		Attested: types.HeaderWithExecProof{
   238  			Header:        data.Data.Attested.Beacon,
   239  			PayloadHeader: attestedExecHeader,
   240  			PayloadBranch: data.Data.Attested.ExecutionBranch,
   241  		},
   242  		Signature:     data.Data.Aggregate,
   243  		SignatureSlot: uint64(data.Data.SignatureSlot),
   244  	}, nil
   245  }
   246  
   247  // GetFinalityUpdate fetches the latest available finality update.
   248  //
   249  // See data structure definition here:
   250  // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientfinalityupdate
   251  func (api *BeaconLightApi) GetFinalityUpdate() (types.FinalityUpdate, error) {
   252  	resp, err := api.httpGet("/eth/v1/beacon/light_client/finality_update", nil)
   253  	if err != nil {
   254  		return types.FinalityUpdate{}, err
   255  	}
   256  	return decodeFinalityUpdate(resp)
   257  }
   258  
   259  func decodeFinalityUpdate(enc []byte) (types.FinalityUpdate, error) {
   260  	var data struct {
   261  		Version string `json:"version"`
   262  		Data    struct {
   263  			Attested       jsonHeaderWithExecProof `json:"attested_header"`
   264  			Finalized      jsonHeaderWithExecProof `json:"finalized_header"`
   265  			FinalityBranch merkle.Values           `json:"finality_branch"`
   266  			Aggregate      types.SyncAggregate     `json:"sync_aggregate"`
   267  			SignatureSlot  common.Decimal          `json:"signature_slot"`
   268  		}
   269  	}
   270  	if err := json.Unmarshal(enc, &data); err != nil {
   271  		return types.FinalityUpdate{}, err
   272  	}
   273  	// Decode the execution payload headers.
   274  	attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution)
   275  	if err != nil {
   276  		return types.FinalityUpdate{}, fmt.Errorf("invalid attested header: %v", err)
   277  	}
   278  	finalizedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Finalized.Execution)
   279  	if err != nil {
   280  		return types.FinalityUpdate{}, fmt.Errorf("invalid finalized header: %v", err)
   281  	}
   282  	// Perform sanity checks.
   283  	if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize {
   284  		return types.FinalityUpdate{}, errors.New("invalid sync_committee_bits length")
   285  	}
   286  	if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize {
   287  		return types.FinalityUpdate{}, errors.New("invalid sync_committee_signature length")
   288  	}
   289  
   290  	return types.FinalityUpdate{
   291  		Version: data.Version,
   292  		Attested: types.HeaderWithExecProof{
   293  			Header:        data.Data.Attested.Beacon,
   294  			PayloadHeader: attestedExecHeader,
   295  			PayloadBranch: data.Data.Attested.ExecutionBranch,
   296  		},
   297  		Finalized: types.HeaderWithExecProof{
   298  			Header:        data.Data.Finalized.Beacon,
   299  			PayloadHeader: finalizedExecHeader,
   300  			PayloadBranch: data.Data.Finalized.ExecutionBranch,
   301  		},
   302  		FinalityBranch: data.Data.FinalityBranch,
   303  		Signature:      data.Data.Aggregate,
   304  		SignatureSlot:  uint64(data.Data.SignatureSlot),
   305  	}, nil
   306  }
   307  
   308  // GetHeader fetches and validates the beacon header with the given blockRoot.
   309  // If blockRoot is null hash then the latest head header is fetched.
   310  // The values of the canonical and finalized flags are also returned. Note that
   311  // these flags are not validated.
   312  func (api *BeaconLightApi) GetHeader(blockRoot common.Hash) (types.Header, bool, bool, error) {
   313  	var blockId string
   314  	if blockRoot == (common.Hash{}) {
   315  		blockId = "head"
   316  	} else {
   317  		blockId = blockRoot.Hex()
   318  	}
   319  	resp, err := api.httpGet(fmt.Sprintf("/eth/v1/beacon/headers/%s", blockId), nil)
   320  	if err != nil {
   321  		return types.Header{}, false, false, err
   322  	}
   323  
   324  	var data struct {
   325  		Finalized bool `json:"finalized"`
   326  		Data      struct {
   327  			Root      common.Hash `json:"root"`
   328  			Canonical bool        `json:"canonical"`
   329  			Header    struct {
   330  				Message   types.Header  `json:"message"`
   331  				Signature hexutil.Bytes `json:"signature"`
   332  			} `json:"header"`
   333  		} `json:"data"`
   334  	}
   335  	if err := json.Unmarshal(resp, &data); err != nil {
   336  		return types.Header{}, false, false, err
   337  	}
   338  	header := data.Data.Header.Message
   339  	if blockRoot == (common.Hash{}) {
   340  		blockRoot = data.Data.Root
   341  	}
   342  	if header.Hash() != blockRoot {
   343  		return types.Header{}, false, false, errors.New("retrieved beacon header root does not match")
   344  	}
   345  	return header, data.Data.Canonical, data.Finalized, nil
   346  }
   347  
   348  // GetCheckpointData fetches and validates bootstrap data belonging to the given checkpoint.
   349  func (api *BeaconLightApi) GetCheckpointData(checkpointHash common.Hash) (*types.BootstrapData, error) {
   350  	resp, err := api.httpGet(fmt.Sprintf("/eth/v1/beacon/light_client/bootstrap/0x%x", checkpointHash[:]), nil)
   351  	if err != nil {
   352  		return nil, err
   353  	}
   354  
   355  	// See data structure definition here:
   356  	// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientbootstrap
   357  	type bootstrapData struct {
   358  		Version string `json:"version"`
   359  		Data    struct {
   360  			Header          jsonBeaconHeader               `json:"header"`
   361  			Committee       *types.SerializedSyncCommittee `json:"current_sync_committee"`
   362  			CommitteeBranch merkle.Values                  `json:"current_sync_committee_branch"`
   363  		} `json:"data"`
   364  	}
   365  
   366  	var data bootstrapData
   367  	if err := json.Unmarshal(resp, &data); err != nil {
   368  		return nil, err
   369  	}
   370  	if data.Data.Committee == nil {
   371  		return nil, errors.New("sync committee is missing")
   372  	}
   373  	header := data.Data.Header.Beacon
   374  	if header.Hash() != checkpointHash {
   375  		return nil, fmt.Errorf("invalid checkpoint block header, have %v want %v", header.Hash(), checkpointHash)
   376  	}
   377  	checkpoint := &types.BootstrapData{
   378  		Version:         data.Version,
   379  		Header:          header,
   380  		CommitteeBranch: data.Data.CommitteeBranch,
   381  		CommitteeRoot:   data.Data.Committee.Root(),
   382  		Committee:       data.Data.Committee,
   383  	}
   384  	if err := checkpoint.Validate(); err != nil {
   385  		return nil, fmt.Errorf("invalid checkpoint: %w", err)
   386  	}
   387  	if checkpoint.Header.Hash() != checkpointHash {
   388  		return nil, errors.New("wrong checkpoint hash")
   389  	}
   390  	return checkpoint, nil
   391  }
   392  
   393  func (api *BeaconLightApi) GetBeaconBlock(blockRoot common.Hash) (*types.BeaconBlock, error) {
   394  	resp, err := api.httpGet(fmt.Sprintf("/eth/v2/beacon/blocks/0x%x", blockRoot), nil)
   395  	if err != nil {
   396  		return nil, err
   397  	}
   398  
   399  	var beaconBlockMessage struct {
   400  		Version string `json:"version"`
   401  		Data    struct {
   402  			Message json.RawMessage `json:"message"`
   403  		}
   404  	}
   405  	if err := json.Unmarshal(resp, &beaconBlockMessage); err != nil {
   406  		return nil, fmt.Errorf("invalid block json data: %v", err)
   407  	}
   408  	block, err := types.BlockFromJSON(beaconBlockMessage.Version, beaconBlockMessage.Data.Message)
   409  	if err != nil {
   410  		return nil, err
   411  	}
   412  	computedRoot := block.Root()
   413  	if computedRoot != blockRoot {
   414  		return nil, fmt.Errorf("Beacon block root hash mismatch (expected: %x, got: %x)", blockRoot, computedRoot)
   415  	}
   416  	return block, nil
   417  }
   418  
   419  func decodeHeadEvent(enc []byte) (uint64, common.Hash, error) {
   420  	var data struct {
   421  		Slot  common.Decimal `json:"slot"`
   422  		Block common.Hash    `json:"block"`
   423  	}
   424  	if err := json.Unmarshal(enc, &data); err != nil {
   425  		return 0, common.Hash{}, err
   426  	}
   427  	return uint64(data.Slot), data.Block, nil
   428  }
   429  
   430  type HeadEventListener struct {
   431  	OnNewHead    func(slot uint64, blockRoot common.Hash)
   432  	OnOptimistic func(head types.OptimisticUpdate)
   433  	OnFinality   func(head types.FinalityUpdate)
   434  	OnError      func(err error)
   435  }
   436  
   437  // StartHeadListener creates an event subscription for heads and signed (optimistic)
   438  // head updates and calls the specified callback functions when they are received.
   439  // The callbacks are also called for the current head and optimistic head at startup.
   440  // They are never called concurrently.
   441  func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func() {
   442  	var (
   443  		ctx, closeCtx = context.WithCancel(context.Background())
   444  		streamCh      = make(chan *eventsource.Stream, 1)
   445  		wg            sync.WaitGroup
   446  	)
   447  
   448  	// When connected to a Lodestar node the subscription blocks until the first actual
   449  	// event arrives; therefore we create the subscription in a separate goroutine while
   450  	// letting the main goroutine sync up to the current head.
   451  	wg.Add(1)
   452  	go func() {
   453  		defer wg.Done()
   454  		stream := api.startEventStream(ctx, &listener)
   455  		if stream == nil {
   456  			// This case happens when the context was closed.
   457  			return
   458  		}
   459  		// Stream was opened, wait for close signal.
   460  		streamCh <- stream
   461  		<-ctx.Done()
   462  		stream.Close()
   463  	}()
   464  
   465  	wg.Add(1)
   466  	go func() {
   467  		defer wg.Done()
   468  
   469  		// Request initial data.
   470  		log.Trace("Requesting initial head header")
   471  		if head, _, _, err := api.GetHeader(common.Hash{}); err == nil {
   472  			log.Trace("Retrieved initial head header", "slot", head.Slot, "hash", head.Hash())
   473  			listener.OnNewHead(head.Slot, head.Hash())
   474  		} else {
   475  			log.Debug("Failed to retrieve initial head header", "error", err)
   476  		}
   477  		log.Trace("Requesting initial optimistic update")
   478  		if optimisticUpdate, err := api.GetOptimisticUpdate(); err == nil {
   479  			log.Trace("Retrieved initial optimistic update", "slot", optimisticUpdate.Attested.Slot, "hash", optimisticUpdate.Attested.Hash())
   480  			listener.OnOptimistic(optimisticUpdate)
   481  		} else {
   482  			log.Debug("Failed to retrieve initial optimistic update", "error", err)
   483  		}
   484  		log.Trace("Requesting initial finality update")
   485  		if finalityUpdate, err := api.GetFinalityUpdate(); err == nil {
   486  			log.Trace("Retrieved initial finality update", "slot", finalityUpdate.Finalized.Slot, "hash", finalityUpdate.Finalized.Hash())
   487  			listener.OnFinality(finalityUpdate)
   488  		} else {
   489  			log.Debug("Failed to retrieve initial finality update", "error", err)
   490  		}
   491  
   492  		log.Trace("Starting event stream processing loop")
   493  		// Receive the stream.
   494  		var stream *eventsource.Stream
   495  		select {
   496  		case stream = <-streamCh:
   497  		case <-ctx.Done():
   498  			log.Trace("Stopping event stream processing loop")
   499  			return
   500  		}
   501  
   502  		for {
   503  			select {
   504  			case event, ok := <-stream.Events:
   505  				if !ok {
   506  					log.Trace("Event stream closed")
   507  					return
   508  				}
   509  				log.Trace("New event received from event stream", "type", event.Event())
   510  				switch event.Event() {
   511  				case "head":
   512  					slot, blockRoot, err := decodeHeadEvent([]byte(event.Data()))
   513  					if err == nil {
   514  						listener.OnNewHead(slot, blockRoot)
   515  					} else {
   516  						listener.OnError(fmt.Errorf("error decoding head event: %v", err))
   517  					}
   518  				case "light_client_optimistic_update":
   519  					optimisticUpdate, err := decodeOptimisticUpdate([]byte(event.Data()))
   520  					if err == nil {
   521  						listener.OnOptimistic(optimisticUpdate)
   522  					} else {
   523  						listener.OnError(fmt.Errorf("error decoding optimistic update event: %v", err))
   524  					}
   525  				case "light_client_finality_update":
   526  					finalityUpdate, err := decodeFinalityUpdate([]byte(event.Data()))
   527  					if err == nil {
   528  						listener.OnFinality(finalityUpdate)
   529  					} else {
   530  						listener.OnError(fmt.Errorf("error decoding finality update event: %v", err))
   531  					}
   532  				default:
   533  					listener.OnError(fmt.Errorf("unexpected event: %s", event.Event()))
   534  				}
   535  
   536  			case err, ok := <-stream.Errors:
   537  				if !ok {
   538  					return
   539  				}
   540  				listener.OnError(err)
   541  			}
   542  		}
   543  	}()
   544  
   545  	return func() {
   546  		closeCtx()
   547  		wg.Wait()
   548  	}
   549  }
   550  
   551  // startEventStream establishes an event stream. This will keep retrying until the stream has been
   552  // established. It can only return nil when the context is canceled.
   553  func (api *BeaconLightApi) startEventStream(ctx context.Context, listener *HeadEventListener) *eventsource.Stream {
   554  	for retry := true; retry; retry = ctxSleep(ctx, 5*time.Second) {
   555  		log.Trace("Sending event subscription request")
   556  		uri, err := api.buildURL("/eth/v1/events", map[string][]string{"topics": {"head", "light_client_finality_update", "light_client_optimistic_update"}})
   557  		if err != nil {
   558  			listener.OnError(fmt.Errorf("error creating event subscription URL: %v", err))
   559  			continue
   560  		}
   561  		req, err := http.NewRequestWithContext(ctx, "GET", uri, nil)
   562  		if err != nil {
   563  			listener.OnError(fmt.Errorf("error creating event subscription request: %v", err))
   564  			continue
   565  		}
   566  		for k, v := range api.customHeaders {
   567  			req.Header.Set(k, v)
   568  		}
   569  		stream, err := eventsource.SubscribeWithRequest("", req)
   570  		if err != nil {
   571  			listener.OnError(fmt.Errorf("error creating event subscription: %v", err))
   572  			continue
   573  		}
   574  		log.Trace("Successfully created event stream")
   575  		return stream
   576  	}
   577  	return nil
   578  }
   579  
   580  func ctxSleep(ctx context.Context, timeout time.Duration) (ok bool) {
   581  	timer := time.NewTimer(timeout)
   582  	defer timer.Stop()
   583  	select {
   584  	case <-timer.C:
   585  		return true
   586  	case <-ctx.Done():
   587  		return false
   588  	}
   589  }
   590  
   591  func (api *BeaconLightApi) buildURL(path string, params url.Values) (string, error) {
   592  	uri, err := url.Parse(api.url)
   593  	if err != nil {
   594  		return "", err
   595  	}
   596  	uri = uri.JoinPath(path)
   597  	if params != nil {
   598  		uri.RawQuery = params.Encode()
   599  	}
   600  	return uri.String(), nil
   601  }