github.com/MetalBlockchain/metalgo@v1.11.9/snow/networking/benchlist/benchlist_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 benchlist 5 6 import ( 7 "testing" 8 "time" 9 10 "github.com/prometheus/client_golang/prometheus" 11 "github.com/stretchr/testify/require" 12 13 "github.com/MetalBlockchain/metalgo/ids" 14 "github.com/MetalBlockchain/metalgo/snow/snowtest" 15 "github.com/MetalBlockchain/metalgo/snow/validators" 16 ) 17 18 var minimumFailingDuration = 5 * time.Minute 19 20 // Test that validators are properly added to the bench 21 func TestBenchlistAdd(t *testing.T) { 22 require := require.New(t) 23 24 snowCtx := snowtest.Context(t, snowtest.CChainID) 25 ctx := snowtest.ConsensusContext(snowCtx) 26 vdrs := validators.NewManager() 27 vdrID0 := ids.GenerateTestNodeID() 28 vdrID1 := ids.GenerateTestNodeID() 29 vdrID2 := ids.GenerateTestNodeID() 30 vdrID3 := ids.GenerateTestNodeID() 31 vdrID4 := ids.GenerateTestNodeID() 32 33 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID0, nil, ids.Empty, 50)) 34 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID1, nil, ids.Empty, 50)) 35 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID2, nil, ids.Empty, 50)) 36 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID3, nil, ids.Empty, 50)) 37 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID4, nil, ids.Empty, 50)) 38 39 benchable := &TestBenchable{T: t} 40 benchable.Default(true) 41 42 threshold := 3 43 duration := time.Minute 44 maxPortion := 0.5 45 benchIntf, err := NewBenchlist( 46 ctx, 47 benchable, 48 vdrs, 49 threshold, 50 minimumFailingDuration, 51 duration, 52 maxPortion, 53 prometheus.NewRegistry(), 54 ) 55 require.NoError(err) 56 b := benchIntf.(*benchlist) 57 now := time.Now() 58 b.clock.Set(now) 59 60 // Nobody should be benched at the start 61 b.lock.Lock() 62 require.Empty(b.benchlistSet) 63 require.Empty(b.failureStreaks) 64 require.Zero(b.benchedHeap.Len()) 65 b.lock.Unlock() 66 67 // Register [threshold - 1] failures in a row for vdr0 68 for i := 0; i < threshold-1; i++ { 69 b.RegisterFailure(vdrID0) 70 } 71 72 // Still shouldn't be benched due to not enough consecutive failure 73 require.Empty(b.benchlistSet) 74 require.Zero(b.benchedHeap.Len()) 75 require.Len(b.failureStreaks, 1) 76 fs := b.failureStreaks[vdrID0] 77 require.Equal(threshold-1, fs.consecutive) 78 require.True(fs.firstFailure.Equal(now)) 79 80 // Register another failure 81 b.RegisterFailure(vdrID0) 82 83 // Still shouldn't be benched because not enough time (any in this case) 84 // has passed since the first failure 85 b.lock.Lock() 86 require.Empty(b.benchlistSet) 87 require.Zero(b.benchedHeap.Len()) 88 b.lock.Unlock() 89 90 // Move the time up 91 now = now.Add(minimumFailingDuration).Add(time.Second) 92 b.lock.Lock() 93 b.clock.Set(now) 94 95 benched := false 96 benchable.BenchedF = func(ids.ID, ids.NodeID) { 97 benched = true 98 } 99 b.lock.Unlock() 100 101 // Register another failure 102 b.RegisterFailure(vdrID0) 103 104 // Now this validator should be benched 105 b.lock.Lock() 106 require.Contains(b.benchlistSet, vdrID0) 107 require.Equal(1, b.benchedHeap.Len()) 108 require.Equal(1, b.benchlistSet.Len()) 109 110 nodeID, benchedUntil, ok := b.benchedHeap.Peek() 111 require.True(ok) 112 require.Equal(vdrID0, nodeID) 113 require.False(benchedUntil.After(now.Add(duration))) 114 require.False(benchedUntil.Before(now.Add(duration / 2))) 115 require.Empty(b.failureStreaks) 116 require.True(benched) 117 benchable.BenchedF = nil 118 b.lock.Unlock() 119 120 // Give another validator [threshold-1] failures 121 for i := 0; i < threshold-1; i++ { 122 b.RegisterFailure(vdrID1) 123 } 124 125 // Register another failure 126 b.RegisterResponse(vdrID1) 127 128 // vdr1 shouldn't be benched 129 // The response should have cleared its consecutive failures 130 b.lock.Lock() 131 require.Contains(b.benchlistSet, vdrID0) 132 require.Equal(1, b.benchedHeap.Len()) 133 require.Equal(1, b.benchlistSet.Len()) 134 require.Empty(b.failureStreaks) 135 b.lock.Unlock() 136 137 // Register another failure for vdr0, who is benched 138 b.RegisterFailure(vdrID0) 139 140 // A failure for an already benched validator should not count against it 141 b.lock.Lock() 142 require.Empty(b.failureStreaks) 143 b.lock.Unlock() 144 } 145 146 // Test that the benchlist won't bench more than the maximum portion of stake 147 func TestBenchlistMaxStake(t *testing.T) { 148 require := require.New(t) 149 150 snowCtx := snowtest.Context(t, snowtest.CChainID) 151 ctx := snowtest.ConsensusContext(snowCtx) 152 vdrs := validators.NewManager() 153 vdrID0 := ids.GenerateTestNodeID() 154 vdrID1 := ids.GenerateTestNodeID() 155 vdrID2 := ids.GenerateTestNodeID() 156 vdrID3 := ids.GenerateTestNodeID() 157 vdrID4 := ids.GenerateTestNodeID() 158 159 // Total weight is 5100 160 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID0, nil, ids.Empty, 1000)) 161 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID1, nil, ids.Empty, 1000)) 162 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID2, nil, ids.Empty, 1000)) 163 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID3, nil, ids.Empty, 2000)) 164 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID4, nil, ids.Empty, 100)) 165 166 threshold := 3 167 duration := 1 * time.Hour 168 // Shouldn't bench more than 2550 (5100/2) 169 maxPortion := 0.5 170 benchIntf, err := NewBenchlist( 171 ctx, 172 &TestBenchable{T: t}, 173 vdrs, 174 threshold, 175 minimumFailingDuration, 176 duration, 177 maxPortion, 178 prometheus.NewRegistry(), 179 ) 180 require.NoError(err) 181 b := benchIntf.(*benchlist) 182 now := time.Now() 183 b.clock.Set(now) 184 185 // Register [threshold-1] failures for 3 validators 186 for _, vdrID := range []ids.NodeID{vdrID0, vdrID1, vdrID2} { 187 for i := 0; i < threshold-1; i++ { 188 b.RegisterFailure(vdrID) 189 } 190 } 191 192 // Advance the time to past the minimum failing duration 193 newTime := now.Add(minimumFailingDuration).Add(time.Second) 194 b.lock.Lock() 195 b.clock.Set(newTime) 196 b.lock.Unlock() 197 198 // Register another failure for all three 199 for _, vdrID := range []ids.NodeID{vdrID0, vdrID1, vdrID2} { 200 b.RegisterFailure(vdrID) 201 } 202 203 // Only vdr0 and vdr1 should be benched (total weight 2000) 204 // Benching vdr2 (weight 1000) would cause the amount benched 205 // to exceed the maximum 206 b.lock.Lock() 207 require.Contains(b.benchlistSet, vdrID0) 208 require.Contains(b.benchlistSet, vdrID1) 209 require.Equal(2, b.benchedHeap.Len()) 210 require.Equal(2, b.benchlistSet.Len()) 211 require.Len(b.failureStreaks, 1) 212 fs := b.failureStreaks[vdrID2] 213 fs.consecutive = threshold 214 fs.firstFailure = now 215 b.lock.Unlock() 216 217 // Register threshold - 1 failures for vdr4 218 for i := 0; i < threshold-1; i++ { 219 b.RegisterFailure(vdrID4) 220 } 221 222 // Advance the time past min failing duration 223 newTime2 := newTime.Add(minimumFailingDuration).Add(time.Second) 224 b.lock.Lock() 225 b.clock.Set(newTime2) 226 b.lock.Unlock() 227 228 // Register another failure for vdr4 229 b.RegisterFailure(vdrID4) 230 231 // vdr4 should be benched now 232 b.lock.Lock() 233 require.Contains(b.benchlistSet, vdrID0) 234 require.Contains(b.benchlistSet, vdrID1) 235 require.Contains(b.benchlistSet, vdrID4) 236 require.Equal(3, b.benchedHeap.Len()) 237 require.Equal(3, b.benchlistSet.Len()) 238 require.Contains(b.benchlistSet, vdrID0) 239 require.Contains(b.benchlistSet, vdrID1) 240 require.Contains(b.benchlistSet, vdrID4) 241 require.Len(b.failureStreaks, 1) // for vdr2 242 b.lock.Unlock() 243 244 // More failures for vdr2 shouldn't add it to the bench 245 // because the max bench amount would be exceeded 246 for i := 0; i < threshold-1; i++ { 247 b.RegisterFailure(vdrID2) 248 } 249 250 b.lock.Lock() 251 require.Contains(b.benchlistSet, vdrID0) 252 require.Contains(b.benchlistSet, vdrID1) 253 require.Contains(b.benchlistSet, vdrID4) 254 require.Equal(3, b.benchedHeap.Len()) 255 require.Equal(3, b.benchlistSet.Len()) 256 require.Len(b.failureStreaks, 1) 257 require.Contains(b.failureStreaks, vdrID2) 258 b.lock.Unlock() 259 } 260 261 // Test validators are removed from the bench correctly 262 func TestBenchlistRemove(t *testing.T) { 263 require := require.New(t) 264 265 snowCtx := snowtest.Context(t, snowtest.CChainID) 266 ctx := snowtest.ConsensusContext(snowCtx) 267 vdrs := validators.NewManager() 268 vdrID0 := ids.GenerateTestNodeID() 269 vdrID1 := ids.GenerateTestNodeID() 270 vdrID2 := ids.GenerateTestNodeID() 271 vdrID3 := ids.GenerateTestNodeID() 272 vdrID4 := ids.GenerateTestNodeID() 273 274 // Total weight is 5000 275 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID0, nil, ids.Empty, 1000)) 276 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID1, nil, ids.Empty, 1000)) 277 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID2, nil, ids.Empty, 1000)) 278 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID3, nil, ids.Empty, 1000)) 279 require.NoError(vdrs.AddStaker(ctx.SubnetID, vdrID4, nil, ids.Empty, 1000)) 280 281 count := 0 282 benchable := &TestBenchable{ 283 T: t, 284 CantUnbenched: true, 285 UnbenchedF: func(ids.ID, ids.NodeID) { 286 count++ 287 }, 288 } 289 290 threshold := 3 291 duration := 2 * time.Second 292 maxPortion := 0.76 // can bench 3 of the 5 validators 293 benchIntf, err := NewBenchlist( 294 ctx, 295 benchable, 296 vdrs, 297 threshold, 298 minimumFailingDuration, 299 duration, 300 maxPortion, 301 prometheus.NewRegistry(), 302 ) 303 require.NoError(err) 304 b := benchIntf.(*benchlist) 305 now := time.Now() 306 b.lock.Lock() 307 b.clock.Set(now) 308 b.lock.Unlock() 309 310 // Register [threshold-1] failures for 3 validators 311 for _, vdrID := range []ids.NodeID{vdrID0, vdrID1, vdrID2} { 312 for i := 0; i < threshold-1; i++ { 313 b.RegisterFailure(vdrID) 314 } 315 } 316 317 // Advance the time past the min failing duration and register another failure 318 // for each 319 now = now.Add(minimumFailingDuration).Add(time.Second) 320 b.lock.Lock() 321 b.clock.Set(now) 322 b.lock.Unlock() 323 for _, vdrID := range []ids.NodeID{vdrID0, vdrID1, vdrID2} { 324 b.RegisterFailure(vdrID) 325 } 326 327 // All 3 should be benched 328 b.lock.Lock() 329 require.Contains(b.benchlistSet, vdrID0) 330 require.Contains(b.benchlistSet, vdrID1) 331 require.Contains(b.benchlistSet, vdrID2) 332 require.Equal(3, b.benchedHeap.Len()) 333 require.Equal(3, b.benchlistSet.Len()) 334 require.Empty(b.failureStreaks) 335 336 // Set the benchlist's clock past when all validators should be unbenched 337 // so that when its timer fires, it can remove them 338 b.clock.Set(b.clock.Time().Add(duration)) 339 b.lock.Unlock() 340 341 // Make sure each validator is eventually removed 342 require.Eventually( 343 func() bool { 344 return !b.IsBenched(vdrID0) 345 }, 346 duration+time.Second, // extra time.Second as grace period 347 100*time.Millisecond, 348 ) 349 350 require.Eventually( 351 func() bool { 352 return !b.IsBenched(vdrID1) 353 }, 354 duration+time.Second, 355 100*time.Millisecond, 356 ) 357 358 require.Eventually( 359 func() bool { 360 return !b.IsBenched(vdrID2) 361 }, 362 duration+time.Second, 363 100*time.Millisecond, 364 ) 365 366 require.Equal(3, count) 367 }