github.com/ava-labs/avalanchego@v1.11.11/vms/proposervm/proposer/windower_test.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  	"math"
     9  	"math/rand"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"github.com/ava-labs/avalanchego/ids"
    16  	"github.com/ava-labs/avalanchego/snow/validators"
    17  	"github.com/ava-labs/avalanchego/snow/validators/validatorstest"
    18  
    19  	safemath "github.com/ava-labs/avalanchego/utils/math"
    20  )
    21  
    22  var (
    23  	subnetID      = ids.GenerateTestID()
    24  	randomChainID = ids.GenerateTestID()
    25  	fixedChainID  = ids.ID{0, 2}
    26  )
    27  
    28  func TestWindowerNoValidators(t *testing.T) {
    29  	require := require.New(t)
    30  
    31  	_, vdrState := makeValidators(t, 0)
    32  	w := New(vdrState, subnetID, randomChainID)
    33  
    34  	var (
    35  		chainHeight  uint64 = 1
    36  		pChainHeight uint64 = 0
    37  		nodeID              = ids.GenerateTestNodeID()
    38  		slot         uint64 = 1
    39  	)
    40  	delay, err := w.Delay(context.Background(), chainHeight, pChainHeight, nodeID, MaxVerifyWindows)
    41  	require.NoError(err)
    42  	require.Zero(delay)
    43  
    44  	proposer, err := w.ExpectedProposer(context.Background(), chainHeight, pChainHeight, slot)
    45  	require.ErrorIs(err, ErrAnyoneCanPropose)
    46  	require.Equal(ids.EmptyNodeID, proposer)
    47  
    48  	delay, err = w.MinDelayForProposer(context.Background(), chainHeight, pChainHeight, nodeID, slot)
    49  	require.ErrorIs(err, ErrAnyoneCanPropose)
    50  	require.Zero(delay)
    51  }
    52  
    53  func TestWindowerRepeatedValidator(t *testing.T) {
    54  	require := require.New(t)
    55  
    56  	var (
    57  		validatorID    = ids.GenerateTestNodeID()
    58  		nonValidatorID = ids.GenerateTestNodeID()
    59  	)
    60  
    61  	vdrState := &validatorstest.State{
    62  		T: t,
    63  		GetValidatorSetF: func(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
    64  			return map[ids.NodeID]*validators.GetValidatorOutput{
    65  				validatorID: {
    66  					NodeID: validatorID,
    67  					Weight: 10,
    68  				},
    69  			}, nil
    70  		},
    71  	}
    72  
    73  	w := New(vdrState, subnetID, randomChainID)
    74  
    75  	validatorDelay, err := w.Delay(context.Background(), 1, 0, validatorID, MaxVerifyWindows)
    76  	require.NoError(err)
    77  	require.Zero(validatorDelay)
    78  
    79  	nonValidatorDelay, err := w.Delay(context.Background(), 1, 0, nonValidatorID, MaxVerifyWindows)
    80  	require.NoError(err)
    81  	require.Equal(MaxVerifyDelay, nonValidatorDelay)
    82  }
    83  
    84  func TestDelayChangeByHeight(t *testing.T) {
    85  	require := require.New(t)
    86  
    87  	validatorIDs, vdrState := makeValidators(t, MaxVerifyWindows)
    88  	w := New(vdrState, subnetID, fixedChainID)
    89  
    90  	expectedDelays1 := []time.Duration{
    91  		2 * WindowDuration,
    92  		5 * WindowDuration,
    93  		3 * WindowDuration,
    94  		4 * WindowDuration,
    95  		0 * WindowDuration,
    96  		1 * WindowDuration,
    97  	}
    98  	for i, expectedDelay := range expectedDelays1 {
    99  		vdrID := validatorIDs[i]
   100  		validatorDelay, err := w.Delay(context.Background(), 1, 0, vdrID, MaxVerifyWindows)
   101  		require.NoError(err)
   102  		require.Equal(expectedDelay, validatorDelay)
   103  	}
   104  
   105  	expectedDelays2 := []time.Duration{
   106  		5 * WindowDuration,
   107  		1 * WindowDuration,
   108  		3 * WindowDuration,
   109  		4 * WindowDuration,
   110  		0 * WindowDuration,
   111  		2 * WindowDuration,
   112  	}
   113  	for i, expectedDelay := range expectedDelays2 {
   114  		vdrID := validatorIDs[i]
   115  		validatorDelay, err := w.Delay(context.Background(), 2, 0, vdrID, MaxVerifyWindows)
   116  		require.NoError(err)
   117  		require.Equal(expectedDelay, validatorDelay)
   118  	}
   119  }
   120  
   121  func TestDelayChangeByChain(t *testing.T) {
   122  	require := require.New(t)
   123  
   124  	source := rand.NewSource(int64(0))
   125  	rng := rand.New(source) // #nosec G404
   126  
   127  	chainID0 := ids.Empty
   128  	_, err := rng.Read(chainID0[:])
   129  	require.NoError(err)
   130  
   131  	chainID1 := ids.Empty
   132  	_, err = rng.Read(chainID1[:])
   133  	require.NoError(err)
   134  
   135  	validatorIDs, vdrState := makeValidators(t, MaxVerifyWindows)
   136  	w0 := New(vdrState, subnetID, chainID0)
   137  	w1 := New(vdrState, subnetID, chainID1)
   138  
   139  	expectedDelays0 := []time.Duration{
   140  		5 * WindowDuration,
   141  		2 * WindowDuration,
   142  		0 * WindowDuration,
   143  		3 * WindowDuration,
   144  		1 * WindowDuration,
   145  		4 * WindowDuration,
   146  	}
   147  	for i, expectedDelay := range expectedDelays0 {
   148  		vdrID := validatorIDs[i]
   149  		validatorDelay, err := w0.Delay(context.Background(), 1, 0, vdrID, MaxVerifyWindows)
   150  		require.NoError(err)
   151  		require.Equal(expectedDelay, validatorDelay)
   152  	}
   153  
   154  	expectedDelays1 := []time.Duration{
   155  		0 * WindowDuration,
   156  		1 * WindowDuration,
   157  		4 * WindowDuration,
   158  		5 * WindowDuration,
   159  		3 * WindowDuration,
   160  		2 * WindowDuration,
   161  	}
   162  	for i, expectedDelay := range expectedDelays1 {
   163  		vdrID := validatorIDs[i]
   164  		validatorDelay, err := w1.Delay(context.Background(), 1, 0, vdrID, MaxVerifyWindows)
   165  		require.NoError(err)
   166  		require.Equal(expectedDelay, validatorDelay)
   167  	}
   168  }
   169  
   170  func TestExpectedProposerChangeByHeight(t *testing.T) {
   171  	require := require.New(t)
   172  
   173  	validatorIDs, vdrState := makeValidators(t, 10)
   174  	w := New(vdrState, subnetID, fixedChainID)
   175  
   176  	var (
   177  		dummyCtx            = context.Background()
   178  		pChainHeight uint64 = 0
   179  		slot         uint64 = 0
   180  	)
   181  
   182  	expectedProposers := map[uint64]ids.NodeID{
   183  		1: validatorIDs[2],
   184  		2: validatorIDs[1],
   185  	}
   186  
   187  	for chainHeight, expectedProposerID := range expectedProposers {
   188  		proposerID, err := w.ExpectedProposer(dummyCtx, chainHeight, pChainHeight, slot)
   189  		require.NoError(err)
   190  		require.Equal(expectedProposerID, proposerID)
   191  	}
   192  }
   193  
   194  func TestExpectedProposerChangeByChain(t *testing.T) {
   195  	require := require.New(t)
   196  
   197  	source := rand.NewSource(int64(0))
   198  	rng := rand.New(source) // #nosec G404
   199  
   200  	chainID0 := ids.Empty
   201  	_, err := rng.Read(chainID0[:])
   202  	require.NoError(err)
   203  
   204  	chainID1 := ids.Empty
   205  	_, err = rng.Read(chainID1[:])
   206  	require.NoError(err)
   207  
   208  	validatorIDs, vdrState := makeValidators(t, 10)
   209  
   210  	var (
   211  		dummyCtx            = context.Background()
   212  		chainHeight  uint64 = 1
   213  		pChainHeight uint64 = 0
   214  		slot         uint64 = 0
   215  	)
   216  
   217  	expectedProposers := map[ids.ID]ids.NodeID{
   218  		chainID0: validatorIDs[5],
   219  		chainID1: validatorIDs[3],
   220  	}
   221  
   222  	for chainID, expectedProposerID := range expectedProposers {
   223  		w := New(vdrState, subnetID, chainID)
   224  		proposerID, err := w.ExpectedProposer(dummyCtx, chainHeight, pChainHeight, slot)
   225  		require.NoError(err)
   226  		require.Equal(expectedProposerID, proposerID)
   227  	}
   228  }
   229  
   230  func TestExpectedProposerChangeBySlot(t *testing.T) {
   231  	require := require.New(t)
   232  
   233  	validatorIDs, vdrState := makeValidators(t, 10)
   234  	w := New(vdrState, subnetID, fixedChainID)
   235  
   236  	var (
   237  		dummyCtx            = context.Background()
   238  		chainHeight  uint64 = 1
   239  		pChainHeight uint64 = 0
   240  	)
   241  
   242  	proposers := []ids.NodeID{
   243  		validatorIDs[2],
   244  		validatorIDs[0],
   245  		validatorIDs[9],
   246  		validatorIDs[7],
   247  		validatorIDs[0],
   248  		validatorIDs[3],
   249  		validatorIDs[3],
   250  		validatorIDs[3],
   251  		validatorIDs[3],
   252  		validatorIDs[3],
   253  		validatorIDs[4],
   254  		validatorIDs[0],
   255  		validatorIDs[6],
   256  		validatorIDs[3],
   257  		validatorIDs[2],
   258  		validatorIDs[1],
   259  		validatorIDs[6],
   260  		validatorIDs[0],
   261  		validatorIDs[5],
   262  		validatorIDs[1],
   263  		validatorIDs[9],
   264  		validatorIDs[6],
   265  		validatorIDs[0],
   266  		validatorIDs[8],
   267  	}
   268  	expectedProposers := map[uint64]ids.NodeID{
   269  		MaxLookAheadSlots:     validatorIDs[4],
   270  		MaxLookAheadSlots + 1: validatorIDs[6],
   271  	}
   272  	for slot, expectedProposerID := range proposers {
   273  		expectedProposers[uint64(slot)] = expectedProposerID
   274  	}
   275  
   276  	for slot, expectedProposerID := range expectedProposers {
   277  		actualProposerID, err := w.ExpectedProposer(dummyCtx, chainHeight, pChainHeight, slot)
   278  		require.NoError(err)
   279  		require.Equal(expectedProposerID, actualProposerID)
   280  	}
   281  }
   282  
   283  func TestCoherenceOfExpectedProposerAndMinDelayForProposer(t *testing.T) {
   284  	require := require.New(t)
   285  
   286  	_, vdrState := makeValidators(t, 10)
   287  	w := New(vdrState, subnetID, fixedChainID)
   288  
   289  	var (
   290  		dummyCtx            = context.Background()
   291  		chainHeight  uint64 = 1
   292  		pChainHeight uint64 = 0
   293  	)
   294  
   295  	for slot := uint64(0); slot < 3*MaxLookAheadSlots; slot++ {
   296  		proposerID, err := w.ExpectedProposer(dummyCtx, chainHeight, pChainHeight, slot)
   297  		require.NoError(err)
   298  
   299  		// proposerID is the scheduled proposer. It should start with the
   300  		// expected delay
   301  		delay, err := w.MinDelayForProposer(dummyCtx, chainHeight, pChainHeight, proposerID, slot)
   302  		require.NoError(err)
   303  		require.Equal(time.Duration(slot)*WindowDuration, delay)
   304  	}
   305  }
   306  
   307  func TestMinDelayForProposer(t *testing.T) {
   308  	require := require.New(t)
   309  
   310  	validatorIDs, vdrState := makeValidators(t, 10)
   311  	w := New(vdrState, subnetID, fixedChainID)
   312  
   313  	var (
   314  		dummyCtx            = context.Background()
   315  		chainHeight  uint64 = 1
   316  		pChainHeight uint64 = 0
   317  		slot         uint64 = 0
   318  	)
   319  
   320  	expectedDelays := map[ids.NodeID]time.Duration{
   321  		validatorIDs[0]:          1 * WindowDuration,
   322  		validatorIDs[1]:          15 * WindowDuration,
   323  		validatorIDs[2]:          0 * WindowDuration,
   324  		validatorIDs[3]:          5 * WindowDuration,
   325  		validatorIDs[4]:          10 * WindowDuration,
   326  		validatorIDs[5]:          18 * WindowDuration,
   327  		validatorIDs[6]:          12 * WindowDuration,
   328  		validatorIDs[7]:          3 * WindowDuration,
   329  		validatorIDs[8]:          23 * WindowDuration,
   330  		validatorIDs[9]:          2 * WindowDuration,
   331  		ids.GenerateTestNodeID(): MaxLookAheadWindow,
   332  	}
   333  
   334  	for nodeID, expectedDelay := range expectedDelays {
   335  		delay, err := w.MinDelayForProposer(dummyCtx, chainHeight, pChainHeight, nodeID, slot)
   336  		require.NoError(err)
   337  		require.Equal(expectedDelay, delay)
   338  	}
   339  }
   340  
   341  func BenchmarkMinDelayForProposer(b *testing.B) {
   342  	require := require.New(b)
   343  
   344  	_, vdrState := makeValidators(b, 10)
   345  	w := New(vdrState, subnetID, fixedChainID)
   346  
   347  	var (
   348  		dummyCtx            = context.Background()
   349  		pChainHeight uint64 = 0
   350  		chainHeight  uint64 = 1
   351  		nodeID              = ids.GenerateTestNodeID() // Ensure to exhaust the search
   352  		slot         uint64 = 0
   353  	)
   354  
   355  	b.ResetTimer()
   356  	for i := 0; i < b.N; i++ {
   357  		_, err := w.MinDelayForProposer(dummyCtx, chainHeight, pChainHeight, nodeID, slot)
   358  		require.NoError(err)
   359  	}
   360  }
   361  
   362  func TestTimeToSlot(t *testing.T) {
   363  	parentTime := time.Now()
   364  	tests := []struct {
   365  		timeOffset   time.Duration
   366  		expectedSlot uint64
   367  	}{
   368  		{
   369  			timeOffset:   -WindowDuration,
   370  			expectedSlot: 0,
   371  		},
   372  		{
   373  			timeOffset:   -time.Second,
   374  			expectedSlot: 0,
   375  		},
   376  		{
   377  			timeOffset:   0,
   378  			expectedSlot: 0,
   379  		},
   380  		{
   381  			timeOffset:   WindowDuration,
   382  			expectedSlot: 1,
   383  		},
   384  		{
   385  			timeOffset:   2 * WindowDuration,
   386  			expectedSlot: 2,
   387  		},
   388  	}
   389  	for _, test := range tests {
   390  		t.Run(test.timeOffset.String(), func(t *testing.T) {
   391  			slot := TimeToSlot(parentTime, parentTime.Add(test.timeOffset))
   392  			require.Equal(t, test.expectedSlot, slot)
   393  		})
   394  	}
   395  }
   396  
   397  // Ensure that the proposer distribution is within 3 standard deviations of the
   398  // expected value assuming a truly random binomial distribution.
   399  func TestProposerDistribution(t *testing.T) {
   400  	require := require.New(t)
   401  
   402  	validatorIDs, vdrState := makeValidators(t, 10)
   403  	w := New(vdrState, subnetID, fixedChainID)
   404  
   405  	var (
   406  		dummyCtx               = context.Background()
   407  		pChainHeight    uint64 = 0
   408  		numChainHeights uint64 = 100
   409  		numSlots        uint64 = 100
   410  	)
   411  
   412  	proposerFrequency := make(map[ids.NodeID]int)
   413  	for _, validatorID := range validatorIDs {
   414  		// Initialize the map to 0s to include validators that are never sampled
   415  		// in the analysis.
   416  		proposerFrequency[validatorID] = 0
   417  	}
   418  	for chainHeight := uint64(0); chainHeight < numChainHeights; chainHeight++ {
   419  		for slot := uint64(0); slot < numSlots; slot++ {
   420  			proposerID, err := w.ExpectedProposer(dummyCtx, chainHeight, pChainHeight, slot)
   421  			require.NoError(err)
   422  			proposerFrequency[proposerID]++
   423  		}
   424  	}
   425  
   426  	var (
   427  		totalNumberOfSamples      = numChainHeights * numSlots
   428  		probabilityOfBeingSampled = 1 / float64(len(validatorIDs))
   429  		expectedNumberOfSamples   = uint64(probabilityOfBeingSampled * float64(totalNumberOfSamples))
   430  		variance                  = float64(totalNumberOfSamples) * probabilityOfBeingSampled * (1 - probabilityOfBeingSampled)
   431  		stdDeviation              = math.Sqrt(variance)
   432  		maxDeviation              uint64
   433  	)
   434  	for _, sampled := range proposerFrequency {
   435  		maxDeviation = max(
   436  			maxDeviation,
   437  			safemath.AbsDiff(
   438  				uint64(sampled),
   439  				expectedNumberOfSamples,
   440  			),
   441  		)
   442  	}
   443  
   444  	maxSTDDeviation := float64(maxDeviation) / stdDeviation
   445  	require.Less(maxSTDDeviation, 3.)
   446  }
   447  
   448  func makeValidators(t testing.TB, count int) ([]ids.NodeID, *validatorstest.State) {
   449  	validatorIDs := make([]ids.NodeID, count)
   450  	for i := range validatorIDs {
   451  		validatorIDs[i] = ids.BuildTestNodeID([]byte{byte(i) + 1})
   452  	}
   453  
   454  	vdrState := &validatorstest.State{
   455  		T: t,
   456  		GetValidatorSetF: func(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
   457  			vdrs := make(map[ids.NodeID]*validators.GetValidatorOutput, MaxVerifyWindows)
   458  			for _, id := range validatorIDs {
   459  				vdrs[id] = &validators.GetValidatorOutput{
   460  					NodeID: id,
   461  					Weight: 1,
   462  				}
   463  			}
   464  			return vdrs, nil
   465  		},
   466  	}
   467  	return validatorIDs, vdrState
   468  }