github.com/MetalBlockchain/metalgo@v1.11.9/snow/engine/snowman/syncer/state_syncer.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package syncer
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"math"
    10  
    11  	"go.uber.org/zap"
    12  
    13  	"github.com/MetalBlockchain/metalgo/database"
    14  	"github.com/MetalBlockchain/metalgo/ids"
    15  	"github.com/MetalBlockchain/metalgo/proto/pb/p2p"
    16  	"github.com/MetalBlockchain/metalgo/snow"
    17  	"github.com/MetalBlockchain/metalgo/snow/engine/common"
    18  	"github.com/MetalBlockchain/metalgo/snow/engine/snowman/block"
    19  	"github.com/MetalBlockchain/metalgo/snow/validators"
    20  	"github.com/MetalBlockchain/metalgo/utils/logging"
    21  	"github.com/MetalBlockchain/metalgo/utils/set"
    22  	"github.com/MetalBlockchain/metalgo/version"
    23  
    24  	safemath "github.com/MetalBlockchain/metalgo/utils/math"
    25  )
    26  
    27  // maxOutstandingBroadcastRequests is the maximum number of requests to have
    28  // outstanding when broadcasting.
    29  const maxOutstandingBroadcastRequests = 50
    30  
    31  var _ common.StateSyncer = (*stateSyncer)(nil)
    32  
    33  // summary content as received from network, along with accumulated weight.
    34  type weightedSummary struct {
    35  	summary block.StateSummary
    36  	weight  uint64
    37  }
    38  
    39  type stateSyncer struct {
    40  	Config
    41  
    42  	// list of NoOpsHandler for messages dropped by state syncer
    43  	common.AcceptedFrontierHandler
    44  	common.AcceptedHandler
    45  	common.AncestorsHandler
    46  	common.PutHandler
    47  	common.QueryHandler
    48  	common.ChitsHandler
    49  	common.AppHandler
    50  
    51  	started bool
    52  
    53  	// Tracks the last requestID that was used in a request
    54  	requestID uint32
    55  
    56  	stateSyncVM        block.StateSyncableVM
    57  	onDoneStateSyncing func(ctx context.Context, lastReqID uint32) error
    58  
    59  	// we track the (possibly nil) local summary to help engine
    60  	// choosing among multiple validated summaries
    61  	locallyAvailableSummary block.StateSummary
    62  
    63  	// Holds the beacons that were sampled for the accepted frontier
    64  	// Won't be consumed as seeders are reached out. Used to rescale
    65  	// alpha for frontiers
    66  	frontierSeeders validators.Manager
    67  	// IDs of validators we should request state summary frontier from.
    68  	// Will be consumed seeders are reached out for frontier.
    69  	targetSeeders set.Set[ids.NodeID]
    70  	// IDs of validators we requested a state summary frontier from
    71  	// but haven't received a reply yet. ID is cleared if/when reply arrives.
    72  	pendingSeeders set.Set[ids.NodeID]
    73  	// IDs of validators that failed to respond with their state summary frontier
    74  	failedSeeders set.Set[ids.NodeID]
    75  
    76  	// IDs of validators we should request filtering the accepted state summaries from
    77  	targetVoters set.Set[ids.NodeID]
    78  	// IDs of validators we requested filtering the accepted state summaries from
    79  	// but haven't received a reply yet. ID is cleared if/when reply arrives.
    80  	pendingVoters set.Set[ids.NodeID]
    81  	// IDs of validators that failed to respond with their filtered accepted state summaries
    82  	failedVoters set.Set[ids.NodeID]
    83  
    84  	// summaryID --> (summary, weight)
    85  	weightedSummaries map[ids.ID]*weightedSummary
    86  
    87  	// summaries received may be different even if referring to the same height
    88  	// we keep a list of deduplicated height ready for voting
    89  	summariesHeights       set.Set[uint64]
    90  	uniqueSummariesHeights []uint64
    91  }
    92  
    93  func New(
    94  	cfg Config,
    95  	onDoneStateSyncing func(ctx context.Context, lastReqID uint32) error,
    96  ) common.StateSyncer {
    97  	ssVM, _ := cfg.VM.(block.StateSyncableVM)
    98  	return &stateSyncer{
    99  		Config:                  cfg,
   100  		AcceptedFrontierHandler: common.NewNoOpAcceptedFrontierHandler(cfg.Ctx.Log),
   101  		AcceptedHandler:         common.NewNoOpAcceptedHandler(cfg.Ctx.Log),
   102  		AncestorsHandler:        common.NewNoOpAncestorsHandler(cfg.Ctx.Log),
   103  		PutHandler:              common.NewNoOpPutHandler(cfg.Ctx.Log),
   104  		QueryHandler:            common.NewNoOpQueryHandler(cfg.Ctx.Log),
   105  		ChitsHandler:            common.NewNoOpChitsHandler(cfg.Ctx.Log),
   106  		AppHandler:              cfg.VM,
   107  		stateSyncVM:             ssVM,
   108  		onDoneStateSyncing:      onDoneStateSyncing,
   109  	}
   110  }
   111  
   112  func (ss *stateSyncer) Context() *snow.ConsensusContext {
   113  	return ss.Ctx
   114  }
   115  
   116  func (ss *stateSyncer) Start(ctx context.Context, startReqID uint32) error {
   117  	ss.Ctx.Log.Info("starting state sync")
   118  
   119  	ss.Ctx.State.Set(snow.EngineState{
   120  		Type:  p2p.EngineType_ENGINE_TYPE_SNOWMAN,
   121  		State: snow.StateSyncing,
   122  	})
   123  	if err := ss.VM.SetState(ctx, snow.StateSyncing); err != nil {
   124  		return fmt.Errorf("failed to notify VM that state syncing has started: %w", err)
   125  	}
   126  
   127  	ss.requestID = startReqID
   128  
   129  	return ss.tryStartSyncing(ctx)
   130  }
   131  
   132  func (ss *stateSyncer) Connected(ctx context.Context, nodeID ids.NodeID, nodeVersion *version.Application) error {
   133  	if err := ss.VM.Connected(ctx, nodeID, nodeVersion); err != nil {
   134  		return err
   135  	}
   136  
   137  	if err := ss.StartupTracker.Connected(ctx, nodeID, nodeVersion); err != nil {
   138  		return err
   139  	}
   140  
   141  	return ss.tryStartSyncing(ctx)
   142  }
   143  
   144  func (ss *stateSyncer) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
   145  	if err := ss.VM.Disconnected(ctx, nodeID); err != nil {
   146  		return err
   147  	}
   148  
   149  	return ss.StartupTracker.Disconnected(ctx, nodeID)
   150  }
   151  
   152  // tryStartSyncing will start syncing the first time it is called while the
   153  // startupTracker is reporting that the protocol should start.
   154  func (ss *stateSyncer) tryStartSyncing(ctx context.Context) error {
   155  	if ss.started || !ss.StartupTracker.ShouldStart() {
   156  		return nil
   157  	}
   158  
   159  	ss.started = true
   160  	return ss.startup(ctx)
   161  }
   162  
   163  func (ss *stateSyncer) StateSummaryFrontier(ctx context.Context, nodeID ids.NodeID, requestID uint32, summaryBytes []byte) error {
   164  	// ignores any late responses
   165  	if requestID != ss.requestID {
   166  		ss.Ctx.Log.Debug("received out-of-sync StateSummaryFrontier message",
   167  			zap.Stringer("nodeID", nodeID),
   168  			zap.Uint32("expectedRequestID", ss.requestID),
   169  			zap.Uint32("requestID", requestID),
   170  		)
   171  		return nil
   172  	}
   173  
   174  	if !ss.pendingSeeders.Contains(nodeID) {
   175  		ss.Ctx.Log.Debug("received unexpected StateSummaryFrontier message",
   176  			zap.Stringer("nodeID", nodeID),
   177  		)
   178  		return nil
   179  	}
   180  
   181  	// Mark that we received a response from [nodeID]
   182  	ss.pendingSeeders.Remove(nodeID)
   183  
   184  	// retrieve summary ID and register frontier;
   185  	// make sure next beacons are reached out
   186  	// even in case invalid summaries are received
   187  	if summary, err := ss.stateSyncVM.ParseStateSummary(ctx, summaryBytes); err == nil {
   188  		ss.weightedSummaries[summary.ID()] = &weightedSummary{
   189  			summary: summary,
   190  		}
   191  
   192  		height := summary.Height()
   193  		if !ss.summariesHeights.Contains(height) {
   194  			ss.summariesHeights.Add(height)
   195  			ss.uniqueSummariesHeights = append(ss.uniqueSummariesHeights, height)
   196  		}
   197  	} else {
   198  		if ss.Ctx.Log.Enabled(logging.Verbo) {
   199  			ss.Ctx.Log.Verbo("failed to parse summary",
   200  				zap.Binary("summary", summaryBytes),
   201  				zap.Error(err),
   202  			)
   203  		} else {
   204  			ss.Ctx.Log.Debug("failed to parse summary",
   205  				zap.Error(err),
   206  			)
   207  		}
   208  	}
   209  
   210  	return ss.receivedStateSummaryFrontier(ctx)
   211  }
   212  
   213  func (ss *stateSyncer) GetStateSummaryFrontierFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error {
   214  	// ignores any late responses
   215  	if requestID != ss.requestID {
   216  		ss.Ctx.Log.Debug("received out-of-sync GetStateSummaryFrontierFailed message",
   217  			zap.Stringer("nodeID", nodeID),
   218  			zap.Uint32("expectedRequestID", ss.requestID),
   219  			zap.Uint32("requestID", requestID),
   220  		)
   221  		return nil
   222  	}
   223  
   224  	// Mark that we didn't get a response from [nodeID]
   225  	ss.failedSeeders.Add(nodeID)
   226  	ss.pendingSeeders.Remove(nodeID)
   227  
   228  	return ss.receivedStateSummaryFrontier(ctx)
   229  }
   230  
   231  func (ss *stateSyncer) receivedStateSummaryFrontier(ctx context.Context) error {
   232  	ss.sendGetStateSummaryFrontiers(ctx)
   233  
   234  	// still waiting on requests
   235  	if ss.pendingSeeders.Len() != 0 {
   236  		return nil
   237  	}
   238  
   239  	// All nodes reached out for the summary frontier have responded or timed out.
   240  	// If enough of them have indeed responded we'll go ahead and ask
   241  	// each state syncer (not just a sample) to filter the list of state summaries
   242  	// that we were told are on the accepted frontier.
   243  	// If we got too many timeouts, we restart state syncing hoping that network
   244  	// problems will go away and we can collect a qualified frontier.
   245  	// We assume the frontier is qualified after an alpha proportion of frontier seeders have responded
   246  	frontiersTotalWeight, err := ss.frontierSeeders.TotalWeight(ss.Ctx.SubnetID)
   247  	if err != nil {
   248  		return fmt.Errorf("failed to get total weight of frontier seeders for subnet %s: %w", ss.Ctx.SubnetID, err)
   249  	}
   250  	beaconsTotalWeight, err := ss.StateSyncBeacons.TotalWeight(ss.Ctx.SubnetID)
   251  	if err != nil {
   252  		return fmt.Errorf("failed to get total weight of state sync beacons for subnet %s: %w", ss.Ctx.SubnetID, err)
   253  	}
   254  	frontierAlpha := float64(frontiersTotalWeight*ss.Alpha) / float64(beaconsTotalWeight)
   255  	failedBeaconWeight, err := ss.StateSyncBeacons.SubsetWeight(ss.Ctx.SubnetID, ss.failedSeeders)
   256  	if err != nil {
   257  		return fmt.Errorf("failed to get total weight of failed beacons: %w", err)
   258  	}
   259  
   260  	frontierStake := frontiersTotalWeight - failedBeaconWeight
   261  	if float64(frontierStake) < frontierAlpha {
   262  		ss.Ctx.Log.Debug("restarting state sync",
   263  			zap.String("reason", "didn't receive enough frontiers"),
   264  			zap.Int("numFailedValidators", ss.failedSeeders.Len()),
   265  		)
   266  		return ss.startup(ctx)
   267  	}
   268  
   269  	ss.requestID++
   270  	ss.sendGetAcceptedStateSummaries(ctx)
   271  	return nil
   272  }
   273  
   274  func (ss *stateSyncer) AcceptedStateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, summaryIDs set.Set[ids.ID]) error {
   275  	// ignores any late responses
   276  	if requestID != ss.requestID {
   277  		ss.Ctx.Log.Debug("received out-of-sync AcceptedStateSummary message",
   278  			zap.Stringer("nodeID", nodeID),
   279  			zap.Uint32("expectedRequestID", ss.requestID),
   280  			zap.Uint32("requestID", requestID),
   281  		)
   282  		return nil
   283  	}
   284  
   285  	if !ss.pendingVoters.Contains(nodeID) {
   286  		ss.Ctx.Log.Debug("received unexpected AcceptedStateSummary message",
   287  			zap.Stringer("nodeID", nodeID),
   288  		)
   289  		return nil
   290  	}
   291  
   292  	// Mark that we received a response from [nodeID]
   293  	ss.pendingVoters.Remove(nodeID)
   294  
   295  	nodeWeight := ss.StateSyncBeacons.GetWeight(ss.Ctx.SubnetID, nodeID)
   296  	ss.Ctx.Log.Debug("adding weight to summaries",
   297  		zap.Stringer("nodeID", nodeID),
   298  		zap.Stringer("subnetID", ss.Ctx.SubnetID),
   299  		zap.Reflect("summaryIDs", summaryIDs),
   300  		zap.Uint64("nodeWeight", nodeWeight),
   301  	)
   302  	for summaryID := range summaryIDs {
   303  		ws, ok := ss.weightedSummaries[summaryID]
   304  		if !ok {
   305  			ss.Ctx.Log.Debug("skipping summary",
   306  				zap.String("reason", "unknown summary"),
   307  				zap.Stringer("nodeID", nodeID),
   308  				zap.Stringer("summaryID", summaryID),
   309  			)
   310  			continue
   311  		}
   312  
   313  		newWeight, err := safemath.Add64(nodeWeight, ws.weight)
   314  		if err != nil {
   315  			ss.Ctx.Log.Error("failed to calculate new summary weight",
   316  				zap.Stringer("nodeID", nodeID),
   317  				zap.Stringer("summaryID", summaryID),
   318  				zap.Uint64("height", ws.summary.Height()),
   319  				zap.Uint64("nodeWeight", nodeWeight),
   320  				zap.Uint64("previousWeight", ws.weight),
   321  				zap.Error(err),
   322  			)
   323  			newWeight = math.MaxUint64
   324  		}
   325  
   326  		ss.Ctx.Log.Verbo("updating summary weight",
   327  			zap.Stringer("nodeID", nodeID),
   328  			zap.Stringer("summaryID", summaryID),
   329  			zap.Uint64("height", ws.summary.Height()),
   330  			zap.Uint64("previousWeight", ws.weight),
   331  			zap.Uint64("newWeight", newWeight),
   332  		)
   333  		ws.weight = newWeight
   334  	}
   335  
   336  	ss.sendGetAcceptedStateSummaries(ctx)
   337  
   338  	// wait on pending responses
   339  	if ss.pendingVoters.Len() != 0 {
   340  		return nil
   341  	}
   342  
   343  	// We've received the filtered accepted frontier from every state sync validator
   344  	// Drop all summaries without a sufficient weight behind them
   345  	for summaryID, ws := range ss.weightedSummaries {
   346  		if ws.weight < ss.Alpha {
   347  			ss.Ctx.Log.Debug("removing summary",
   348  				zap.String("reason", "insufficient weight"),
   349  				zap.Stringer("summaryID", summaryID),
   350  				zap.Uint64("height", ws.summary.Height()),
   351  				zap.Uint64("currentWeight", ws.weight),
   352  				zap.Uint64("requiredWeight", ss.Alpha),
   353  			)
   354  			delete(ss.weightedSummaries, summaryID)
   355  		}
   356  	}
   357  
   358  	// if we don't have enough weight for the state summary to be accepted then retry or fail the state sync
   359  	size := len(ss.weightedSummaries)
   360  	if size == 0 {
   361  		// retry the state sync if the weight is not enough to state sync
   362  		failedVotersWeight, err := ss.StateSyncBeacons.SubsetWeight(ss.Ctx.SubnetID, ss.failedVoters)
   363  		if err != nil {
   364  			return fmt.Errorf("failed to get total weight of failed voters: %w", err)
   365  		}
   366  
   367  		// if we had too many timeouts when asking for validator votes, we should restart
   368  		// state sync hoping for the network problems to go away; otherwise, we received
   369  		// enough (>= ss.Alpha) responses, but no state summary was supported by a majority
   370  		// of validators (i.e. votes are split between minorities supporting different state
   371  		// summaries), so there is no point in retrying state sync; we should move ahead to bootstrapping
   372  		beaconsTotalWeight, err := ss.StateSyncBeacons.TotalWeight(ss.Ctx.SubnetID)
   373  		if err != nil {
   374  			return fmt.Errorf("failed to get total weight of state sync beacons for subnet %s: %w", ss.Ctx.SubnetID, err)
   375  		}
   376  		votingStakes := beaconsTotalWeight - failedVotersWeight
   377  		if votingStakes < ss.Alpha {
   378  			ss.Ctx.Log.Debug("restarting state sync",
   379  				zap.String("reason", "not enough votes received"),
   380  				zap.Int("numBeacons", ss.StateSyncBeacons.Count(ss.Ctx.SubnetID)),
   381  				zap.Int("numFailedSyncers", ss.failedVoters.Len()),
   382  			)
   383  			return ss.startup(ctx)
   384  		}
   385  
   386  		ss.Ctx.Log.Info("skipping state sync",
   387  			zap.String("reason", "no acceptable summaries found"),
   388  		)
   389  
   390  		// if we do not restart state sync, move on to bootstrapping.
   391  		return ss.onDoneStateSyncing(ctx, ss.requestID)
   392  	}
   393  
   394  	preferredStateSummary := ss.selectSyncableStateSummary()
   395  	syncMode, err := preferredStateSummary.Accept(ctx)
   396  	if err != nil {
   397  		return err
   398  	}
   399  
   400  	ss.Ctx.Log.Info("accepted state summary",
   401  		zap.Stringer("summaryID", preferredStateSummary.ID()),
   402  		zap.Stringer("syncMode", syncMode),
   403  		zap.Int("numTotalSummaries", size),
   404  	)
   405  
   406  	switch syncMode {
   407  	case block.StateSyncSkipped:
   408  		// VM did not accept the summary, move on to bootstrapping.
   409  		return ss.onDoneStateSyncing(ctx, ss.requestID)
   410  	case block.StateSyncStatic:
   411  		// Summary was accepted and VM is state syncing.
   412  		// Engine will wait for notification of state sync done.
   413  		ss.Ctx.StateSyncing.Set(true)
   414  		return nil
   415  	case block.StateSyncDynamic:
   416  		// Summary was accepted and VM is state syncing.
   417  		// Engine will continue into bootstrapping and the VM will sync in the
   418  		// background.
   419  		ss.Ctx.StateSyncing.Set(true)
   420  		return ss.onDoneStateSyncing(ctx, ss.requestID)
   421  	default:
   422  		ss.Ctx.Log.Warn("unhandled state summary mode, proceeding to bootstrap",
   423  			zap.Stringer("syncMode", syncMode),
   424  		)
   425  		return ss.onDoneStateSyncing(ctx, ss.requestID)
   426  	}
   427  }
   428  
   429  // selectSyncableStateSummary chooses a state summary from all
   430  // the network validated summaries.
   431  func (ss *stateSyncer) selectSyncableStateSummary() block.StateSummary {
   432  	var (
   433  		maxSummaryHeight      uint64
   434  		preferredStateSummary block.StateSummary
   435  	)
   436  
   437  	// by default pick highest summary, unless locallyAvailableSummary is still valid.
   438  	// In such case we pick locallyAvailableSummary to allow VM resuming state syncing.
   439  	for id, ws := range ss.weightedSummaries {
   440  		if ss.locallyAvailableSummary != nil && id == ss.locallyAvailableSummary.ID() {
   441  			return ss.locallyAvailableSummary
   442  		}
   443  
   444  		height := ws.summary.Height()
   445  		if maxSummaryHeight <= height {
   446  			maxSummaryHeight = height
   447  			preferredStateSummary = ws.summary
   448  		}
   449  	}
   450  	return preferredStateSummary
   451  }
   452  
   453  func (ss *stateSyncer) GetAcceptedStateSummaryFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error {
   454  	// ignores any late responses
   455  	if requestID != ss.requestID {
   456  		ss.Ctx.Log.Debug("received out-of-sync GetAcceptedStateSummaryFailed message",
   457  			zap.Stringer("nodeID", nodeID),
   458  			zap.Uint32("expectedRequestID", ss.requestID),
   459  			zap.Uint32("requestID", requestID),
   460  		)
   461  		return nil
   462  	}
   463  
   464  	// If we can't get a response from [nodeID], act as though they said that
   465  	// they think none of the containers we sent them in GetAccepted are
   466  	// accepted
   467  	ss.failedVoters.Add(nodeID)
   468  
   469  	return ss.AcceptedStateSummary(ctx, nodeID, requestID, nil)
   470  }
   471  
   472  // startup do start the whole state sync process by
   473  // sampling frontier seeders, listing state syncers to request votes to
   474  // and reaching out frontier seeders if any. Otherwise, it moves immediately
   475  // to bootstrapping. Unlike Start, startup does not check
   476  // whether sufficient stake amount is connected.
   477  func (ss *stateSyncer) startup(ctx context.Context) error {
   478  	ss.Config.Ctx.Log.Info("starting state sync")
   479  
   480  	// clear up messages trackers
   481  	ss.weightedSummaries = make(map[ids.ID]*weightedSummary)
   482  	ss.summariesHeights.Clear()
   483  	ss.uniqueSummariesHeights = nil
   484  
   485  	ss.targetSeeders.Clear()
   486  	ss.pendingSeeders.Clear()
   487  	ss.failedSeeders.Clear()
   488  	ss.targetVoters.Clear()
   489  	ss.pendingVoters.Clear()
   490  	ss.failedVoters.Clear()
   491  
   492  	// sample K beacons to retrieve frontier from
   493  	beaconIDs, err := ss.StateSyncBeacons.Sample(ss.Ctx.SubnetID, ss.Config.SampleK)
   494  	if err != nil {
   495  		return err
   496  	}
   497  
   498  	ss.frontierSeeders = validators.NewManager()
   499  	for _, nodeID := range beaconIDs {
   500  		if _, ok := ss.frontierSeeders.GetValidator(ss.Ctx.SubnetID, nodeID); !ok {
   501  			// Invariant: We never use the TxID or BLS keys populated here.
   502  			err = ss.frontierSeeders.AddStaker(ss.Ctx.SubnetID, nodeID, nil, ids.Empty, 1)
   503  		} else {
   504  			err = ss.frontierSeeders.AddWeight(ss.Ctx.SubnetID, nodeID, 1)
   505  		}
   506  		if err != nil {
   507  			return err
   508  		}
   509  		ss.targetSeeders.Add(nodeID)
   510  	}
   511  
   512  	// list all beacons, to reach them for voting on frontier
   513  	ss.targetVoters.Add(ss.StateSyncBeacons.GetValidatorIDs(ss.Ctx.SubnetID)...)
   514  
   515  	// check if there is an ongoing state sync; if so add its state summary
   516  	// to the frontier to request votes on
   517  	// Note: database.ErrNotFound means there is no ongoing summary
   518  	localSummary, err := ss.stateSyncVM.GetOngoingSyncStateSummary(ctx)
   519  	switch err {
   520  	case database.ErrNotFound:
   521  		// no action needed
   522  	case nil:
   523  		ss.locallyAvailableSummary = localSummary
   524  		ss.weightedSummaries[localSummary.ID()] = &weightedSummary{
   525  			summary: localSummary,
   526  		}
   527  
   528  		height := localSummary.Height()
   529  		ss.summariesHeights.Add(height)
   530  		ss.uniqueSummariesHeights = append(ss.uniqueSummariesHeights, height)
   531  	default:
   532  		return err
   533  	}
   534  
   535  	// initiate messages exchange
   536  	if ss.targetSeeders.Len() == 0 {
   537  		ss.Ctx.Log.Info("State syncing skipped due to no provided syncers")
   538  		return ss.onDoneStateSyncing(ctx, ss.requestID)
   539  	}
   540  
   541  	ss.requestID++
   542  	ss.sendGetStateSummaryFrontiers(ctx)
   543  	return nil
   544  }
   545  
   546  // Ask up to [common.MaxOutstandingBroadcastRequests] state sync validators at a time
   547  // to send their accepted state summary. It is called again until there are
   548  // no more seeders to be reached in the pending set
   549  func (ss *stateSyncer) sendGetStateSummaryFrontiers(ctx context.Context) {
   550  	vdrs := set.NewSet[ids.NodeID](1)
   551  	for ss.targetSeeders.Len() > 0 && ss.pendingSeeders.Len() < maxOutstandingBroadcastRequests {
   552  		vdr, _ := ss.targetSeeders.Pop()
   553  		vdrs.Add(vdr)
   554  		ss.pendingSeeders.Add(vdr)
   555  	}
   556  
   557  	if vdrs.Len() > 0 {
   558  		ss.Sender.SendGetStateSummaryFrontier(ctx, vdrs, ss.requestID)
   559  	}
   560  }
   561  
   562  // Ask up to [common.MaxOutstandingStateSyncRequests] syncers validators to send
   563  // their filtered accepted frontier. It is called again until there are
   564  // no more voters to be reached in the pending set.
   565  func (ss *stateSyncer) sendGetAcceptedStateSummaries(ctx context.Context) {
   566  	vdrs := set.NewSet[ids.NodeID](1)
   567  	for ss.targetVoters.Len() > 0 && ss.pendingVoters.Len() < maxOutstandingBroadcastRequests {
   568  		vdr, _ := ss.targetVoters.Pop()
   569  		vdrs.Add(vdr)
   570  		ss.pendingVoters.Add(vdr)
   571  	}
   572  
   573  	if len(vdrs) > 0 {
   574  		ss.Sender.SendGetAcceptedStateSummary(ctx, vdrs, ss.requestID, ss.uniqueSummariesHeights)
   575  		ss.Ctx.Log.Debug("sent GetAcceptedStateSummary messages",
   576  			zap.Int("numSent", vdrs.Len()),
   577  			zap.Int("numPending", ss.targetVoters.Len()),
   578  		)
   579  	}
   580  }
   581  
   582  func (ss *stateSyncer) Notify(ctx context.Context, msg common.Message) error {
   583  	if msg != common.StateSyncDone {
   584  		ss.Ctx.Log.Warn("received an unexpected message from the VM",
   585  			zap.Stringer("msg", msg),
   586  		)
   587  		return nil
   588  	}
   589  
   590  	ss.Ctx.StateSyncing.Set(false)
   591  	return ss.onDoneStateSyncing(ctx, ss.requestID)
   592  }
   593  
   594  func (*stateSyncer) Gossip(context.Context) error {
   595  	return nil
   596  }
   597  
   598  func (ss *stateSyncer) Shutdown(ctx context.Context) error {
   599  	ss.Config.Ctx.Log.Info("shutting down state syncer")
   600  
   601  	ss.Ctx.Lock.Lock()
   602  	defer ss.Ctx.Lock.Unlock()
   603  
   604  	return ss.VM.Shutdown(ctx)
   605  }
   606  
   607  func (*stateSyncer) Halt(context.Context) {}
   608  
   609  func (*stateSyncer) Timeout(context.Context) error {
   610  	return nil
   611  }
   612  
   613  func (ss *stateSyncer) HealthCheck(ctx context.Context) (interface{}, error) {
   614  	ss.Ctx.Lock.Lock()
   615  	defer ss.Ctx.Lock.Unlock()
   616  
   617  	vmIntf, vmErr := ss.VM.HealthCheck(ctx)
   618  	intf := map[string]interface{}{
   619  		"consensus": struct{}{},
   620  		"vm":        vmIntf,
   621  	}
   622  	return intf, vmErr
   623  }
   624  
   625  func (ss *stateSyncer) IsEnabled(ctx context.Context) (bool, error) {
   626  	if ss.stateSyncVM == nil {
   627  		// state sync is not implemented
   628  		return false, nil
   629  	}
   630  
   631  	ss.Ctx.Lock.Lock()
   632  	defer ss.Ctx.Lock.Unlock()
   633  
   634  	return ss.stateSyncVM.StateSyncEnabled(ctx)
   635  }