github.com/ava-labs/avalanchego@v1.11.11/vms/proposervm/proposer/windower.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package proposer
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"math/bits"
    10  	"time"
    11  
    12  	"gonum.org/v1/gonum/mathext/prng"
    13  
    14  	"github.com/ava-labs/avalanchego/ids"
    15  	"github.com/ava-labs/avalanchego/snow/validators"
    16  	"github.com/ava-labs/avalanchego/utils"
    17  	"github.com/ava-labs/avalanchego/utils/math"
    18  	"github.com/ava-labs/avalanchego/utils/sampler"
    19  	"github.com/ava-labs/avalanchego/utils/wrappers"
    20  )
    21  
    22  // Proposer list constants
    23  const (
    24  	WindowDuration = 5 * time.Second
    25  
    26  	MaxVerifyWindows = 6
    27  	MaxVerifyDelay   = MaxVerifyWindows * WindowDuration // 30 seconds
    28  
    29  	MaxBuildWindows = 60
    30  	MaxBuildDelay   = MaxBuildWindows * WindowDuration // 5 minutes
    31  
    32  	MaxLookAheadSlots  = 720
    33  	MaxLookAheadWindow = MaxLookAheadSlots * WindowDuration // 1 hour
    34  )
    35  
    36  var (
    37  	_ Windower = (*windower)(nil)
    38  
    39  	ErrAnyoneCanPropose         = errors.New("anyone can propose")
    40  	ErrUnexpectedSamplerFailure = errors.New("unexpected sampler failure")
    41  )
    42  
    43  type Windower interface {
    44  	// Proposers returns the proposer list for building a block at [blockHeight]
    45  	// when the validator set is defined at [pChainHeight]. The list is returned
    46  	// in order. The minimum delay of a validator is the index they appear times
    47  	// [WindowDuration].
    48  	Proposers(
    49  		ctx context.Context,
    50  		blockHeight,
    51  		pChainHeight uint64,
    52  		maxWindows int,
    53  	) ([]ids.NodeID, error)
    54  
    55  	// Delay returns the amount of time that [validatorID] must wait before
    56  	// building a block at [blockHeight] when the validator set is defined at
    57  	// [pChainHeight].
    58  	Delay(
    59  		ctx context.Context,
    60  		blockHeight,
    61  		pChainHeight uint64,
    62  		validatorID ids.NodeID,
    63  		maxWindows int,
    64  	) (time.Duration, error)
    65  
    66  	// In the Post-Durango windowing scheme, every validator active at
    67  	// [pChainHeight] gets specific slots it can propose in (instead of being
    68  	// able to propose from a given time on as it happens Pre-Durango).
    69  	// [ExpectedProposer] calculates which nodeID is scheduled to propose a
    70  	// block of height [blockHeight] at [slot].
    71  	// If no validators are currently available, [ErrAnyoneCanPropose] is
    72  	// returned.
    73  	ExpectedProposer(
    74  		ctx context.Context,
    75  		blockHeight,
    76  		pChainHeight,
    77  		slot uint64,
    78  	) (ids.NodeID, error)
    79  
    80  	// In the Post-Durango windowing scheme, every validator active at
    81  	// [pChainHeight] gets specific slots it can propose in (instead of being
    82  	// able to propose from a given time on as it happens Pre-Durango).
    83  	// [MinDelayForProposer] specifies how long [nodeID] needs to wait for its
    84  	// slot to start. Delay is specified as starting from slot zero start.
    85  	// (which is parent timestamp). For efficiency reasons, we cap the slot
    86  	// search to [MaxLookAheadSlots].
    87  	// If no validators are currently available, [ErrAnyoneCanPropose] is
    88  	// returned.
    89  	MinDelayForProposer(
    90  		ctx context.Context,
    91  		blockHeight,
    92  		pChainHeight uint64,
    93  		nodeID ids.NodeID,
    94  		startSlot uint64,
    95  	) (time.Duration, error)
    96  }
    97  
    98  // windower interfaces with P-Chain and it is responsible for calculating the
    99  // delay for the block submission window of a given validator
   100  type windower struct {
   101  	state       validators.State
   102  	subnetID    ids.ID
   103  	chainSource uint64
   104  }
   105  
   106  func New(state validators.State, subnetID, chainID ids.ID) Windower {
   107  	w := wrappers.Packer{Bytes: chainID[:]}
   108  	return &windower{
   109  		state:       state,
   110  		subnetID:    subnetID,
   111  		chainSource: w.UnpackLong(),
   112  	}
   113  }
   114  
   115  func (w *windower) Proposers(ctx context.Context, blockHeight, pChainHeight uint64, maxWindows int) ([]ids.NodeID, error) {
   116  	// Note: The 32-bit prng is used here for legacy reasons. All other usages
   117  	// of a prng in this file should use the 64-bit version.
   118  	source := prng.NewMT19937()
   119  	sampler, validators, err := w.makeSampler(ctx, pChainHeight, source)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	var totalWeight uint64
   125  	for _, validator := range validators {
   126  		totalWeight, err = math.Add(totalWeight, validator.weight)
   127  		if err != nil {
   128  			return nil, err
   129  		}
   130  	}
   131  
   132  	source.Seed(w.chainSource ^ blockHeight)
   133  
   134  	numToSample := int(min(uint64(maxWindows), totalWeight))
   135  	indices, ok := sampler.Sample(numToSample)
   136  	if !ok {
   137  		return nil, ErrUnexpectedSamplerFailure
   138  	}
   139  
   140  	nodeIDs := make([]ids.NodeID, numToSample)
   141  	for i, index := range indices {
   142  		nodeIDs[i] = validators[index].id
   143  	}
   144  	return nodeIDs, nil
   145  }
   146  
   147  func (w *windower) Delay(ctx context.Context, blockHeight, pChainHeight uint64, validatorID ids.NodeID, maxWindows int) (time.Duration, error) {
   148  	if validatorID == ids.EmptyNodeID {
   149  		return time.Duration(maxWindows) * WindowDuration, nil
   150  	}
   151  
   152  	proposers, err := w.Proposers(ctx, blockHeight, pChainHeight, maxWindows)
   153  	if err != nil {
   154  		return 0, err
   155  	}
   156  
   157  	delay := time.Duration(0)
   158  	for _, nodeID := range proposers {
   159  		if nodeID == validatorID {
   160  			return delay, nil
   161  		}
   162  		delay += WindowDuration
   163  	}
   164  	return delay, nil
   165  }
   166  
   167  func (w *windower) ExpectedProposer(
   168  	ctx context.Context,
   169  	blockHeight,
   170  	pChainHeight,
   171  	slot uint64,
   172  ) (ids.NodeID, error) {
   173  	source := prng.NewMT19937_64()
   174  	sampler, validators, err := w.makeSampler(ctx, pChainHeight, source)
   175  	if err != nil {
   176  		return ids.EmptyNodeID, err
   177  	}
   178  	if len(validators) == 0 {
   179  		return ids.EmptyNodeID, ErrAnyoneCanPropose
   180  	}
   181  
   182  	return w.expectedProposer(
   183  		validators,
   184  		source,
   185  		sampler,
   186  		blockHeight,
   187  		slot,
   188  	)
   189  }
   190  
   191  func (w *windower) MinDelayForProposer(
   192  	ctx context.Context,
   193  	blockHeight,
   194  	pChainHeight uint64,
   195  	nodeID ids.NodeID,
   196  	startSlot uint64,
   197  ) (time.Duration, error) {
   198  	source := prng.NewMT19937_64()
   199  	sampler, validators, err := w.makeSampler(ctx, pChainHeight, source)
   200  	if err != nil {
   201  		return 0, err
   202  	}
   203  	if len(validators) == 0 {
   204  		return 0, ErrAnyoneCanPropose
   205  	}
   206  
   207  	maxSlot := startSlot + MaxLookAheadSlots
   208  	for slot := startSlot; slot < maxSlot; slot++ {
   209  		expectedNodeID, err := w.expectedProposer(
   210  			validators,
   211  			source,
   212  			sampler,
   213  			blockHeight,
   214  			slot,
   215  		)
   216  		if err != nil {
   217  			return 0, err
   218  		}
   219  
   220  		if expectedNodeID == nodeID {
   221  			return time.Duration(slot) * WindowDuration, nil
   222  		}
   223  	}
   224  
   225  	// no slots scheduled for the max window we inspect. Return max delay
   226  	return time.Duration(maxSlot) * WindowDuration, nil
   227  }
   228  
   229  func (w *windower) makeSampler(
   230  	ctx context.Context,
   231  	pChainHeight uint64,
   232  	source sampler.Source,
   233  ) (sampler.WeightedWithoutReplacement, []validatorData, error) {
   234  	// Get the canonical representation of the validator set at the provided
   235  	// p-chain height.
   236  	validatorsMap, err := w.state.GetValidatorSet(ctx, pChainHeight, w.subnetID)
   237  	if err != nil {
   238  		return nil, nil, err
   239  	}
   240  
   241  	validators := make([]validatorData, 0, len(validatorsMap))
   242  	for k, v := range validatorsMap {
   243  		validators = append(validators, validatorData{
   244  			id:     k,
   245  			weight: v.Weight,
   246  		})
   247  	}
   248  
   249  	// Note: validators are sorted by ID. Sorting by weight would not create a
   250  	// canonically sorted list.
   251  	utils.Sort(validators)
   252  
   253  	weights := make([]uint64, len(validators))
   254  	for i, validator := range validators {
   255  		weights[i] = validator.weight
   256  	}
   257  
   258  	sampler := sampler.NewDeterministicWeightedWithoutReplacement(source)
   259  	return sampler, validators, sampler.Initialize(weights)
   260  }
   261  
   262  func (w *windower) expectedProposer(
   263  	validators []validatorData,
   264  	source *prng.MT19937_64,
   265  	sampler sampler.WeightedWithoutReplacement,
   266  	blockHeight,
   267  	slot uint64,
   268  ) (ids.NodeID, error) {
   269  	// Slot is reversed to utilize a different state space in the seed than the
   270  	// height. If the slot was not reversed the state space would collide;
   271  	// biasing the seed generation. For example, without reversing the slot
   272  	// height=0 and slot=1 would equal height=1 and slot=0.
   273  	source.Seed(w.chainSource ^ blockHeight ^ bits.Reverse64(slot))
   274  	indices, ok := sampler.Sample(1)
   275  	if !ok {
   276  		return ids.EmptyNodeID, ErrUnexpectedSamplerFailure
   277  	}
   278  	return validators[indices[0]].id, nil
   279  }
   280  
   281  func TimeToSlot(start, now time.Time) uint64 {
   282  	if now.Before(start) {
   283  		return 0
   284  	}
   285  	return uint64(now.Sub(start) / WindowDuration)
   286  }