diff --git a/validator/client/propose.go b/validator/client/propose.go index 1473c73339..91a4100311 100644 --- a/validator/client/propose.go +++ b/validator/client/propose.go @@ -305,7 +305,18 @@ func (v *validator) getGraffiti(ctx context.Context, pubKey [48]byte) ([]byte, e return []byte(g), nil } - // When specified, a graffiti from the random list in the file take third priority. + // When specified, a graffiti from the ordered list in the file take third priority. + if v.graffitiOrderedIndex < uint64(len(v.graffitiStruct.Ordered)) { + graffiti := v.graffitiStruct.Ordered[v.graffitiOrderedIndex] + v.graffitiOrderedIndex = v.graffitiOrderedIndex + 1 + err := v.db.SaveGraffitiOrderedIndex(ctx, v.graffitiOrderedIndex) + if err != nil { + return nil, errors.Wrap(err, "failed to update graffiti ordered index") + } + return []byte(graffiti), nil + } + + // When specified, a graffiti from the random list in the file take fourth priority. if len(v.graffitiStruct.Random) != 0 { r := rand.NewGenerator() r.Seed(time.Now().Unix()) diff --git a/validator/client/propose_test.go b/validator/client/propose_test.go index 57e0e30270..cdbf4db00a 100644 --- a/validator/client/propose_test.go +++ b/validator/client/propose_test.go @@ -744,3 +744,30 @@ func TestGetGraffiti_Ok(t *testing.T) { }) } } + +func TestGetGraffitiOrdered_Ok(t *testing.T) { + pubKey := [48]byte{'a'} + valDB := testing2.SetupDB(t, [][48]byte{pubKey}) + ctrl := gomock.NewController(t) + m := &mocks{ + validatorClient: mock.NewMockBeaconNodeValidatorClient(ctrl), + } + m.validatorClient.EXPECT(). + ValidatorIndex(gomock.Any(), ðpb.ValidatorIndexRequest{PublicKey: pubKey[:]}). + Times(5). + Return(ðpb.ValidatorIndexResponse{Index: 2}, nil) + + v := &validator{ + db: valDB, + validatorClient: m.validatorClient, + graffitiStruct: &graffiti.Graffiti{ + Ordered: []string{"a", "b", "c"}, + Default: "d", + }, + } + for _, want := range [][]byte{[]byte{'a'}, []byte{'b'}, []byte{'c'}, []byte{'d'}, []byte{'d'}} { + got, err := v.getGraffiti(context.Background(), pubKey) + require.NoError(t, err) + require.DeepEqual(t, want, got) + } +} diff --git a/validator/client/service.go b/validator/client/service.go index d21a961b44..a22df2b53e 100644 --- a/validator/client/service.go +++ b/validator/client/service.go @@ -176,6 +176,12 @@ func (v *ValidatorService) Start() { slashablePublicKeys[pubKey] = true } + graffitiOrderedIndex, err := v.db.GraffitiOrderedIndex(v.ctx, v.graffitiStruct.Hash) + if err != nil { + log.Errorf("Could not read graffiti ordered index from disk: %v", err) + return + } + v.validator = &validator{ db: v.db, validatorClient: ethpb.NewBeaconNodeValidatorClient(v.conn), @@ -196,6 +202,7 @@ func (v *ValidatorService) Start() { walletInitializedFeed: v.walletInitializedFeed, blockFeed: new(event.Feed), graffitiStruct: v.graffitiStruct, + graffitiOrderedIndex: graffitiOrderedIndex, eipImportBlacklistedPublicKeys: slashablePublicKeys, logDutyCountDown: v.logDutyCountDown, } diff --git a/validator/client/validator.go b/validator/client/validator.go index 7becff0995..69f7712045 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -91,6 +91,7 @@ type validator struct { graffiti []byte voteStats voteStats graffitiStruct *graffiti.Graffiti + graffitiOrderedIndex uint64 eipImportBlacklistedPublicKeys map[[48]byte]bool } diff --git a/validator/db/iface/interface.go b/validator/db/iface/interface.go index 68ed74a17a..6c432c19dd 100644 --- a/validator/db/iface/interface.go +++ b/validator/db/iface/interface.go @@ -57,4 +57,8 @@ type ValidatorDB interface { AttestationHistoryForPubKey( ctx context.Context, pubKey [48]byte, ) ([]*kv.AttestationRecord, error) + + // Graffiti ordered index related methods + SaveGraffitiOrderedIndex(ctx context.Context, index uint64) error + GraffitiOrderedIndex(ctx context.Context, fileHash [32]byte) (uint64, error) } diff --git a/validator/db/kv/BUILD.bazel b/validator/db/kv/BUILD.bazel index 8e022b2831..e8fbe3a147 100644 --- a/validator/db/kv/BUILD.bazel +++ b/validator/db/kv/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "deprecated_attester_protection.go", "eip_blacklisted_keys.go", "genesis.go", + "graffiti.go", "log.go", "migration.go", "migration_optimal_attester_protection.go", @@ -50,6 +51,7 @@ go_test( "deprecated_attester_protection_test.go", "eip_blacklisted_keys_test.go", "genesis_test.go", + "graffiti_test.go", "migration_optimal_attester_protection_test.go", "migration_source_target_epochs_bucket_test.go", "proposer_protection_test.go", @@ -58,6 +60,7 @@ go_test( embed = [":go_default_library"], deps = [ "//shared/bytesutil:go_default_library", + "//shared/hashutil:go_default_library", "//shared/params:go_default_library", "//shared/testutil/assert:go_default_library", "//shared/testutil/require:go_default_library", diff --git a/validator/db/kv/db.go b/validator/db/kv/db.go index e45b7e5360..a288b8aca0 100644 --- a/validator/db/kv/db.go +++ b/validator/db/kv/db.go @@ -147,6 +147,7 @@ func NewKVStore(ctx context.Context, dirPath string, config *Config) (*Store, er slashablePublicKeysBucket, pubKeysBucket, migrationsBucket, + graffitiBucket, ) }); err != nil { return nil, err diff --git a/validator/db/kv/graffiti.go b/validator/db/kv/graffiti.go new file mode 100644 index 0000000000..b14d7d9140 --- /dev/null +++ b/validator/db/kv/graffiti.go @@ -0,0 +1,39 @@ +package kv + +import ( + "bytes" + "context" + + "github.com/prysmaticlabs/prysm/shared/bytesutil" + bolt "go.etcd.io/bbolt" +) + +// SaveGraffitiOrderedIndex writes the current graffiti index to the db +func (s *Store) SaveGraffitiOrderedIndex(ctx context.Context, index uint64) error { + return s.db.Update(func(tx *bolt.Tx) error { + bkt := tx.Bucket(graffitiBucket) + indexBytes := bytesutil.Uint64ToBytesBigEndian(index) + return bkt.Put(graffitiOrderedIndexKey, indexBytes) + }) +} + +// GraffitiOrderedIndex fetches the ordered index, resetting if the file hash changed +func (s *Store) GraffitiOrderedIndex(ctx context.Context, fileHash [32]byte) (uint64, error) { + orderedIndex := uint64(0) + err := s.db.Update(func(tx *bolt.Tx) error { + bkt := tx.Bucket(graffitiBucket) + dbFileHash := bkt.Get(graffitiFileHashKey) + if bytes.Equal(dbFileHash, fileHash[:]) { + indexBytes := bkt.Get(graffitiOrderedIndexKey) + orderedIndex = bytesutil.BytesToUint64BigEndian(indexBytes) + } else { + indexBytes := bytesutil.Uint64ToBytesBigEndian(0) + if err := bkt.Put(graffitiOrderedIndexKey, indexBytes); err != nil { + return err + } + return bkt.Put(graffitiFileHashKey, fileHash[:]) + } + return nil + }) + return orderedIndex, err +} diff --git a/validator/db/kv/graffiti_test.go b/validator/db/kv/graffiti_test.go new file mode 100644 index 0000000000..52c7ca9bbc --- /dev/null +++ b/validator/db/kv/graffiti_test.go @@ -0,0 +1,59 @@ +package kv + +import ( + "context" + "testing" + + "github.com/prysmaticlabs/prysm/shared/hashutil" + "github.com/prysmaticlabs/prysm/shared/testutil/require" +) + +func TestStore_GraffitiOrderedIndex_ReadAndWrite(t *testing.T) { + ctx := context.Background() + db := setupDB(t, [][48]byte{}) + tests := []struct { + name string + want uint64 + write uint64 + fileHash [32]byte + }{ + { + name: "empty then write", + want: 0, + write: 15, + fileHash: hashutil.Hash([]byte("one")), + }, + { + name: "update with same file hash", + want: 15, + write: 20, + fileHash: hashutil.Hash([]byte("one")), + }, + { + name: "continued updates", + want: 20, + write: 21, + fileHash: hashutil.Hash([]byte("one")), + }, + { + name: "reset with new file hash", + want: 0, + write: 10, + fileHash: hashutil.Hash([]byte("two")), + }, + { + name: "read with new file hash", + want: 10, + fileHash: hashutil.Hash([]byte("two")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := db.GraffitiOrderedIndex(ctx, tt.fileHash) + require.NoError(t, err) + require.DeepEqual(t, tt.want, got) + err = db.SaveGraffitiOrderedIndex(ctx, tt.write) + require.NoError(t, err) + }) + } +} diff --git a/validator/db/kv/schema.go b/validator/db/kv/schema.go index 06317dfd70..1fb1b36482 100644 --- a/validator/db/kv/schema.go +++ b/validator/db/kv/schema.go @@ -30,4 +30,11 @@ var ( // Migrations migrationsBucket = []byte("migrations") + + // Graffiti + graffitiBucket = []byte("graffiti") + + // Graffiti ordered index and hash keys + graffitiOrderedIndexKey = []byte("graffiti-ordered-index") + graffitiFileHashKey = []byte("graffiti-file-hash") ) diff --git a/validator/graffiti/BUILD.bazel b/validator/graffiti/BUILD.bazel index 4fb1d97ac2..672fb4fa05 100644 --- a/validator/graffiti/BUILD.bazel +++ b/validator/graffiti/BUILD.bazel @@ -7,6 +7,7 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/validator/graffiti", visibility = ["//validator:__subpackages__"], deps = [ + "//shared/hashutil:go_default_library", "@com_github_prysmaticlabs_eth2_types//:go_default_library", "@in_gopkg_yaml_v2//:go_default_library", ], @@ -17,6 +18,7 @@ go_test( srcs = ["parse_graffiti_test.go"], embed = [":go_default_library"], deps = [ + "//shared/hashutil:go_default_library", "//shared/testutil/require:go_default_library", "@com_github_prysmaticlabs_eth2_types//:go_default_library", ], diff --git a/validator/graffiti/parse_graffiti.go b/validator/graffiti/parse_graffiti.go index b25de39d59..5f5d703ea9 100644 --- a/validator/graffiti/parse_graffiti.go +++ b/validator/graffiti/parse_graffiti.go @@ -4,11 +4,14 @@ import ( "io/ioutil" types "github.com/prysmaticlabs/eth2-types" + "github.com/prysmaticlabs/prysm/shared/hashutil" "gopkg.in/yaml.v2" ) type Graffiti struct { + Hash [32]byte Default string `yaml:"default,omitempty"` + Ordered []string `yaml:"ordered,omitempty"` Random []string `yaml:"random,omitempty"` Specific map[types.ValidatorIndex]string `yaml:"specific,omitempty"` } @@ -23,5 +26,6 @@ func ParseGraffitiFile(f string) (*Graffiti, error) { if err := yaml.Unmarshal(yamlFile, g); err != nil { return nil, err } + g.Hash = hashutil.Hash(yamlFile) return g, nil } diff --git a/validator/graffiti/parse_graffiti_test.go b/validator/graffiti/parse_graffiti_test.go index e7d5bb4470..46c8cea1c0 100644 --- a/validator/graffiti/parse_graffiti_test.go +++ b/validator/graffiti/parse_graffiti_test.go @@ -7,6 +7,7 @@ import ( "testing" types "github.com/prysmaticlabs/eth2-types" + "github.com/prysmaticlabs/prysm/shared/hashutil" "github.com/prysmaticlabs/prysm/shared/testutil/require" ) @@ -23,13 +24,14 @@ func TestParseGraffitiFile_Default(t *testing.T) { require.NoError(t, err) wanted := &Graffiti{ + Hash: hashutil.Hash(input), Default: "Mr T was here", } require.DeepEqual(t, wanted, got) } func TestParseGraffitiFile_Random(t *testing.T) { - input := []byte(`random: + input := []byte(`random: - "Mr A was here" - "Mr B was here" - "Mr C was here"`) @@ -44,6 +46,7 @@ func TestParseGraffitiFile_Random(t *testing.T) { require.NoError(t, err) wanted := &Graffiti{ + Hash: hashutil.Hash(input), Random: []string{ "Mr A was here", "Mr B was here", @@ -53,9 +56,35 @@ func TestParseGraffitiFile_Random(t *testing.T) { require.DeepEqual(t, wanted, got) } +func TestParseGraffitiFile_Ordered(t *testing.T) { + input := []byte(`ordered: + - "Mr D was here" + - "Mr E was here" + - "Mr F was here"`) + + dirName := t.TempDir() + "somedir" + err := os.MkdirAll(dirName, os.ModePerm) + require.NoError(t, err) + someFileName := filepath.Join(dirName, "somefile.txt") + require.NoError(t, ioutil.WriteFile(someFileName, input, os.ModePerm)) + + got, err := ParseGraffitiFile(someFileName) + require.NoError(t, err) + + wanted := &Graffiti{ + Hash: hashutil.Hash(input), + Ordered: []string{ + "Mr D was here", + "Mr E was here", + "Mr F was here", + }, + } + require.DeepEqual(t, wanted, got) +} + func TestParseGraffitiFile_Validators(t *testing.T) { input := []byte(` -specific: +specific: 1234: Yolo 555: "What's up" 703727: Meow`) @@ -70,6 +99,7 @@ specific: require.NoError(t, err) wanted := &Graffiti{ + Hash: hashutil.Hash(input), Specific: map[types.ValidatorIndex]string{ 1234: "Yolo", 555: "What's up", @@ -82,12 +112,17 @@ specific: func TestParseGraffitiFile_AllFields(t *testing.T) { input := []byte(`default: "Mr T was here" -random: +random: - "Mr A was here" - "Mr B was here" - "Mr C was here" -specific: +ordered: + - "Mr D was here" + - "Mr E was here" + - "Mr F was here" + +specific: 1234: Yolo 555: "What's up" 703727: Meow`) @@ -102,12 +137,18 @@ specific: require.NoError(t, err) wanted := &Graffiti{ + Hash: hashutil.Hash(input), Default: "Mr T was here", Random: []string{ "Mr A was here", "Mr B was here", "Mr C was here", }, + Ordered: []string{ + "Mr D was here", + "Mr E was here", + "Mr F was here", + }, Specific: map[types.ValidatorIndex]string{ 1234: "Yolo", 555: "What's up",