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