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  }