github.com/prysmaticlabs/prysm@v1.4.4/slasher/detection/attestations/spanner_test.go (about)

     1  package attestations
     2  
     3  import (
     4  	"context"
     5  	"reflect"
     6  	"testing"
     7  
     8  	types "github.com/prysmaticlabs/eth2-types"
     9  	ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1"
    10  	"github.com/prysmaticlabs/prysm/shared/featureconfig"
    11  	"github.com/prysmaticlabs/prysm/shared/sliceutil"
    12  	"github.com/prysmaticlabs/prysm/shared/testutil/assert"
    13  	"github.com/prysmaticlabs/prysm/shared/testutil/require"
    14  	testDB "github.com/prysmaticlabs/prysm/slasher/db/testing"
    15  	dbtypes "github.com/prysmaticlabs/prysm/slasher/db/types"
    16  	slashertypes "github.com/prysmaticlabs/prysm/slasher/detection/attestations/types"
    17  )
    18  
    19  func indexedAttestation(source, target types.Epoch, indices []uint64) *ethpb.IndexedAttestation {
    20  	return &ethpb.IndexedAttestation{
    21  		AttestingIndices: indices,
    22  		Data: &ethpb.AttestationData{
    23  			Source: &ethpb.Checkpoint{
    24  				Epoch: source,
    25  				Root:  []byte("good source"),
    26  			},
    27  			Target: &ethpb.Checkpoint{
    28  				Epoch: target,
    29  				Root:  []byte("good target"),
    30  			},
    31  		},
    32  		Signature: []byte{1, 2},
    33  	}
    34  }
    35  
    36  func TestSpanDetector_DetectSlashingsForAttestation_Double(t *testing.T) {
    37  	type testStruct struct {
    38  		name        string
    39  		att         *ethpb.IndexedAttestation
    40  		incomingAtt *ethpb.IndexedAttestation
    41  		slashCount  int
    42  	}
    43  	tests := []testStruct{
    44  		{
    45  			name: "att with different target root, same target epoch, should slash",
    46  			att: &ethpb.IndexedAttestation{
    47  				AttestingIndices: []uint64{1, 2},
    48  				Data: &ethpb.AttestationData{
    49  					Source: &ethpb.Checkpoint{
    50  						Epoch: 0,
    51  						Root:  []byte("good source"),
    52  					},
    53  					Target: &ethpb.Checkpoint{
    54  						Epoch: 2,
    55  						Root:  []byte("good target"),
    56  					},
    57  				},
    58  				Signature: []byte{1, 2},
    59  			},
    60  			incomingAtt: &ethpb.IndexedAttestation{
    61  				AttestingIndices: []uint64{2},
    62  				Data: &ethpb.AttestationData{
    63  					Source: &ethpb.Checkpoint{
    64  						Epoch: 0,
    65  						Root:  []byte("good source"),
    66  					},
    67  					Target: &ethpb.Checkpoint{
    68  						Epoch: 2,
    69  						Root:  []byte("bad target"),
    70  					},
    71  				},
    72  			},
    73  			slashCount: 1,
    74  		},
    75  		{
    76  			name: "att with different source, same target, should slash",
    77  			att: &ethpb.IndexedAttestation{
    78  				AttestingIndices: []uint64{1, 2},
    79  				Data: &ethpb.AttestationData{
    80  					Source: &ethpb.Checkpoint{
    81  						Epoch: 0,
    82  						Root:  []byte("good source"),
    83  					},
    84  					Target: &ethpb.Checkpoint{
    85  						Epoch: 2,
    86  						Root:  []byte("good target"),
    87  					},
    88  				},
    89  				Signature: []byte{1, 2},
    90  			},
    91  			incomingAtt: &ethpb.IndexedAttestation{
    92  				AttestingIndices: []uint64{2},
    93  				Data: &ethpb.AttestationData{
    94  					Source: &ethpb.Checkpoint{
    95  						Epoch: 1,
    96  						Root:  []byte("bad source"),
    97  					},
    98  					Target: &ethpb.Checkpoint{
    99  						Epoch: 2,
   100  						Root:  []byte("bad target"),
   101  					},
   102  				},
   103  			},
   104  			slashCount: 1,
   105  		},
   106  		{
   107  			name: "att with different committee index, rest is the same, should slash",
   108  			att: &ethpb.IndexedAttestation{
   109  				AttestingIndices: []uint64{1, 2},
   110  				Data: &ethpb.AttestationData{
   111  					CommitteeIndex: 4,
   112  					Source: &ethpb.Checkpoint{
   113  						Epoch: 0,
   114  						Root:  []byte("good source"),
   115  					},
   116  					Target: &ethpb.Checkpoint{
   117  						Epoch: 2,
   118  						Root:  []byte("good target"),
   119  					},
   120  				},
   121  				Signature: []byte{1, 2},
   122  			},
   123  			incomingAtt: &ethpb.IndexedAttestation{
   124  				AttestingIndices: []uint64{2},
   125  				Data: &ethpb.AttestationData{
   126  					CommitteeIndex: 3,
   127  					Source: &ethpb.Checkpoint{
   128  						Epoch: 1,
   129  						Root:  []byte("bad source"),
   130  					},
   131  					Target: &ethpb.Checkpoint{
   132  						Epoch: 2,
   133  						Root:  []byte("bad target"),
   134  					},
   135  				},
   136  				Signature: []byte{1, 2},
   137  			},
   138  			slashCount: 1,
   139  		},
   140  		{
   141  			name: "att with same target and source, different block root, should slash",
   142  			att: &ethpb.IndexedAttestation{
   143  				AttestingIndices: []uint64{1, 2, 4, 6},
   144  				Data: &ethpb.AttestationData{
   145  					Source: &ethpb.Checkpoint{
   146  						Epoch: 0,
   147  						Root:  []byte("good source"),
   148  					},
   149  					Target: &ethpb.Checkpoint{
   150  						Epoch: 2,
   151  						Root:  []byte("good target"),
   152  					},
   153  					BeaconBlockRoot: []byte("good block root"),
   154  				},
   155  				Signature: []byte{1, 2},
   156  			},
   157  			incomingAtt: &ethpb.IndexedAttestation{
   158  				AttestingIndices: []uint64{2, 4, 6},
   159  				Data: &ethpb.AttestationData{
   160  					Source: &ethpb.Checkpoint{
   161  						Epoch: 0,
   162  						Root:  []byte("good source"),
   163  					},
   164  					Target: &ethpb.Checkpoint{
   165  						Epoch: 2,
   166  						Root:  []byte("good target"),
   167  					},
   168  					BeaconBlockRoot: []byte("bad block root"),
   169  				},
   170  			},
   171  			slashCount: 3,
   172  		},
   173  		{
   174  			name: "att with different target, should not detect possible double",
   175  			att: &ethpb.IndexedAttestation{
   176  				AttestingIndices: []uint64{1, 2, 4, 6},
   177  				Data: &ethpb.AttestationData{
   178  					Source: &ethpb.Checkpoint{
   179  						Epoch: 0,
   180  						Root:  []byte("good source"),
   181  					},
   182  					Target: &ethpb.Checkpoint{
   183  						Epoch: 2,
   184  						Root:  []byte("good target"),
   185  					},
   186  					BeaconBlockRoot: []byte("good block root"),
   187  				},
   188  				Signature: []byte{1, 2},
   189  			},
   190  			incomingAtt: &ethpb.IndexedAttestation{
   191  				AttestingIndices: []uint64{2, 4, 6},
   192  				Data: &ethpb.AttestationData{
   193  					Source: &ethpb.Checkpoint{
   194  						Epoch: 0,
   195  						Root:  []byte("good source"),
   196  					},
   197  					Target: &ethpb.Checkpoint{
   198  						Epoch: 1,
   199  						Root:  []byte("really good target"),
   200  					},
   201  					BeaconBlockRoot: []byte("really good block root"),
   202  				},
   203  			},
   204  			slashCount: 0,
   205  		},
   206  		{
   207  			name: "same att with different aggregates, should detect possible double",
   208  			att: &ethpb.IndexedAttestation{
   209  				AttestingIndices: []uint64{1, 2, 4, 6},
   210  				Data: &ethpb.AttestationData{
   211  					Source: &ethpb.Checkpoint{
   212  						Epoch: 0,
   213  						Root:  []byte("good source"),
   214  					},
   215  					Target: &ethpb.Checkpoint{
   216  						Epoch: 2,
   217  						Root:  []byte("good target"),
   218  					},
   219  					BeaconBlockRoot: []byte("good block root"),
   220  				},
   221  				Signature: []byte{1, 2},
   222  			},
   223  			incomingAtt: &ethpb.IndexedAttestation{
   224  				AttestingIndices: []uint64{2, 3, 4, 16},
   225  				Data: &ethpb.AttestationData{
   226  					Source: &ethpb.Checkpoint{
   227  						Epoch: 0,
   228  						Root:  []byte("good source"),
   229  					},
   230  					Target: &ethpb.Checkpoint{
   231  						Epoch: 2,
   232  						Root:  []byte("good target"),
   233  					},
   234  					BeaconBlockRoot: []byte("good block root"),
   235  				},
   236  			},
   237  			slashCount: 2,
   238  		},
   239  	}
   240  	for _, tt := range tests {
   241  		t.Run(tt.name, func(t *testing.T) {
   242  			db := testDB.SetupSlasherDB(t, false)
   243  			ctx := context.Background()
   244  
   245  			sd := &SpanDetector{
   246  				slasherDB: db,
   247  			}
   248  
   249  			require.NoError(t, sd.UpdateSpans(ctx, tt.att))
   250  
   251  			res, err := sd.DetectSlashingsForAttestation(ctx, tt.incomingAtt)
   252  			require.NoError(t, err)
   253  
   254  			var want []*slashertypes.DetectionResult
   255  			if tt.slashCount > 0 {
   256  				for _, indice := range sliceutil.IntersectionUint64(tt.att.AttestingIndices, tt.incomingAtt.AttestingIndices) {
   257  					want = append(want, &slashertypes.DetectionResult{
   258  						ValidatorIndex: indice,
   259  						Kind:           slashertypes.DoubleVote,
   260  						SlashableEpoch: tt.incomingAtt.Data.Target.Epoch,
   261  						SigBytes:       [2]byte{1, 2},
   262  					})
   263  				}
   264  			}
   265  			assert.DeepEqual(t, want, res)
   266  			require.Equal(t, tt.slashCount, len(res), "Unexpected amount of slashings found")
   267  		})
   268  	}
   269  }
   270  
   271  func TestSpanDetector_DetectSlashingsForAttestation_Surround(t *testing.T) {
   272  	type testStruct struct {
   273  		name                     string
   274  		sourceEpoch              types.Epoch
   275  		targetEpoch              types.Epoch
   276  		slashableEpoch           types.Epoch
   277  		shouldSlash              bool
   278  		spansByEpochForValidator map[types.Epoch][3]uint16
   279  	}
   280  	tests := []testStruct{
   281  		{
   282  			name:           "Should slash if max span > distance",
   283  			sourceEpoch:    3,
   284  			targetEpoch:    6,
   285  			slashableEpoch: 7,
   286  			shouldSlash:    true,
   287  			// Given a distance of (6 - 3) = 3, we want the validator at epoch 3 to have
   288  			// committed a slashable offense by having a max span of 4 > distance.
   289  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   290  				3: {0, 4},
   291  			},
   292  		},
   293  		{
   294  			name:        "Should NOT slash if max span < distance",
   295  			sourceEpoch: 3,
   296  			targetEpoch: 6,
   297  			// Given a distance of (6 - 3) = 3, we want the validator at epoch 3 to NOT
   298  			// have committed slashable offense by having a max span of 1 < distance.
   299  			shouldSlash: false,
   300  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   301  				3: {0, 1},
   302  			},
   303  		},
   304  		{
   305  			name:        "Should NOT slash if max span == distance",
   306  			sourceEpoch: 3,
   307  			targetEpoch: 6,
   308  			// Given a distance of (6 - 3) = 3, we want the validator at epoch 3 to NOT
   309  			// have committed slashable offense by having a max span of 3 == distance.
   310  			shouldSlash: false,
   311  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   312  				3: {0, 3},
   313  			},
   314  		},
   315  		{
   316  			name:        "Should NOT slash if min span == 0",
   317  			sourceEpoch: 3,
   318  			targetEpoch: 6,
   319  			// Given a min span of 0 and no max span slashing, we want validator to NOT
   320  			// have committed a slashable offense if min span == 0.
   321  			shouldSlash: false,
   322  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   323  				3: {0, 1},
   324  			},
   325  		},
   326  		{
   327  			name:        "Should slash if min span > 0 and min span < distance",
   328  			sourceEpoch: 3,
   329  			targetEpoch: 6,
   330  			// Given a distance of (6 - 3) = 3, we want the validator at epoch 3 to have
   331  			// committed a slashable offense by having a min span of 1 < distance.
   332  			shouldSlash:    true,
   333  			slashableEpoch: 4,
   334  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   335  				3: {1, 0},
   336  			},
   337  		},
   338  		// Proto Max Span Tests from the eth2-surround repo.
   339  		{
   340  			name:        "Proto max span test #1",
   341  			sourceEpoch: 8,
   342  			targetEpoch: 18,
   343  			shouldSlash: false,
   344  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   345  				0: {4, 0},
   346  				1: {2, 0},
   347  				2: {1, 0},
   348  				4: {0, 2},
   349  				5: {0, 1},
   350  			},
   351  		},
   352  		{
   353  			name:           "Proto max span test #2",
   354  			sourceEpoch:    4,
   355  			targetEpoch:    12,
   356  			shouldSlash:    false,
   357  			slashableEpoch: 0,
   358  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   359  				4:  {14, 2},
   360  				5:  {13, 1},
   361  				6:  {12, 0},
   362  				7:  {11, 0},
   363  				9:  {0, 9},
   364  				10: {0, 8},
   365  				11: {0, 7},
   366  				12: {0, 6},
   367  				13: {0, 5},
   368  				14: {0, 4},
   369  				15: {0, 3},
   370  				16: {0, 2},
   371  				17: {0, 1},
   372  			},
   373  		},
   374  		{
   375  			name:           "Proto max span test #3",
   376  			sourceEpoch:    10,
   377  			targetEpoch:    15,
   378  			shouldSlash:    true,
   379  			slashableEpoch: 18,
   380  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   381  				4:  {14, 2},
   382  				5:  {13, 7},
   383  				6:  {12, 6},
   384  				7:  {11, 5},
   385  				8:  {0, 4},
   386  				9:  {0, 9},
   387  				10: {0, 8},
   388  				11: {0, 7},
   389  				12: {0, 6},
   390  				13: {0, 5},
   391  				14: {0, 4},
   392  				15: {0, 3},
   393  				16: {0, 2},
   394  				17: {0, 1},
   395  			},
   396  		},
   397  		// Proto Min Span Tests from the eth2-surround repo.
   398  		{
   399  			name:        "Proto min span test #1",
   400  			sourceEpoch: 4,
   401  			targetEpoch: 6,
   402  			shouldSlash: false,
   403  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   404  				1: {5, 0},
   405  				2: {4, 0},
   406  				3: {3, 0},
   407  			},
   408  		},
   409  		{
   410  			name:        "Proto min span test #2",
   411  			sourceEpoch: 11,
   412  			targetEpoch: 15,
   413  			shouldSlash: false,
   414  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   415  				1:  {5, 0},
   416  				2:  {4, 0},
   417  				3:  {3, 0},
   418  				4:  {14, 0},
   419  				5:  {13, 1},
   420  				6:  {12, 0},
   421  				7:  {11, 0},
   422  				8:  {10, 0},
   423  				9:  {9, 0},
   424  				10: {8, 0},
   425  				11: {7, 0},
   426  				12: {6, 0},
   427  				14: {0, 4},
   428  				15: {0, 3},
   429  				16: {0, 2},
   430  				17: {0, 1},
   431  			},
   432  		},
   433  		{
   434  			name:           "Proto min span test #3",
   435  			sourceEpoch:    9,
   436  			targetEpoch:    19,
   437  			shouldSlash:    true,
   438  			slashableEpoch: 14,
   439  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   440  				0:  {5, 0},
   441  				1:  {4, 0},
   442  				2:  {3, 0},
   443  				3:  {11, 0},
   444  				4:  {10, 1},
   445  				5:  {9, 0},
   446  				6:  {8, 0},
   447  				7:  {7, 0},
   448  				8:  {6, 0},
   449  				9:  {5, 0},
   450  				10: {7, 0},
   451  				11: {6, 3},
   452  				12: {0, 2},
   453  				13: {0, 1},
   454  				14: {0, 3},
   455  				15: {0, 2},
   456  				16: {0, 1},
   457  				17: {0, 0},
   458  			},
   459  		},
   460  		{
   461  			name:           "Should slash if max span > distance && source > target",
   462  			sourceEpoch:    6,
   463  			targetEpoch:    3,
   464  			slashableEpoch: 7,
   465  			shouldSlash:    true,
   466  			// Given a distance of (6 - 3) = 3, we want the validator at epoch 3 to have
   467  			// committed a slashable offense by having a max span of 4 > distance.
   468  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   469  				3: {0, 4},
   470  			},
   471  		},
   472  		{
   473  			name:        "Should not slash if max span = distance && source > target",
   474  			sourceEpoch: 6,
   475  			targetEpoch: 3,
   476  			shouldSlash: false,
   477  			// Given a distance of (6 - 3) = 3, we want the validator at epoch 3 to NOT
   478  			// have committed slashable offense by having a max span of 1 < distance.
   479  			spansByEpochForValidator: map[types.Epoch][3]uint16{
   480  				3: {0, 1},
   481  			},
   482  		},
   483  	}
   484  	for _, tt := range tests {
   485  		t.Run(tt.name, func(t *testing.T) {
   486  			db := testDB.SetupSlasherDB(t, false)
   487  			ctx := context.Background()
   488  
   489  			sd := &SpanDetector{
   490  				slasherDB: db,
   491  			}
   492  			// We only care about validator index 0 for these tests for simplicity.
   493  			validatorIndex := uint64(0)
   494  			for k, v := range tt.spansByEpochForValidator {
   495  				epochStore, err := slashertypes.NewEpochStore([]byte{})
   496  				require.NoError(t, err)
   497  				span := slashertypes.Span{
   498  					MinSpan: v[0],
   499  					MaxSpan: v[1],
   500  				}
   501  				epochStore, err = epochStore.SetValidatorSpan(validatorIndex, span)
   502  				require.NoError(t, err)
   503  				require.NoError(t, sd.slasherDB.SaveEpochSpans(ctx, k, epochStore, dbtypes.UseDB), "Failed to save to slasherDB")
   504  			}
   505  
   506  			att := &ethpb.IndexedAttestation{
   507  				Data: &ethpb.AttestationData{
   508  					Source: &ethpb.Checkpoint{
   509  						Epoch: tt.sourceEpoch,
   510  					},
   511  					Target: &ethpb.Checkpoint{
   512  						Epoch: tt.targetEpoch,
   513  					},
   514  				},
   515  				AttestingIndices: []uint64{0},
   516  			}
   517  			res, err := sd.DetectSlashingsForAttestation(ctx, att)
   518  			require.NoError(t, err)
   519  			require.Equal(t, false, !tt.shouldSlash && res != nil, "Did not want validator to be slashed but found slashable offense: %v", res)
   520  			if tt.shouldSlash {
   521  				want := []*slashertypes.DetectionResult{
   522  					{
   523  						Kind:           slashertypes.SurroundVote,
   524  						SlashableEpoch: tt.slashableEpoch,
   525  					},
   526  				}
   527  				assert.DeepEqual(t, want, res)
   528  			}
   529  		})
   530  	}
   531  }
   532  
   533  func TestSpanDetector_DetectSlashingsForAttestation_MultipleValidators(t *testing.T) {
   534  	type testStruct struct {
   535  		name            string
   536  		incomingAtt     *ethpb.IndexedAttestation
   537  		slashableEpochs []types.Epoch
   538  		shouldSlash     []bool
   539  		atts            []*ethpb.IndexedAttestation
   540  	}
   541  	tests := []testStruct{
   542  		{
   543  			name: "3 of 4 validators slashed, differing histories",
   544  			incomingAtt: &ethpb.IndexedAttestation{
   545  				AttestingIndices: []uint64{0, 1, 2, 3},
   546  				Data: &ethpb.AttestationData{
   547  					Source: &ethpb.Checkpoint{
   548  						Epoch: 3,
   549  						Root:  []byte("good source"),
   550  					},
   551  					Target: &ethpb.Checkpoint{
   552  						Epoch: 6,
   553  						Root:  []byte("good target"),
   554  					},
   555  				},
   556  				Signature: []byte{1, 2},
   557  			},
   558  			slashableEpochs: []types.Epoch{6, 7, 5, 0},
   559  			// Detections - double, surround, surrounded, none.
   560  			shouldSlash: []bool{true, true, true, false},
   561  			// Atts in map: (src, epoch) - 0: (3, 6), 1: (2, 7), 2: (4, 5), 3: (5, 7)
   562  			atts: []*ethpb.IndexedAttestation{
   563  				indexedAttestation(3, 6, []uint64{0}),
   564  				indexedAttestation(2, 7, []uint64{1}),
   565  				indexedAttestation(4, 5, []uint64{2}),
   566  				indexedAttestation(5, 7, []uint64{3}),
   567  			},
   568  		},
   569  		{
   570  			name: "3 of 4 validators slashed, differing surrounds",
   571  			incomingAtt: &ethpb.IndexedAttestation{
   572  				AttestingIndices: []uint64{0, 1, 2, 3},
   573  				Data: &ethpb.AttestationData{
   574  					Source: &ethpb.Checkpoint{
   575  						Epoch: 5,
   576  						Root:  []byte("good source"),
   577  					},
   578  					Target: &ethpb.Checkpoint{
   579  						Epoch: 7,
   580  						Root:  []byte("good target"),
   581  					},
   582  				},
   583  				Signature: []byte{1, 2},
   584  			},
   585  			slashableEpochs: []types.Epoch{8, 9, 10, 0},
   586  			// Detections - surround, surround, surround, none.
   587  			shouldSlash: []bool{true, true, true, false},
   588  			// Atts in map: (src, epoch) - 0: (1, 8), 1: (3, 9), 2: (2, 10), 3: (4, 6)
   589  			atts: []*ethpb.IndexedAttestation{
   590  				indexedAttestation(1, 8, []uint64{0}),
   591  				indexedAttestation(3, 9, []uint64{1}),
   592  				indexedAttestation(2, 10, []uint64{2}),
   593  				indexedAttestation(4, 6, []uint64{3}),
   594  			},
   595  		},
   596  		{
   597  			name: "3 of 4 validators slashed, differing surrounded",
   598  			incomingAtt: &ethpb.IndexedAttestation{
   599  				AttestingIndices: []uint64{0, 1, 2, 3},
   600  				Data: &ethpb.AttestationData{
   601  					Source: &ethpb.Checkpoint{
   602  						Epoch: 2,
   603  						Root:  []byte("good source"),
   604  					},
   605  					Target: &ethpb.Checkpoint{
   606  						Epoch: 9,
   607  						Root:  []byte("good target"),
   608  					},
   609  				},
   610  				Signature: []byte{1, 2},
   611  			},
   612  			slashableEpochs: []types.Epoch{8, 8, 7, 0},
   613  			// Detections - surround, surround, surround, none.
   614  			shouldSlash: []bool{true, true, true, false},
   615  			// Atts in map: (src, epoch) - 0: (5, 8), 1: (3, 8), 2: (4, 7), 3: (1, 5)
   616  			atts: []*ethpb.IndexedAttestation{
   617  				indexedAttestation(5, 8, []uint64{0}),
   618  				indexedAttestation(3, 8, []uint64{1}),
   619  				indexedAttestation(4, 7, []uint64{2}),
   620  				indexedAttestation(1, 5, []uint64{3}),
   621  			},
   622  		},
   623  		{
   624  			name: "3 of 4 validators slashed, differing doubles",
   625  			incomingAtt: &ethpb.IndexedAttestation{
   626  				AttestingIndices: []uint64{0, 1, 2, 3},
   627  				Data: &ethpb.AttestationData{
   628  					Source: &ethpb.Checkpoint{
   629  						Epoch: 2,
   630  						Root:  []byte("good source"),
   631  					},
   632  					Target: &ethpb.Checkpoint{
   633  						Epoch: 7,
   634  						Root:  []byte("good target"),
   635  					},
   636  				},
   637  				Signature: []byte{1, 2},
   638  			},
   639  			slashableEpochs: []types.Epoch{7, 7, 7, 0},
   640  			// Detections - surround, surround, surround, none.
   641  			shouldSlash: []bool{true, true, true, false},
   642  			// Atts in map: (src, epoch) - 0: (2, 7), 1: (3, 7), 2: (6, 7), 3: (1, 5)
   643  			atts: []*ethpb.IndexedAttestation{
   644  				indexedAttestation(2, 7, []uint64{0}),
   645  				indexedAttestation(3, 7, []uint64{1}),
   646  				indexedAttestation(6, 7, []uint64{2}),
   647  				indexedAttestation(1, 5, []uint64{3}),
   648  			},
   649  		},
   650  		{
   651  			name: "3 of 4 validators slashed, differing surrounds source > target",
   652  			incomingAtt: &ethpb.IndexedAttestation{
   653  				AttestingIndices: []uint64{0, 1, 2, 3},
   654  				Data: &ethpb.AttestationData{
   655  					Source: &ethpb.Checkpoint{
   656  						Epoch: 7,
   657  						Root:  []byte("good source"),
   658  					},
   659  					Target: &ethpb.Checkpoint{
   660  						Epoch: 5,
   661  						Root:  []byte("good target"),
   662  					},
   663  				},
   664  				Signature: []byte{1, 2},
   665  			},
   666  			slashableEpochs: []types.Epoch{8, 9, 10, 0},
   667  			// Detections - surround, surround, surround, none.
   668  			shouldSlash: []bool{true, true, true, false},
   669  			// Atts in map: (src, epoch) - 0: (1, 8), 1: (3, 9), 2: (2, 10), 3: (4, 6)
   670  			atts: []*ethpb.IndexedAttestation{
   671  				indexedAttestation(1, 8, []uint64{0}),
   672  				indexedAttestation(3, 9, []uint64{1}),
   673  				indexedAttestation(2, 10, []uint64{2}),
   674  				indexedAttestation(4, 6, []uint64{3}),
   675  			},
   676  		},
   677  		{
   678  			name: "3 of 4 validators slashed, differing surrounded source > target",
   679  			incomingAtt: &ethpb.IndexedAttestation{
   680  				AttestingIndices: []uint64{0, 1, 2, 3},
   681  				Data: &ethpb.AttestationData{
   682  					Source: &ethpb.Checkpoint{
   683  						Epoch: 9,
   684  						Root:  []byte("good source"),
   685  					},
   686  					Target: &ethpb.Checkpoint{
   687  						Epoch: 2,
   688  						Root:  []byte("good target"),
   689  					},
   690  				},
   691  				Signature: []byte{1, 2},
   692  			},
   693  			slashableEpochs: []types.Epoch{8, 8, 7, 0},
   694  			// Detections - surround, surround, surround, none.
   695  			shouldSlash: []bool{true, true, true, false},
   696  			// Atts in map: (src, epoch) - 0: (5, 8), 1: (3, 8), 2: (4, 7), 3: (1, 5)
   697  			atts: []*ethpb.IndexedAttestation{
   698  				indexedAttestation(5, 8, []uint64{0}),
   699  				indexedAttestation(3, 8, []uint64{1}),
   700  				indexedAttestation(4, 7, []uint64{2}),
   701  				indexedAttestation(1, 5, []uint64{3}),
   702  			},
   703  		},
   704  		{
   705  			name: "3 of 4 validators slashed, differing surrounded source > target in update",
   706  			incomingAtt: &ethpb.IndexedAttestation{
   707  				AttestingIndices: []uint64{0, 1, 2, 3},
   708  				Data: &ethpb.AttestationData{
   709  					Source: &ethpb.Checkpoint{
   710  						Epoch: 2,
   711  						Root:  []byte("good source"),
   712  					},
   713  					Target: &ethpb.Checkpoint{
   714  						Epoch: 9,
   715  						Root:  []byte("good target"),
   716  					},
   717  				},
   718  				Signature: []byte{1, 2},
   719  			},
   720  			slashableEpochs: []types.Epoch{8, 8, 7, 0},
   721  			// Detections - surround, surround, surround, none.
   722  			shouldSlash: []bool{true, true, true, false},
   723  			// Atts in map: (src, epoch) - 0: (5, 8), 1: (3, 8), 2: (4, 7), 3: (1, 5)
   724  			atts: []*ethpb.IndexedAttestation{
   725  				indexedAttestation(8, 5, []uint64{0}),
   726  				indexedAttestation(8, 3, []uint64{1}),
   727  				indexedAttestation(7, 4, []uint64{2}),
   728  				indexedAttestation(5, 1, []uint64{3}),
   729  			},
   730  		},
   731  	}
   732  	for _, tt := range tests {
   733  		t.Run(tt.name, func(t *testing.T) {
   734  			db := testDB.SetupSlasherDB(t, false)
   735  			ctx := context.Background()
   736  			defer func() {
   737  				assert.NoError(t, db.ClearDB())
   738  			}()
   739  			defer func() {
   740  				assert.NoError(t, db.Close())
   741  			}()
   742  
   743  			spanDetector := &SpanDetector{
   744  				slasherDB: db,
   745  			}
   746  			for _, att := range tt.atts {
   747  				require.NoError(t, spanDetector.UpdateSpans(ctx, att), "Failed to save to slasherDB")
   748  			}
   749  			res, err := spanDetector.DetectSlashingsForAttestation(ctx, tt.incomingAtt)
   750  			require.NoError(t, err)
   751  			var want []*slashertypes.DetectionResult
   752  			for i := 0; i < len(tt.incomingAtt.AttestingIndices); i++ {
   753  				if tt.shouldSlash[i] {
   754  					if tt.slashableEpochs[i] == tt.incomingAtt.Data.Target.Epoch {
   755  						want = append(want, &slashertypes.DetectionResult{
   756  							ValidatorIndex: uint64(i),
   757  							Kind:           slashertypes.DoubleVote,
   758  							SlashableEpoch: tt.slashableEpochs[i],
   759  							SigBytes:       [2]byte{1, 2},
   760  						})
   761  					} else {
   762  						want = append(want, &slashertypes.DetectionResult{
   763  							ValidatorIndex: uint64(i),
   764  							Kind:           slashertypes.SurroundVote,
   765  							SlashableEpoch: tt.slashableEpochs[i],
   766  							SigBytes:       [2]byte{1, 2},
   767  						})
   768  					}
   769  				}
   770  			}
   771  			if !reflect.DeepEqual(want, res) {
   772  				for i, ww := range want {
   773  					t.Errorf("Wanted   %d: %+v\n", i, ww)
   774  				}
   775  				for i, rr := range res {
   776  					t.Errorf("Received %d: %+v\n", i, rr)
   777  				}
   778  				t.Errorf("Wanted: %v, received %v", want, res)
   779  			}
   780  		})
   781  	}
   782  }
   783  
   784  func TestNewSpanDetector_UpdateSpans(t *testing.T) {
   785  	type testStruct struct {
   786  		name string
   787  		att  *ethpb.IndexedAttestation
   788  		want []map[uint64]slashertypes.Span
   789  	}
   790  	tests := []testStruct{
   791  		{
   792  			name: "Distance of 2 should update min spans accordingly",
   793  			att: &ethpb.IndexedAttestation{
   794  				AttestingIndices: []uint64{0, 1, 2},
   795  				Data: &ethpb.AttestationData{
   796  					CommitteeIndex: 0,
   797  					Source: &ethpb.Checkpoint{
   798  						Epoch: 2,
   799  					},
   800  					Target: &ethpb.Checkpoint{
   801  						Epoch: 4,
   802  					},
   803  				},
   804  				Signature: []byte{1, 2},
   805  			},
   806  			want: []map[uint64]slashertypes.Span{
   807  				// Epoch 0.
   808  				{
   809  					0: {MinSpan: 4, MaxSpan: 0, SigBytes: [2]byte{0, 0}, HasAttested: false},
   810  					1: {MinSpan: 4, MaxSpan: 0, SigBytes: [2]byte{0, 0}, HasAttested: false},
   811  					2: {MinSpan: 4, MaxSpan: 0, SigBytes: [2]byte{0, 0}, HasAttested: false},
   812  				},
   813  				// Epoch 1.
   814  				{
   815  					0: {MinSpan: 3, MaxSpan: 0, SigBytes: [2]byte{0, 0}, HasAttested: false},
   816  					1: {MinSpan: 3, MaxSpan: 0, SigBytes: [2]byte{0, 0}, HasAttested: false},
   817  					2: {MinSpan: 3, MaxSpan: 0, SigBytes: [2]byte{0, 0}, HasAttested: false},
   818  				},
   819  				// Epoch 2.
   820  				{},
   821  				// Epoch 3.
   822  				{
   823  					0: {MinSpan: 0, MaxSpan: 1, SigBytes: [2]byte{0, 0}, HasAttested: false},
   824  					1: {MinSpan: 0, MaxSpan: 1, SigBytes: [2]byte{0, 0}, HasAttested: false},
   825  					2: {MinSpan: 0, MaxSpan: 1, SigBytes: [2]byte{0, 0}, HasAttested: false},
   826  				},
   827  				// Epoch 4.
   828  				{
   829  					0: {MinSpan: 0, MaxSpan: 0, SigBytes: [2]byte{1, 2}, HasAttested: true},
   830  					1: {MinSpan: 0, MaxSpan: 0, SigBytes: [2]byte{1, 2}, HasAttested: true},
   831  					2: {MinSpan: 0, MaxSpan: 0, SigBytes: [2]byte{1, 2}, HasAttested: true},
   832  				},
   833  				{},
   834  				{},
   835  				{},
   836  			},
   837  		},
   838  		{
   839  			name: "Distance of 4 should update max spans accordingly",
   840  			att: &ethpb.IndexedAttestation{
   841  				AttestingIndices: []uint64{0, 1, 2},
   842  				Data: &ethpb.AttestationData{
   843  					CommitteeIndex: 1,
   844  					Source: &ethpb.Checkpoint{
   845  						Epoch: 0,
   846  					},
   847  					Target: &ethpb.Checkpoint{
   848  						Epoch: 5,
   849  					},
   850  				},
   851  				Signature: []byte{1, 2},
   852  			},
   853  			want: []map[uint64]slashertypes.Span{
   854  				// Epoch 0.
   855  				{},
   856  				// Epoch 1.
   857  				{
   858  					0: {MinSpan: 0, MaxSpan: 4, SigBytes: [2]byte{0, 0}, HasAttested: false},
   859  					1: {MinSpan: 0, MaxSpan: 4, SigBytes: [2]byte{0, 0}, HasAttested: false},
   860  					2: {MinSpan: 0, MaxSpan: 4, SigBytes: [2]byte{0, 0}, HasAttested: false},
   861  				},
   862  				// Epoch 2.
   863  				{
   864  					0: {MinSpan: 0, MaxSpan: 3, SigBytes: [2]byte{0, 0}, HasAttested: false},
   865  					1: {MinSpan: 0, MaxSpan: 3, SigBytes: [2]byte{0, 0}, HasAttested: false},
   866  					2: {MinSpan: 0, MaxSpan: 3, SigBytes: [2]byte{0, 0}, HasAttested: false},
   867  				},
   868  				// Epoch 3.
   869  				{
   870  					0: {MinSpan: 0, MaxSpan: 2, SigBytes: [2]byte{0, 0}, HasAttested: false},
   871  					1: {MinSpan: 0, MaxSpan: 2, SigBytes: [2]byte{0, 0}, HasAttested: false},
   872  					2: {MinSpan: 0, MaxSpan: 2, SigBytes: [2]byte{0, 0}, HasAttested: false},
   873  				},
   874  				// Epoch 4.
   875  				{
   876  					0: {MinSpan: 0, MaxSpan: 1, SigBytes: [2]byte{0, 0}, HasAttested: false},
   877  					1: {MinSpan: 0, MaxSpan: 1, SigBytes: [2]byte{0, 0}, HasAttested: false},
   878  					2: {MinSpan: 0, MaxSpan: 1, SigBytes: [2]byte{0, 0}, HasAttested: false},
   879  				},
   880  				// Epoch 5.
   881  				{
   882  					0: {MinSpan: 0, MaxSpan: 0, SigBytes: [2]byte{1, 2}, HasAttested: true},
   883  					1: {MinSpan: 0, MaxSpan: 0, SigBytes: [2]byte{1, 2}, HasAttested: true},
   884  					2: {MinSpan: 0, MaxSpan: 0, SigBytes: [2]byte{1, 2}, HasAttested: true},
   885  				},
   886  				{},
   887  				{},
   888  			},
   889  		},
   890  	}
   891  	for _, tt := range tests {
   892  		t.Run(tt.name, func(t *testing.T) {
   893  			db := testDB.SetupSlasherDB(t, false)
   894  			ctx := context.Background()
   895  			defer func() {
   896  				assert.NoError(t, db.ClearDB())
   897  			}()
   898  			defer func() {
   899  				assert.NoError(t, db.Close())
   900  			}()
   901  
   902  			sd := &SpanDetector{
   903  				slasherDB: db,
   904  			}
   905  			require.NoError(t, sd.UpdateSpans(ctx, tt.att))
   906  			for epoch := range tt.want {
   907  				sm, err := sd.slasherDB.EpochSpans(ctx, types.Epoch(epoch), dbtypes.UseDB)
   908  				require.NoError(t, err, "Failed to read from slasherDB")
   909  				resMap, err := sm.ToMap()
   910  				require.NoError(t, err)
   911  				assert.DeepEqual(t, tt.want[epoch], resMap)
   912  			}
   913  		})
   914  	}
   915  }
   916  
   917  func TestSpanDetector_UpdateMinSpansCheckCacheSize(t *testing.T) {
   918  	resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{DisableLookback: true})
   919  	defer resetCfg()
   920  
   921  	att := &ethpb.IndexedAttestation{
   922  		AttestingIndices: []uint64{0},
   923  		Data: &ethpb.AttestationData{
   924  			CommitteeIndex: 0,
   925  			Source: &ethpb.Checkpoint{
   926  				Epoch: 150,
   927  			},
   928  			Target: &ethpb.Checkpoint{
   929  				Epoch: 152,
   930  			},
   931  		},
   932  		Signature: []byte{1, 2},
   933  	}
   934  
   935  	db := testDB.SetupSlasherDB(t, false)
   936  	ctx := context.Background()
   937  	defer func() {
   938  		assert.NoError(t, db.ClearDB())
   939  	}()
   940  	defer func() {
   941  		assert.NoError(t, db.Close())
   942  	}()
   943  
   944  	sd := &SpanDetector{
   945  		slasherDB: db,
   946  	}
   947  	require.NoError(t, sd.updateMinSpan(ctx, att))
   948  	require.Equal(t, int(epochLookback), db.CacheLength(ctx), "Unexpected cache length")
   949  }