github.com/ethereum/go-ethereum@v1.14.3/beacon/light/sync/update_sync.go (about)

     1  // Copyright 2023 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 details.
    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 sync
    18  
    19  import (
    20  	"sort"
    21  
    22  	"github.com/ethereum/go-ethereum/beacon/light"
    23  	"github.com/ethereum/go-ethereum/beacon/light/request"
    24  	"github.com/ethereum/go-ethereum/beacon/params"
    25  	"github.com/ethereum/go-ethereum/beacon/types"
    26  	"github.com/ethereum/go-ethereum/common"
    27  	"github.com/ethereum/go-ethereum/log"
    28  )
    29  
    30  const maxUpdateRequest = 8 // maximum number of updates requested in a single request
    31  
    32  type committeeChain interface {
    33  	CheckpointInit(bootstrap types.BootstrapData) error
    34  	InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error
    35  	NextSyncPeriod() (uint64, bool)
    36  }
    37  
    38  // CheckpointInit implements request.Module; it fetches the light client bootstrap
    39  // data belonging to the given checkpoint hash and initializes the committee chain
    40  // if successful.
    41  type CheckpointInit struct {
    42  	chain          committeeChain
    43  	checkpointHash common.Hash
    44  	locked         request.ServerAndID
    45  	initialized    bool
    46  	// per-server state is used to track the state of requesting checkpoint header
    47  	// info. Part of this info (canonical and finalized state) is not validated
    48  	// and therefore it is requested from each server separately after it has
    49  	// reported a missing checkpoint (which is also not validated info).
    50  	serverState map[request.Server]serverState
    51  	// the following fields are used to determine whether the checkpoint is on
    52  	// epoch boundary. This information is validated and therefore stored globally.
    53  	parentHash                  common.Hash
    54  	hasEpochInfo, epochBoundary bool
    55  	cpSlot, parentSlot          uint64
    56  }
    57  
    58  const (
    59  	ssDefault         = iota // no action yet or checkpoint requested
    60  	ssNeedHeader             // checkpoint req failed, need cp header
    61  	ssHeaderRequested        // cp header requested
    62  	ssNeedParent             // cp header slot %32 != 0, need parent to check epoch boundary
    63  	ssParentRequested        // cp parent header requested
    64  	ssPrintStatus            // has all necessary info, print log message if init still not successful
    65  	ssDone                   // log message printed, no more action required
    66  )
    67  
    68  type serverState struct {
    69  	state                           int
    70  	hasHeader, canonical, finalized bool // stored per server because not validated
    71  }
    72  
    73  // NewCheckpointInit creates a new CheckpointInit.
    74  func NewCheckpointInit(chain committeeChain, checkpointHash common.Hash) *CheckpointInit {
    75  	return &CheckpointInit{
    76  		chain:          chain,
    77  		checkpointHash: checkpointHash,
    78  		serverState:    make(map[request.Server]serverState),
    79  	}
    80  }
    81  
    82  // Process implements request.Module.
    83  func (s *CheckpointInit) Process(requester request.Requester, events []request.Event) {
    84  	if s.initialized {
    85  		return
    86  	}
    87  
    88  	for _, event := range events {
    89  		switch event.Type {
    90  		case request.EvResponse, request.EvFail, request.EvTimeout:
    91  			sid, req, resp := event.RequestInfo()
    92  			if s.locked == sid {
    93  				s.locked = request.ServerAndID{}
    94  			}
    95  			if event.Type == request.EvTimeout {
    96  				continue
    97  			}
    98  			switch s.serverState[sid.Server].state {
    99  			case ssDefault:
   100  				if resp != nil {
   101  					if checkpoint := resp.(*types.BootstrapData); checkpoint.Header.Hash() == common.Hash(req.(ReqCheckpointData)) {
   102  						s.chain.CheckpointInit(*checkpoint)
   103  						s.initialized = true
   104  						return
   105  					}
   106  					requester.Fail(event.Server, "invalid checkpoint data")
   107  				}
   108  				s.serverState[sid.Server] = serverState{state: ssNeedHeader}
   109  			case ssHeaderRequested:
   110  				if resp == nil {
   111  					s.serverState[sid.Server] = serverState{state: ssPrintStatus}
   112  					continue
   113  				}
   114  				newState := serverState{
   115  					hasHeader: true,
   116  					canonical: resp.(RespHeader).Canonical,
   117  					finalized: resp.(RespHeader).Finalized,
   118  				}
   119  				s.cpSlot, s.parentHash = resp.(RespHeader).Header.Slot, resp.(RespHeader).Header.ParentRoot
   120  				if s.cpSlot%params.EpochLength == 0 {
   121  					s.hasEpochInfo, s.epochBoundary = true, true
   122  				}
   123  				if s.hasEpochInfo {
   124  					newState.state = ssPrintStatus
   125  				} else {
   126  					newState.state = ssNeedParent
   127  				}
   128  				s.serverState[sid.Server] = newState
   129  			case ssParentRequested:
   130  				s.parentSlot = resp.(RespHeader).Header.Slot
   131  				s.hasEpochInfo, s.epochBoundary = true, s.cpSlot/params.EpochLength > s.parentSlot/params.EpochLength
   132  				newState := s.serverState[sid.Server]
   133  				newState.state = ssPrintStatus
   134  				s.serverState[sid.Server] = newState
   135  			}
   136  
   137  		case request.EvUnregistered:
   138  			delete(s.serverState, event.Server)
   139  		}
   140  	}
   141  
   142  	// start a request if possible
   143  	for _, server := range requester.CanSendTo() {
   144  		switch s.serverState[server].state {
   145  		case ssDefault:
   146  			if s.locked == (request.ServerAndID{}) {
   147  				id := requester.Send(server, ReqCheckpointData(s.checkpointHash))
   148  				s.locked = request.ServerAndID{Server: server, ID: id}
   149  			}
   150  		case ssNeedHeader:
   151  			requester.Send(server, ReqHeader(s.checkpointHash))
   152  			newState := s.serverState[server]
   153  			newState.state = ssHeaderRequested
   154  			s.serverState[server] = newState
   155  		case ssNeedParent:
   156  			requester.Send(server, ReqHeader(s.parentHash))
   157  			newState := s.serverState[server]
   158  			newState.state = ssParentRequested
   159  			s.serverState[server] = newState
   160  		}
   161  	}
   162  
   163  	// print log message if necessary
   164  	for server, state := range s.serverState {
   165  		if state.state != ssPrintStatus {
   166  			continue
   167  		}
   168  		switch {
   169  		case !state.hasHeader:
   170  			log.Error("blsync: checkpoint block is not available, reported as unknown", "server", server.Name())
   171  		case !state.canonical:
   172  			log.Error("blsync: checkpoint block is not available, reported as non-canonical", "server", server.Name())
   173  		case !s.hasEpochInfo:
   174  			// should be available if hasHeader is true and state is ssPrintStatus
   175  			panic("checkpoint epoch info not available when printing retrieval status")
   176  		case !s.epochBoundary:
   177  			log.Error("blsync: checkpoint block is not first of epoch", "slot", s.cpSlot, "parent", s.parentSlot, "server", server.Name())
   178  		case !state.finalized:
   179  			log.Error("blsync: checkpoint block is reported as non-finalized", "server", server.Name())
   180  		default:
   181  			log.Error("blsync: checkpoint not available, but reported as finalized; specified checkpoint hash might be too old", "server", server.Name())
   182  		}
   183  		s.serverState[server] = serverState{state: ssDone}
   184  	}
   185  }
   186  
   187  // ForwardUpdateSync implements request.Module; it fetches updates between the
   188  // committee chain head and each server's announced head. Updates are fetched
   189  // in batches and multiple batches can also be requested in parallel.
   190  // Out of order responses are also handled; if a batch of updates cannot be added
   191  // to the chain immediately because of a gap then the future updates are
   192  // remembered until they can be processed.
   193  type ForwardUpdateSync struct {
   194  	chain          committeeChain
   195  	rangeLock      rangeLock
   196  	lockedIDs      map[request.ServerAndID]struct{}
   197  	processQueue   []updateResponse
   198  	nextSyncPeriod map[request.Server]uint64
   199  }
   200  
   201  // NewForwardUpdateSync creates a new ForwardUpdateSync.
   202  func NewForwardUpdateSync(chain committeeChain) *ForwardUpdateSync {
   203  	return &ForwardUpdateSync{
   204  		chain:          chain,
   205  		rangeLock:      make(rangeLock),
   206  		lockedIDs:      make(map[request.ServerAndID]struct{}),
   207  		nextSyncPeriod: make(map[request.Server]uint64),
   208  	}
   209  }
   210  
   211  // rangeLock allows locking sections of an integer space, preventing the syncing
   212  // mechanism from making requests again for sections where a not timed out request
   213  // is already pending or where already fetched and unprocessed data is available.
   214  type rangeLock map[uint64]int
   215  
   216  // lock locks or unlocks the given section, depending on the sign of the add parameter.
   217  func (r rangeLock) lock(first, count uint64, add int) {
   218  	for i := first; i < first+count; i++ {
   219  		if v := r[i] + add; v > 0 {
   220  			r[i] = v
   221  		} else {
   222  			delete(r, i)
   223  		}
   224  	}
   225  }
   226  
   227  // firstUnlocked returns the first unlocked section starting at or after start
   228  // and not longer than maxCount.
   229  func (r rangeLock) firstUnlocked(start, maxCount uint64) (first, count uint64) {
   230  	first = start
   231  	for {
   232  		if _, ok := r[first]; !ok {
   233  			break
   234  		}
   235  		first++
   236  	}
   237  	for {
   238  		count++
   239  		if count == maxCount {
   240  			break
   241  		}
   242  		if _, ok := r[first+count]; ok {
   243  			break
   244  		}
   245  	}
   246  	return
   247  }
   248  
   249  // lockRange locks the range belonging to the given update request, unless the
   250  // same request has already been locked
   251  func (s *ForwardUpdateSync) lockRange(sid request.ServerAndID, req ReqUpdates) {
   252  	if _, ok := s.lockedIDs[sid]; ok {
   253  		return
   254  	}
   255  	s.lockedIDs[sid] = struct{}{}
   256  	s.rangeLock.lock(req.FirstPeriod, req.Count, 1)
   257  }
   258  
   259  // unlockRange unlocks the range belonging to the given update request, unless
   260  // same request has already been unlocked
   261  func (s *ForwardUpdateSync) unlockRange(sid request.ServerAndID, req ReqUpdates) {
   262  	if _, ok := s.lockedIDs[sid]; !ok {
   263  		return
   264  	}
   265  	delete(s.lockedIDs, sid)
   266  	s.rangeLock.lock(req.FirstPeriod, req.Count, -1)
   267  }
   268  
   269  // verifyRange returns true if the number of updates and the individual update
   270  // periods in the response match the requested section.
   271  func (s *ForwardUpdateSync) verifyRange(request ReqUpdates, response RespUpdates) bool {
   272  	if uint64(len(response.Updates)) != request.Count || uint64(len(response.Committees)) != request.Count {
   273  		return false
   274  	}
   275  	for i, update := range response.Updates {
   276  		if update.AttestedHeader.Header.SyncPeriod() != request.FirstPeriod+uint64(i) {
   277  			return false
   278  		}
   279  	}
   280  	return true
   281  }
   282  
   283  // updateResponse is a response that has passed initial verification and has been
   284  // queued for processing. Note that an update response cannot be processed until
   285  // the previous updates have also been added to the chain.
   286  type updateResponse struct {
   287  	sid      request.ServerAndID
   288  	request  ReqUpdates
   289  	response RespUpdates
   290  }
   291  
   292  // updateResponseList implements sort.Sort and sorts update request/response events by FirstPeriod.
   293  type updateResponseList []updateResponse
   294  
   295  func (u updateResponseList) Len() int      { return len(u) }
   296  func (u updateResponseList) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
   297  func (u updateResponseList) Less(i, j int) bool {
   298  	return u[i].request.FirstPeriod < u[j].request.FirstPeriod
   299  }
   300  
   301  // Process implements request.Module.
   302  func (s *ForwardUpdateSync) Process(requester request.Requester, events []request.Event) {
   303  	for _, event := range events {
   304  		switch event.Type {
   305  		case request.EvResponse, request.EvFail, request.EvTimeout:
   306  			sid, rq, rs := event.RequestInfo()
   307  			req := rq.(ReqUpdates)
   308  			var queued bool
   309  			if event.Type == request.EvResponse {
   310  				resp := rs.(RespUpdates)
   311  				if s.verifyRange(req, resp) {
   312  					// there is a response with a valid format; put it in the process queue
   313  					s.processQueue = append(s.processQueue, updateResponse{sid: sid, request: req, response: resp})
   314  					s.lockRange(sid, req)
   315  					queued = true
   316  				} else {
   317  					requester.Fail(event.Server, "invalid update range")
   318  				}
   319  			}
   320  			if !queued {
   321  				s.unlockRange(sid, req)
   322  			}
   323  		case EvNewOptimisticUpdate:
   324  			update := event.Data.(types.OptimisticUpdate)
   325  			s.nextSyncPeriod[event.Server] = types.SyncPeriod(update.SignatureSlot + 256)
   326  		case request.EvUnregistered:
   327  			delete(s.nextSyncPeriod, event.Server)
   328  		}
   329  	}
   330  
   331  	// try processing ordered list of available responses
   332  	sort.Sort(updateResponseList(s.processQueue))
   333  	for s.processQueue != nil {
   334  		u := s.processQueue[0]
   335  		if !s.processResponse(requester, u) {
   336  			break
   337  		}
   338  		s.unlockRange(u.sid, u.request)
   339  		s.processQueue = s.processQueue[1:]
   340  		if len(s.processQueue) == 0 {
   341  			s.processQueue = nil
   342  		}
   343  	}
   344  
   345  	// start new requests if possible
   346  	startPeriod, chainInit := s.chain.NextSyncPeriod()
   347  	if !chainInit {
   348  		return
   349  	}
   350  	for {
   351  		firstPeriod, maxCount := s.rangeLock.firstUnlocked(startPeriod, maxUpdateRequest)
   352  		var (
   353  			sendTo    request.Server
   354  			bestCount uint64
   355  		)
   356  		for _, server := range requester.CanSendTo() {
   357  			nextPeriod := s.nextSyncPeriod[server]
   358  			if nextPeriod <= firstPeriod {
   359  				continue
   360  			}
   361  			count := maxCount
   362  			if nextPeriod < firstPeriod+maxCount {
   363  				count = nextPeriod - firstPeriod
   364  			}
   365  			if count > bestCount {
   366  				sendTo, bestCount = server, count
   367  			}
   368  		}
   369  		if sendTo == nil {
   370  			return
   371  		}
   372  		req := ReqUpdates{FirstPeriod: firstPeriod, Count: bestCount}
   373  		id := requester.Send(sendTo, req)
   374  		s.lockRange(request.ServerAndID{Server: sendTo, ID: id}, req)
   375  	}
   376  }
   377  
   378  // processResponse adds the fetched updates and committees to the committee chain.
   379  // Returns true in case of full or partial success.
   380  func (s *ForwardUpdateSync) processResponse(requester request.Requester, u updateResponse) (success bool) {
   381  	for i, update := range u.response.Updates {
   382  		if err := s.chain.InsertUpdate(update, u.response.Committees[i]); err != nil {
   383  			if err == light.ErrInvalidPeriod {
   384  				// there is a gap in the update periods; stop processing without
   385  				// failing and try again next time
   386  				return
   387  			}
   388  			if err == light.ErrInvalidUpdate || err == light.ErrWrongCommitteeRoot || err == light.ErrCannotReorg {
   389  				requester.Fail(u.sid.Server, "invalid update received")
   390  			} else {
   391  				log.Error("Unexpected InsertUpdate error", "error", err)
   392  			}
   393  			return
   394  		}
   395  		success = true
   396  	}
   397  	return
   398  }