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