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 }