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 }