github.com/prysmaticlabs/prysm@v1.4.4/validator/slashing-protection/local/standard-protection-format/import_test.go (about)

     1  package interchangeformat
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"fmt"
     9  	"reflect"
    10  	"testing"
    11  
    12  	types "github.com/prysmaticlabs/eth2-types"
    13  	ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1"
    14  	"github.com/prysmaticlabs/prysm/shared/testutil/assert"
    15  	"github.com/prysmaticlabs/prysm/shared/testutil/require"
    16  	"github.com/prysmaticlabs/prysm/validator/db/kv"
    17  	dbtest "github.com/prysmaticlabs/prysm/validator/db/testing"
    18  	"github.com/prysmaticlabs/prysm/validator/slashing-protection/local/standard-protection-format/format"
    19  	valtest "github.com/prysmaticlabs/prysm/validator/testing"
    20  	logTest "github.com/sirupsen/logrus/hooks/test"
    21  )
    22  
    23  func TestStore_ImportInterchangeData_BadJSON(t *testing.T) {
    24  	ctx := context.Background()
    25  	validatorDB := dbtest.SetupDB(t, nil)
    26  
    27  	buf := bytes.NewBuffer([]byte("helloworld"))
    28  	err := ImportStandardProtectionJSON(ctx, validatorDB, buf)
    29  	require.ErrorContains(t, "could not unmarshal slashing protection JSON file", err)
    30  }
    31  
    32  func TestStore_ImportInterchangeData_NilData_FailsSilently(t *testing.T) {
    33  	hook := logTest.NewGlobal()
    34  	ctx := context.Background()
    35  	validatorDB := dbtest.SetupDB(t, nil)
    36  
    37  	interchangeJSON := &format.EIPSlashingProtectionFormat{}
    38  	encoded, err := json.Marshal(interchangeJSON)
    39  	require.NoError(t, err)
    40  
    41  	buf := bytes.NewBuffer(encoded)
    42  	err = ImportStandardProtectionJSON(ctx, validatorDB, buf)
    43  	require.NoError(t, err)
    44  	require.LogsContain(t, hook, "No slashing protection data to import")
    45  }
    46  
    47  func TestStore_ImportInterchangeData_BadFormat_PreventsDBWrites(t *testing.T) {
    48  	ctx := context.Background()
    49  	numValidators := 10
    50  	publicKeys, err := valtest.CreateRandomPubKeys(numValidators)
    51  	require.NoError(t, err)
    52  	validatorDB := dbtest.SetupDB(t, publicKeys)
    53  
    54  	// First we setup some mock attesting and proposal histories and create a mock
    55  	// standard slashing protection format JSON struct.
    56  	attestingHistory, proposalHistory := valtest.MockAttestingAndProposalHistories(publicKeys)
    57  	standardProtectionFormat, err := valtest.MockSlashingProtectionJSON(publicKeys, attestingHistory, proposalHistory)
    58  	require.NoError(t, err)
    59  
    60  	// We replace a slot of one of the blocks with junk data.
    61  	standardProtectionFormat.Data[0].SignedBlocks[0].Slot = "BadSlot"
    62  
    63  	// We encode the standard slashing protection struct into a JSON format.
    64  	blob, err := json.Marshal(standardProtectionFormat)
    65  	require.NoError(t, err)
    66  	buf := bytes.NewBuffer(blob)
    67  
    68  	// Next, we attempt to import it into our validator database and check that
    69  	// we obtain an error during the import process.
    70  	err = ImportStandardProtectionJSON(ctx, validatorDB, buf)
    71  	assert.NotNil(t, err)
    72  
    73  	// Next, we attempt to retrieve the attesting and proposals histories from our database and
    74  	// verify nothing was saved to the DB. If there is an error in the import process, we need to make
    75  	// sure writing is an atomic operation: either the import succeeds and saves the slashing protection
    76  	// data to our DB, or it does not.
    77  	for i := 0; i < len(publicKeys); i++ {
    78  		for _, att := range attestingHistory[i] {
    79  			indexedAtt := &ethpb.IndexedAttestation{
    80  				Data: &ethpb.AttestationData{
    81  					Source: &ethpb.Checkpoint{
    82  						Epoch: att.Source,
    83  					},
    84  					Target: &ethpb.Checkpoint{
    85  						Epoch: att.Target,
    86  					},
    87  				},
    88  			}
    89  			slashingKind, err := validatorDB.CheckSlashableAttestation(ctx, publicKeys[i], [32]byte{}, indexedAtt)
    90  			// We expect we do not have an attesting history for each attestation
    91  			require.NoError(t, err)
    92  			require.Equal(t, kv.NotSlashable, slashingKind)
    93  		}
    94  		receivedHistory, err := validatorDB.ProposalHistoryForPubKey(ctx, publicKeys[i])
    95  		require.NoError(t, err)
    96  		require.DeepEqual(
    97  			t,
    98  			make([]*kv.Proposal, 0),
    99  			receivedHistory,
   100  			"Imported proposal signing root is different than the empty default",
   101  		)
   102  	}
   103  }
   104  
   105  func TestStore_ImportInterchangeData_OK(t *testing.T) {
   106  	ctx := context.Background()
   107  	numValidators := 10
   108  	publicKeys, err := valtest.CreateRandomPubKeys(numValidators)
   109  	require.NoError(t, err)
   110  	validatorDB := dbtest.SetupDB(t, publicKeys)
   111  
   112  	// First we setup some mock attesting and proposal histories and create a mock
   113  	// standard slashing protection format JSON struct.
   114  	attestingHistory, proposalHistory := valtest.MockAttestingAndProposalHistories(publicKeys)
   115  	standardProtectionFormat, err := valtest.MockSlashingProtectionJSON(publicKeys, attestingHistory, proposalHistory)
   116  	require.NoError(t, err)
   117  
   118  	// We encode the standard slashing protection struct into a JSON format.
   119  	blob, err := json.Marshal(standardProtectionFormat)
   120  	require.NoError(t, err)
   121  	buf := bytes.NewBuffer(blob)
   122  
   123  	// Next, we attempt to import it into our validator database.
   124  	err = ImportStandardProtectionJSON(ctx, validatorDB, buf)
   125  	require.NoError(t, err)
   126  
   127  	// Next, we attempt to retrieve the attesting and proposals histories from our database and
   128  	// verify those indeed match the originally generated mock histories.
   129  	for i := 0; i < len(publicKeys); i++ {
   130  		for _, att := range attestingHistory[i] {
   131  			indexedAtt := &ethpb.IndexedAttestation{
   132  				Data: &ethpb.AttestationData{
   133  					Source: &ethpb.Checkpoint{
   134  						Epoch: att.Source,
   135  					},
   136  					Target: &ethpb.Checkpoint{
   137  						Epoch: att.Target,
   138  					},
   139  				},
   140  			}
   141  			slashingKind, err := validatorDB.CheckSlashableAttestation(ctx, publicKeys[i], [32]byte{}, indexedAtt)
   142  			// We expect we have an attesting history for the attestation and when
   143  			// attempting to verify the same att is slashable with a different signing root,
   144  			// we expect to receive a double vote slashing kind.
   145  			require.NotNil(t, err)
   146  			require.Equal(t, kv.DoubleVote, slashingKind)
   147  		}
   148  
   149  		proposals := proposalHistory[i].Proposals
   150  
   151  		receivedProposalHistory, err := validatorDB.ProposalHistoryForPubKey(ctx, publicKeys[i])
   152  		require.NoError(t, err)
   153  		rootsBySlot := make(map[types.Slot][]byte)
   154  		for _, proposal := range receivedProposalHistory {
   155  			rootsBySlot[proposal.Slot] = proposal.SigningRoot
   156  		}
   157  		for _, proposal := range proposals {
   158  			receivedRoot, ok := rootsBySlot[proposal.Slot]
   159  			require.DeepEqual(t, true, ok)
   160  			require.DeepEqual(
   161  				t,
   162  				receivedRoot,
   163  				proposal.SigningRoot,
   164  				"Imported proposals are different then the generated ones",
   165  			)
   166  		}
   167  	}
   168  }
   169  
   170  func Test_validateMetadata(t *testing.T) {
   171  	goodRoot := [32]byte{1}
   172  	goodStr := make([]byte, hex.EncodedLen(len(goodRoot)))
   173  	hex.Encode(goodStr, goodRoot[:])
   174  	tests := []struct {
   175  		name                   string
   176  		interchangeJSON        *format.EIPSlashingProtectionFormat
   177  		dbGenesisValidatorRoot []byte
   178  		wantErr                bool
   179  		wantFatal              string
   180  	}{
   181  		{
   182  			name: "Incorrect version for EIP format should fail",
   183  			interchangeJSON: &format.EIPSlashingProtectionFormat{
   184  				Metadata: struct {
   185  					InterchangeFormatVersion string `json:"interchange_format_version"`
   186  					GenesisValidatorsRoot    string `json:"genesis_validators_root"`
   187  				}{
   188  					InterchangeFormatVersion: "1",
   189  					GenesisValidatorsRoot:    string(goodStr),
   190  				},
   191  			},
   192  			wantErr: true,
   193  		},
   194  		{
   195  			name: "Junk data for version should fail",
   196  			interchangeJSON: &format.EIPSlashingProtectionFormat{
   197  				Metadata: struct {
   198  					InterchangeFormatVersion string `json:"interchange_format_version"`
   199  					GenesisValidatorsRoot    string `json:"genesis_validators_root"`
   200  				}{
   201  					InterchangeFormatVersion: "asdljas$d",
   202  					GenesisValidatorsRoot:    string(goodStr),
   203  				},
   204  			},
   205  			wantErr: true,
   206  		},
   207  		{
   208  			name: "Proper version field should pass",
   209  			interchangeJSON: &format.EIPSlashingProtectionFormat{
   210  				Metadata: struct {
   211  					InterchangeFormatVersion string `json:"interchange_format_version"`
   212  					GenesisValidatorsRoot    string `json:"genesis_validators_root"`
   213  				}{
   214  					InterchangeFormatVersion: format.InterchangeFormatVersion,
   215  					GenesisValidatorsRoot:    string(goodStr),
   216  				},
   217  			},
   218  			wantErr: false,
   219  		},
   220  	}
   221  	for _, tt := range tests {
   222  		t.Run(tt.name, func(t *testing.T) {
   223  			validatorDB := dbtest.SetupDB(t, nil)
   224  			ctx := context.Background()
   225  			if err := validateMetadata(ctx, validatorDB, tt.interchangeJSON); (err != nil) != tt.wantErr {
   226  				t.Errorf("validateMetadata() error = %v, wantErr %v", err, tt.wantErr)
   227  			}
   228  
   229  		})
   230  	}
   231  }
   232  
   233  func Test_validateMetadataGenesisValidatorRoot(t *testing.T) {
   234  	goodRoot := [32]byte{1}
   235  	goodStr := make([]byte, hex.EncodedLen(len(goodRoot)))
   236  	hex.Encode(goodStr, goodRoot[:])
   237  	secondRoot := [32]byte{2}
   238  	secondStr := make([]byte, hex.EncodedLen(len(secondRoot)))
   239  	hex.Encode(secondStr, secondRoot[:])
   240  
   241  	tests := []struct {
   242  		name                   string
   243  		interchangeJSON        *format.EIPSlashingProtectionFormat
   244  		dbGenesisValidatorRoot []byte
   245  		wantErr                bool
   246  	}{
   247  		{
   248  			name: "Same genesis roots should not fail",
   249  			interchangeJSON: &format.EIPSlashingProtectionFormat{
   250  				Metadata: struct {
   251  					InterchangeFormatVersion string `json:"interchange_format_version"`
   252  					GenesisValidatorsRoot    string `json:"genesis_validators_root"`
   253  				}{
   254  					InterchangeFormatVersion: format.InterchangeFormatVersion,
   255  					GenesisValidatorsRoot:    string(goodStr),
   256  				},
   257  			},
   258  			dbGenesisValidatorRoot: goodRoot[:],
   259  			wantErr:                false,
   260  		},
   261  		{
   262  			name: "Different genesis roots should not fail",
   263  			interchangeJSON: &format.EIPSlashingProtectionFormat{
   264  				Metadata: struct {
   265  					InterchangeFormatVersion string `json:"interchange_format_version"`
   266  					GenesisValidatorsRoot    string `json:"genesis_validators_root"`
   267  				}{
   268  					InterchangeFormatVersion: format.InterchangeFormatVersion,
   269  					GenesisValidatorsRoot:    string(secondStr),
   270  				},
   271  			},
   272  			dbGenesisValidatorRoot: goodRoot[:],
   273  			wantErr:                true,
   274  		},
   275  	}
   276  	for _, tt := range tests {
   277  		t.Run(tt.name, func(t *testing.T) {
   278  			validatorDB := dbtest.SetupDB(t, nil)
   279  			ctx := context.Background()
   280  			require.NoError(t, validatorDB.SaveGenesisValidatorsRoot(ctx, tt.dbGenesisValidatorRoot))
   281  			err := validateMetadata(ctx, validatorDB, tt.interchangeJSON)
   282  			if tt.wantErr {
   283  				require.ErrorContains(t, "genesis validator root doesnt match the one that is stored", err)
   284  			} else {
   285  				require.NoError(t, err)
   286  			}
   287  
   288  		})
   289  	}
   290  }
   291  
   292  func Test_parseUniqueSignedBlocksByPubKey(t *testing.T) {
   293  	numValidators := 4
   294  	publicKeys, err := valtest.CreateRandomPubKeys(numValidators)
   295  	require.NoError(t, err)
   296  	roots := valtest.CreateMockRoots(numValidators)
   297  	tests := []struct {
   298  		name    string
   299  		data    []*format.ProtectionData
   300  		want    map[[48]byte][]*format.SignedBlock
   301  		wantErr bool
   302  	}{
   303  		{
   304  			name: "nil values are skipped",
   305  			data: []*format.ProtectionData{
   306  				{
   307  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   308  					SignedBlocks: []*format.SignedBlock{
   309  						{
   310  							Slot:        "1",
   311  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   312  						},
   313  						nil,
   314  					},
   315  				},
   316  				{
   317  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   318  					SignedBlocks: []*format.SignedBlock{
   319  						{
   320  							Slot:        "3",
   321  							SigningRoot: fmt.Sprintf("%x", roots[2]),
   322  						},
   323  					},
   324  				},
   325  			},
   326  			want: map[[48]byte][]*format.SignedBlock{
   327  				publicKeys[0]: {
   328  					{
   329  						Slot:        "1",
   330  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   331  					},
   332  					{
   333  						Slot:        "3",
   334  						SigningRoot: fmt.Sprintf("%x", roots[2]),
   335  					},
   336  				},
   337  			},
   338  		},
   339  		{
   340  			name: "same blocks but different public keys are parsed correctly",
   341  			data: []*format.ProtectionData{
   342  				{
   343  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   344  					SignedBlocks: []*format.SignedBlock{
   345  						{
   346  							Slot:        "1",
   347  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   348  						},
   349  						{
   350  							Slot:        "2",
   351  							SigningRoot: fmt.Sprintf("%x", roots[1]),
   352  						},
   353  					},
   354  				},
   355  				{
   356  					Pubkey: fmt.Sprintf("%x", publicKeys[1]),
   357  					SignedBlocks: []*format.SignedBlock{
   358  						{
   359  							Slot:        "1",
   360  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   361  						},
   362  						{
   363  							Slot:        "2",
   364  							SigningRoot: fmt.Sprintf("%x", roots[1]),
   365  						},
   366  					},
   367  				},
   368  			},
   369  			want: map[[48]byte][]*format.SignedBlock{
   370  				publicKeys[0]: {
   371  					{
   372  						Slot:        "1",
   373  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   374  					},
   375  					{
   376  						Slot:        "2",
   377  						SigningRoot: fmt.Sprintf("%x", roots[1]),
   378  					},
   379  				},
   380  				publicKeys[1]: {
   381  					{
   382  						Slot:        "1",
   383  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   384  					},
   385  					{
   386  						Slot:        "2",
   387  						SigningRoot: fmt.Sprintf("%x", roots[1]),
   388  					},
   389  				},
   390  			},
   391  		},
   392  		{
   393  			name: "disjoint sets of signed blocks by the same public key are parsed correctly",
   394  			data: []*format.ProtectionData{
   395  				{
   396  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   397  					SignedBlocks: []*format.SignedBlock{
   398  						{
   399  							Slot:        "1",
   400  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   401  						},
   402  						{
   403  							Slot:        "2",
   404  							SigningRoot: fmt.Sprintf("%x", roots[1]),
   405  						},
   406  					},
   407  				},
   408  				{
   409  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   410  					SignedBlocks: []*format.SignedBlock{
   411  						{
   412  							Slot:        "3",
   413  							SigningRoot: fmt.Sprintf("%x", roots[2]),
   414  						},
   415  					},
   416  				},
   417  			},
   418  			want: map[[48]byte][]*format.SignedBlock{
   419  				publicKeys[0]: {
   420  					{
   421  						Slot:        "1",
   422  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   423  					},
   424  					{
   425  						Slot:        "2",
   426  						SigningRoot: fmt.Sprintf("%x", roots[1]),
   427  					},
   428  					{
   429  						Slot:        "3",
   430  						SigningRoot: fmt.Sprintf("%x", roots[2]),
   431  					},
   432  				},
   433  			},
   434  		},
   435  		{
   436  			name: "full duplicate entries are uniquely parsed",
   437  			data: []*format.ProtectionData{
   438  				{
   439  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   440  					SignedBlocks: []*format.SignedBlock{
   441  						{
   442  							Slot:        "1",
   443  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   444  						},
   445  					},
   446  				},
   447  				{
   448  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   449  					SignedBlocks: []*format.SignedBlock{
   450  						{
   451  							Slot:        "1",
   452  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   453  						},
   454  					},
   455  				},
   456  			},
   457  			want: map[[48]byte][]*format.SignedBlock{
   458  				publicKeys[0]: {
   459  					{
   460  						Slot:        "1",
   461  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   462  					},
   463  					{
   464  						Slot:        "1",
   465  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   466  					},
   467  				},
   468  			},
   469  		},
   470  		{
   471  			name: "intersecting duplicate public key entries are handled properly",
   472  			data: []*format.ProtectionData{
   473  				{
   474  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   475  					SignedBlocks: []*format.SignedBlock{
   476  						{
   477  							Slot:        "1",
   478  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   479  						},
   480  						{
   481  							Slot:        "2",
   482  							SigningRoot: fmt.Sprintf("%x", roots[1]),
   483  						},
   484  					},
   485  				},
   486  				{
   487  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   488  					SignedBlocks: []*format.SignedBlock{
   489  						{
   490  							Slot:        "2",
   491  							SigningRoot: fmt.Sprintf("%x", roots[1]),
   492  						},
   493  						{
   494  							Slot:        "3",
   495  							SigningRoot: fmt.Sprintf("%x", roots[2]),
   496  						},
   497  					},
   498  				},
   499  			},
   500  			want: map[[48]byte][]*format.SignedBlock{
   501  				publicKeys[0]: {
   502  					{
   503  						Slot:        "1",
   504  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   505  					},
   506  					{
   507  						Slot:        "2",
   508  						SigningRoot: fmt.Sprintf("%x", roots[1]),
   509  					},
   510  					{
   511  						Slot:        "2",
   512  						SigningRoot: fmt.Sprintf("%x", roots[1]),
   513  					},
   514  					{
   515  						Slot:        "3",
   516  						SigningRoot: fmt.Sprintf("%x", roots[2]),
   517  					},
   518  				},
   519  			},
   520  		},
   521  	}
   522  	for _, tt := range tests {
   523  		t.Run(tt.name, func(t *testing.T) {
   524  			got, err := parseBlocksForUniquePublicKeys(tt.data)
   525  			if (err != nil) != tt.wantErr {
   526  				t.Errorf("parseBlocksForUniquePublicKeys() error = %v, wantErr %v", err, tt.wantErr)
   527  				return
   528  			}
   529  			if !reflect.DeepEqual(got, tt.want) {
   530  				t.Errorf("parseBlocksForUniquePublicKeys() got = %v, want %v", got, tt.want)
   531  			}
   532  		})
   533  	}
   534  }
   535  
   536  func Test_parseUniqueSignedAttestationsByPubKey(t *testing.T) {
   537  	numValidators := 4
   538  	publicKeys, err := valtest.CreateRandomPubKeys(numValidators)
   539  	require.NoError(t, err)
   540  	roots := valtest.CreateMockRoots(numValidators)
   541  	tests := []struct {
   542  		name    string
   543  		data    []*format.ProtectionData
   544  		want    map[[48]byte][]*format.SignedAttestation
   545  		wantErr bool
   546  	}{
   547  		{
   548  			name: "nil values are skipped",
   549  			data: []*format.ProtectionData{
   550  				{
   551  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   552  					SignedAttestations: []*format.SignedAttestation{
   553  						{
   554  							SourceEpoch: "1",
   555  							TargetEpoch: "3",
   556  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   557  						},
   558  						nil,
   559  					},
   560  				},
   561  				{
   562  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   563  					SignedAttestations: []*format.SignedAttestation{
   564  						{
   565  							SourceEpoch: "3",
   566  							TargetEpoch: "5",
   567  							SigningRoot: fmt.Sprintf("%x", roots[2]),
   568  						},
   569  					},
   570  				},
   571  			},
   572  			want: map[[48]byte][]*format.SignedAttestation{
   573  				publicKeys[0]: {
   574  					{
   575  						SourceEpoch: "1",
   576  						TargetEpoch: "3",
   577  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   578  					},
   579  					{
   580  						SourceEpoch: "3",
   581  						TargetEpoch: "5",
   582  						SigningRoot: fmt.Sprintf("%x", roots[2]),
   583  					},
   584  				},
   585  			},
   586  		},
   587  		{
   588  			name: "same attestations but different public keys are parsed correctly",
   589  			data: []*format.ProtectionData{
   590  				{
   591  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   592  					SignedAttestations: []*format.SignedAttestation{
   593  						{
   594  							SourceEpoch: "1",
   595  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   596  						},
   597  						{
   598  							SourceEpoch: "2",
   599  							SigningRoot: fmt.Sprintf("%x", roots[1]),
   600  						},
   601  					},
   602  				},
   603  				{
   604  					Pubkey: fmt.Sprintf("%x", publicKeys[1]),
   605  					SignedAttestations: []*format.SignedAttestation{
   606  						{
   607  							SourceEpoch: "1",
   608  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   609  						},
   610  						{
   611  							SourceEpoch: "2",
   612  							SigningRoot: fmt.Sprintf("%x", roots[1]),
   613  						},
   614  					},
   615  				},
   616  			},
   617  			want: map[[48]byte][]*format.SignedAttestation{
   618  				publicKeys[0]: {
   619  					{
   620  						SourceEpoch: "1",
   621  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   622  					},
   623  					{
   624  						SourceEpoch: "2",
   625  						SigningRoot: fmt.Sprintf("%x", roots[1]),
   626  					},
   627  				},
   628  				publicKeys[1]: {
   629  					{
   630  						SourceEpoch: "1",
   631  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   632  					},
   633  					{
   634  						SourceEpoch: "2",
   635  						SigningRoot: fmt.Sprintf("%x", roots[1]),
   636  					},
   637  				},
   638  			},
   639  		},
   640  		{
   641  			name: "disjoint sets of signed attestations by the same public key are parsed correctly",
   642  			data: []*format.ProtectionData{
   643  				{
   644  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   645  					SignedAttestations: []*format.SignedAttestation{
   646  						{
   647  							SourceEpoch: "1",
   648  							TargetEpoch: "3",
   649  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   650  						},
   651  						{
   652  							SourceEpoch: "2",
   653  							TargetEpoch: "4",
   654  							SigningRoot: fmt.Sprintf("%x", roots[1]),
   655  						},
   656  					},
   657  				},
   658  				{
   659  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   660  					SignedAttestations: []*format.SignedAttestation{
   661  						{
   662  							SourceEpoch: "3",
   663  							TargetEpoch: "5",
   664  							SigningRoot: fmt.Sprintf("%x", roots[2]),
   665  						},
   666  					},
   667  				},
   668  			},
   669  			want: map[[48]byte][]*format.SignedAttestation{
   670  				publicKeys[0]: {
   671  					{
   672  						SourceEpoch: "1",
   673  						TargetEpoch: "3",
   674  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   675  					},
   676  					{
   677  						SourceEpoch: "2",
   678  						TargetEpoch: "4",
   679  						SigningRoot: fmt.Sprintf("%x", roots[1]),
   680  					},
   681  					{
   682  						SourceEpoch: "3",
   683  						TargetEpoch: "5",
   684  						SigningRoot: fmt.Sprintf("%x", roots[2]),
   685  					},
   686  				},
   687  			},
   688  		},
   689  		{
   690  			name: "full duplicate entries are uniquely parsed",
   691  			data: []*format.ProtectionData{
   692  				{
   693  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   694  					SignedAttestations: []*format.SignedAttestation{
   695  						{
   696  							SourceEpoch: "1",
   697  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   698  						},
   699  					},
   700  				},
   701  				{
   702  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   703  					SignedAttestations: []*format.SignedAttestation{
   704  						{
   705  							SourceEpoch: "1",
   706  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   707  						},
   708  					},
   709  				},
   710  			},
   711  			want: map[[48]byte][]*format.SignedAttestation{
   712  				publicKeys[0]: {
   713  					{
   714  						SourceEpoch: "1",
   715  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   716  					},
   717  					{
   718  						SourceEpoch: "1",
   719  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   720  					},
   721  				},
   722  			},
   723  		},
   724  		{
   725  			name: "intersecting duplicate public key entries are handled properly",
   726  			data: []*format.ProtectionData{
   727  				{
   728  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   729  					SignedAttestations: []*format.SignedAttestation{
   730  						{
   731  							SourceEpoch: "1",
   732  							SigningRoot: fmt.Sprintf("%x", roots[0]),
   733  						},
   734  						{
   735  							SourceEpoch: "2",
   736  							SigningRoot: fmt.Sprintf("%x", roots[1]),
   737  						},
   738  					},
   739  				},
   740  				{
   741  					Pubkey: fmt.Sprintf("%x", publicKeys[0]),
   742  					SignedAttestations: []*format.SignedAttestation{
   743  						{
   744  							SourceEpoch: "2",
   745  							SigningRoot: fmt.Sprintf("%x", roots[1]),
   746  						},
   747  						{
   748  							SourceEpoch: "3",
   749  							SigningRoot: fmt.Sprintf("%x", roots[2]),
   750  						},
   751  					},
   752  				},
   753  			},
   754  			want: map[[48]byte][]*format.SignedAttestation{
   755  				publicKeys[0]: {
   756  					{
   757  						SourceEpoch: "1",
   758  						SigningRoot: fmt.Sprintf("%x", roots[0]),
   759  					},
   760  					{
   761  						SourceEpoch: "2",
   762  						SigningRoot: fmt.Sprintf("%x", roots[1]),
   763  					},
   764  					{
   765  						SourceEpoch: "2",
   766  						SigningRoot: fmt.Sprintf("%x", roots[1]),
   767  					},
   768  					{
   769  						SourceEpoch: "3",
   770  						SigningRoot: fmt.Sprintf("%x", roots[2]),
   771  					},
   772  				},
   773  			},
   774  		},
   775  	}
   776  	for _, tt := range tests {
   777  		t.Run(tt.name, func(t *testing.T) {
   778  			got, err := parseAttestationsForUniquePublicKeys(tt.data)
   779  			if (err != nil) != tt.wantErr {
   780  				t.Errorf("parseAttestationsForUniquePublicKeys() error = %v, wantErr %v", err, tt.wantErr)
   781  				return
   782  			}
   783  			if !reflect.DeepEqual(got, tt.want) {
   784  				t.Errorf("parseAttestationsForUniquePublicKeys() got = %v, want %v", got, tt.want)
   785  			}
   786  		})
   787  	}
   788  }
   789  
   790  func Test_filterSlashablePubKeysFromBlocks(t *testing.T) {
   791  	var tests = []struct {
   792  		name     string
   793  		expected [][48]byte
   794  		given    map[[48]byte][]*format.SignedBlock
   795  	}{
   796  		{
   797  			name:     "No slashable keys returns empty",
   798  			expected: make([][48]byte, 0),
   799  			given: map[[48]byte][]*format.SignedBlock{
   800  				{1}: {
   801  					{
   802  						Slot: "1",
   803  					},
   804  					{
   805  						Slot: "2",
   806  					},
   807  				},
   808  				{2}: {
   809  					{
   810  						Slot: "2",
   811  					},
   812  					{
   813  						Slot: "3",
   814  					},
   815  				},
   816  			},
   817  		},
   818  		{
   819  			name:     "Empty data returns empty",
   820  			expected: make([][48]byte, 0),
   821  			given:    make(map[[48]byte][]*format.SignedBlock),
   822  		},
   823  		{
   824  			name: "Properly finds public keys with slashable data",
   825  			expected: [][48]byte{
   826  				{1}, {3},
   827  			},
   828  			given: map[[48]byte][]*format.SignedBlock{
   829  				{1}: {
   830  					{
   831  						Slot: "1",
   832  					},
   833  					{
   834  						Slot: "1",
   835  					},
   836  					{
   837  						Slot: "2",
   838  					},
   839  				},
   840  				{2}: {
   841  					{
   842  						Slot: "2",
   843  					},
   844  					{
   845  						Slot: "3",
   846  					},
   847  				},
   848  				{3}: {
   849  					{
   850  						Slot: "3",
   851  					},
   852  					{
   853  						Slot: "3",
   854  					},
   855  				},
   856  			},
   857  		},
   858  		{
   859  			name: "Considers nil signing roots and mismatched signing roots when determining slashable keys",
   860  			expected: [][48]byte{
   861  				{2}, {3},
   862  			},
   863  			given: map[[48]byte][]*format.SignedBlock{
   864  				// Different signing roots and same slot should not be slashable.
   865  				{1}: {
   866  					{
   867  						Slot:        "1",
   868  						SigningRoot: fmt.Sprintf("%#x", [32]byte{1}),
   869  					},
   870  					{
   871  						Slot:        "1",
   872  						SigningRoot: fmt.Sprintf("%#x", [32]byte{1}),
   873  					},
   874  				},
   875  				// No signing root specified but same slot should be slashable.
   876  				{2}: {
   877  					{
   878  						Slot: "2",
   879  					},
   880  					{
   881  						Slot: "2",
   882  					},
   883  				},
   884  				// No signing root in one slot, and same slot with signing root should be slashable.
   885  				{3}: {
   886  					{
   887  						Slot: "3",
   888  					},
   889  					{
   890  						Slot:        "3",
   891  						SigningRoot: fmt.Sprintf("%#x", [32]byte{3}),
   892  					},
   893  				},
   894  			},
   895  		},
   896  	}
   897  	for _, tt := range tests {
   898  		tt := tt
   899  		t.Run(tt.name, func(t *testing.T) {
   900  			ctx := context.Background()
   901  			historyByPubKey := make(map[[48]byte]kv.ProposalHistoryForPubkey)
   902  			for pubKey, signedBlocks := range tt.given {
   903  				proposalHistory, err := transformSignedBlocks(ctx, signedBlocks)
   904  				require.NoError(t, err)
   905  				historyByPubKey[pubKey] = *proposalHistory
   906  			}
   907  			slashablePubKeys := filterSlashablePubKeysFromBlocks(context.Background(), historyByPubKey)
   908  			wantedPubKeys := make(map[[48]byte]bool)
   909  			for _, pk := range tt.expected {
   910  				wantedPubKeys[pk] = true
   911  				wantedPubKeys[pk] = true
   912  			}
   913  			for _, pk := range slashablePubKeys {
   914  				ok := wantedPubKeys[pk]
   915  				require.Equal(t, true, ok)
   916  			}
   917  		})
   918  	}
   919  }
   920  
   921  func Test_filterSlashablePubKeysFromAttestations(t *testing.T) {
   922  	ctx := context.Background()
   923  	tests := []struct {
   924  		name                 string
   925  		previousAttsByPubKey map[[48]byte][]*format.SignedAttestation
   926  		incomingAttsByPubKey map[[48]byte][]*format.SignedAttestation
   927  		want                 map[[48]byte]bool
   928  		wantErr              bool
   929  	}{
   930  		{
   931  			name: "Properly filters out double voting attester keys",
   932  			previousAttsByPubKey: map[[48]byte][]*format.SignedAttestation{
   933  				{1}: {
   934  					{
   935  						SourceEpoch: "2",
   936  						TargetEpoch: "4",
   937  					},
   938  					{
   939  						SourceEpoch: "2",
   940  						TargetEpoch: "4",
   941  					},
   942  				},
   943  				{2}: {
   944  					{
   945  						SourceEpoch: "2",
   946  						TargetEpoch: "4",
   947  					},
   948  					{
   949  						SourceEpoch: "2",
   950  						TargetEpoch: "5",
   951  					},
   952  				},
   953  				{3}: {
   954  					{
   955  						SourceEpoch: "2",
   956  						TargetEpoch: "4",
   957  					},
   958  					{
   959  						SourceEpoch: "2",
   960  						TargetEpoch: "4",
   961  					},
   962  				},
   963  			},
   964  			want: map[[48]byte]bool{
   965  				{1}: true,
   966  				{3}: true,
   967  			},
   968  		},
   969  		{
   970  			name: "Returns empty if no keys are slashable",
   971  			previousAttsByPubKey: map[[48]byte][]*format.SignedAttestation{
   972  				{1}: {
   973  					{
   974  						SourceEpoch: "2",
   975  						TargetEpoch: "4",
   976  					},
   977  				},
   978  				{2}: {
   979  					{
   980  						SourceEpoch: "2",
   981  						TargetEpoch: "4",
   982  					},
   983  					{
   984  						SourceEpoch: "2",
   985  						TargetEpoch: "5",
   986  					},
   987  				},
   988  				{3}: {
   989  					{
   990  						SourceEpoch: "2",
   991  						TargetEpoch: "4",
   992  					},
   993  					{
   994  						SourceEpoch: "3",
   995  						TargetEpoch: "6",
   996  					},
   997  				},
   998  			},
   999  			want: map[[48]byte]bool{},
  1000  		},
  1001  		{
  1002  			name: "Properly filters out surround voting attester keys",
  1003  			previousAttsByPubKey: map[[48]byte][]*format.SignedAttestation{
  1004  				{1}: {
  1005  					{
  1006  						SourceEpoch: "2",
  1007  						TargetEpoch: "4",
  1008  					},
  1009  					{
  1010  						SourceEpoch: "1",
  1011  						TargetEpoch: "5",
  1012  					},
  1013  				},
  1014  				{2}: {
  1015  					{
  1016  						SourceEpoch: "2",
  1017  						TargetEpoch: "4",
  1018  					},
  1019  					{
  1020  						SourceEpoch: "2",
  1021  						TargetEpoch: "5",
  1022  					},
  1023  				},
  1024  				{3}: {
  1025  					{
  1026  						SourceEpoch: "2",
  1027  						TargetEpoch: "5",
  1028  					},
  1029  					{
  1030  						SourceEpoch: "3",
  1031  						TargetEpoch: "4",
  1032  					},
  1033  				},
  1034  			},
  1035  			want: map[[48]byte]bool{
  1036  				{1}: true,
  1037  				{3}: true,
  1038  			},
  1039  		},
  1040  	}
  1041  	for _, tt := range tests {
  1042  		t.Run(tt.name, func(t *testing.T) {
  1043  			attestingHistoriesByPubKey := make(map[[48]byte][]*kv.AttestationRecord)
  1044  			pubKeys := make([][48]byte, 0)
  1045  			for pubKey := range tt.incomingAttsByPubKey {
  1046  				pubKeys = append(pubKeys, pubKey)
  1047  			}
  1048  			validatorDB := dbtest.SetupDB(t, pubKeys)
  1049  			for pubKey, signedAtts := range tt.incomingAttsByPubKey {
  1050  				attestingHistory, err := transformSignedAttestations(pubKey, signedAtts)
  1051  				require.NoError(t, err)
  1052  				for _, att := range attestingHistory {
  1053  					indexedAtt := createAttestation(att.Source, att.Target)
  1054  					err := validatorDB.SaveAttestationForPubKey(ctx, pubKey, att.SigningRoot, indexedAtt)
  1055  					require.NoError(t, err)
  1056  				}
  1057  			}
  1058  			got, err := filterSlashablePubKeysFromAttestations(ctx, validatorDB, attestingHistoriesByPubKey)
  1059  			if (err != nil) != tt.wantErr {
  1060  				t.Errorf("filterSlashablePubKeysFromAttestations() error = %v, wantErr %v", err, tt.wantErr)
  1061  				return
  1062  			}
  1063  			for _, pubKey := range got {
  1064  				ok := tt.want[pubKey]
  1065  				assert.Equal(t, true, ok)
  1066  			}
  1067  		})
  1068  	}
  1069  }