diff --git a/shared/params/config.go b/shared/params/config.go index a32bab643b..68e4e0ac11 100644 --- a/shared/params/config.go +++ b/shared/params/config.go @@ -118,6 +118,9 @@ type BeaconChainConfig struct { WeakSubjectivityPeriod types.Epoch // WeakSubjectivityPeriod defines the time period expressed in number of epochs were proof of stake network should validate block headers and attestations for slashable events. PruneSlasherStoragePeriod types.Epoch // PruneSlasherStoragePeriod defines the time period expressed in number of epochs were proof of stake network should prune attestation and block header store. + // Slashing protection constants. + SlashingProtectionPruningEpochs types.Epoch // SlashingProtectionPruningEpochs defines a period after which all prior epochs are pruned in the validator database. + // Fork-related values. GenesisForkVersion []byte `yaml:"GENESIS_FORK_VERSION" spec:"true"` // GenesisForkVersion is used to track fork version between state transitions. NextForkVersion []byte `yaml:"NEXT_FORK_VERSION"` // NextForkVersion is used to track the upcoming fork version, if any. diff --git a/shared/params/mainnet_config.go b/shared/params/mainnet_config.go index c127dae5e3..b1383b467d 100644 --- a/shared/params/mainnet_config.go +++ b/shared/params/mainnet_config.go @@ -163,8 +163,9 @@ var mainnetBeaconConfig = &BeaconChainConfig{ BeaconStateFieldCount: 21, // Slasher related values. - WeakSubjectivityPeriod: 54000, - PruneSlasherStoragePeriod: 10, + WeakSubjectivityPeriod: 54000, + PruneSlasherStoragePeriod: 10, + SlashingProtectionPruningEpochs: 512, // Weak subjectivity values. SafetyDecay: 10, diff --git a/validator/db/kv/db.go b/validator/db/kv/db.go index a288b8aca0..fe75ffb66d 100644 --- a/validator/db/kv/db.go +++ b/validator/db/kv/db.go @@ -161,7 +161,7 @@ func NewKVStore(ctx context.Context, dirPath string, config *Config) (*Store, er } // Prune attesting records older than the current weak subjectivity period. - if err := kv.PruneAttestationsOlderThanCurrentWeakSubjectivity(ctx); err != nil { + if err := kv.PruneAttestations(ctx); err != nil { return nil, errors.Wrap(err, "could not prune old attestations from DB") } diff --git a/validator/db/kv/prune_attester_protection.go b/validator/db/kv/prune_attester_protection.go index 925e288a92..c556f35c2a 100644 --- a/validator/db/kv/prune_attester_protection.go +++ b/validator/db/kv/prune_attester_protection.go @@ -10,12 +10,12 @@ import ( "go.opencensus.io/trace" ) -// PruneAttestationsOlderThanCurrentWeakSubjectivity loops through every -// public key in the public keys bucket and prunes all attestation data -// that has target epochs older than the highest weak subjectivity period -// in our database. This routine is meant to run on startup. -func (s *Store) PruneAttestationsOlderThanCurrentWeakSubjectivity(ctx context.Context) error { - ctx, span := trace.StartSpan(ctx, "Validator.PruneAttestationsOlderThanCurrentWeakSubjectivity") +// PruneAttestations loops through every public key in the public keys bucket +// and prunes all attestation data that has target epochs older the highest +// target epoch minus some constant of how many epochs we keep track of for slashing +// protection. This routine is meant to run on startup. +func (s *Store) PruneAttestations(ctx context.Context) error { + ctx, span := trace.StartSpan(ctx, "Validator.PruneAttestations") defer span.End() return s.update(func(tx *bolt.Tx) error { bucket := tx.Bucket(pubKeysBucket) @@ -36,7 +36,6 @@ func (s *Store) PruneAttestationsOlderThanCurrentWeakSubjectivity(ctx context.Co } func pruneSourceEpochsBucket(bucket *bolt.Bucket) error { - wssPeriod := params.BeaconConfig().WeakSubjectivityPeriod sourceEpochsBucket := bucket.Bucket(attestationSourceEpochsBucket) if sourceEpochsBucket == nil { return nil @@ -47,20 +46,9 @@ func pruneSourceEpochsBucket(bucket *bolt.Bucket) error { highestTargetEpochBytes := sourceEpochsBucket.Get(highestSourceEpochBytes) highestTargetEpoch := bytesutil.BytesToEpochBigEndian(highestTargetEpochBytes) - // No need to prune if the highest epoch we've written is still - // before the first weak subjectivity period. - if highestTargetEpoch < wssPeriod { - return nil - } - return sourceEpochsBucket.ForEach(func(k []byte, v []byte) error { targetEpoch := bytesutil.BytesToEpochBigEndian(v) - - // For each source epoch we find, we check - // if its associated target epoch is less than the weak - // subjectivity period of the highest written target epoch - // in the bucket and delete if so. - if olderThanCurrentWeakSubjectivityPeriod(targetEpoch, highestTargetEpoch) { + if targetEpoch < pruningEpochCutoff(highestTargetEpoch) { return sourceEpochsBucket.Delete(k) } return nil @@ -68,7 +56,6 @@ func pruneSourceEpochsBucket(bucket *bolt.Bucket) error { } func pruneTargetEpochsBucket(bucket *bolt.Bucket) error { - wssPeriod := params.BeaconConfig().WeakSubjectivityPeriod targetEpochsBucket := bucket.Bucket(attestationTargetEpochsBucket) if targetEpochsBucket == nil { return nil @@ -77,23 +64,16 @@ func pruneTargetEpochsBucket(bucket *bolt.Bucket) error { highestTargetEpochBytes, _ := targetEpochsBucket.Cursor().Last() highestTargetEpoch := bytesutil.BytesToEpochBigEndian(highestTargetEpochBytes) - // No need to prune if the highest epoch we've written is still - // before the first weak subjectivity period. - if highestTargetEpoch < wssPeriod { - return nil - } - c := targetEpochsBucket.Cursor() - for k, _ := c.First(); k != nil; k, _ = c.Next() { + return targetEpochsBucket.ForEach(func(k []byte, v []byte) error { targetEpoch := bytesutil.BytesToEpochBigEndian(k) - if olderThanCurrentWeakSubjectivityPeriod(targetEpoch, highestTargetEpoch) { + if targetEpoch < pruningEpochCutoff(highestTargetEpoch) { return targetEpochsBucket.Delete(k) } - } - return nil + return nil + }) } func pruneSigningRootsBucket(bucket *bolt.Bucket) error { - wssPeriod := params.BeaconConfig().WeakSubjectivityPeriod signingRootsBucket := bucket.Bucket(attestationSigningRootsBucket) if signingRootsBucket == nil { return nil @@ -103,31 +83,23 @@ func pruneSigningRootsBucket(bucket *bolt.Bucket) error { highestTargetEpochBytes, _ := signingRootsBucket.Cursor().Last() highestTargetEpoch := bytesutil.BytesToEpochBigEndian(highestTargetEpochBytes) - // No need to prune if the highest epoch we've written is still - // before the first weak subjectivity period. - if highestTargetEpoch < wssPeriod { - return nil - } - return signingRootsBucket.ForEach(func(k []byte, v []byte) error { targetEpoch := bytesutil.BytesToEpochBigEndian(k) - // For each target epoch we find in the bucket, we check - // if it less than the weak subjectivity period of the - // highest written target epoch in the bucket and delete if so. - if olderThanCurrentWeakSubjectivityPeriod(targetEpoch, highestTargetEpoch) { + if targetEpoch < pruningEpochCutoff(highestTargetEpoch) { return signingRootsBucket.Delete(k) } return nil }) } -func olderThanCurrentWeakSubjectivityPeriod(epoch, highestEpoch types.Epoch) bool { - wssPeriod := params.BeaconConfig().WeakSubjectivityPeriod - // Number of weak subjectivity periods that have passed. - currentWeakSubjectivityPeriod := highestEpoch / wssPeriod - // We check if either the epoch is less than WEAK_SUBJECTIVITY_PERIOD - // or is it is from a weak subjectivity period older than the current one, - // for example, if 5 weak subjectivity periods have passed and epoch is - // from 2 weak subjectivity periods ago, then we return true. - return epoch < wssPeriod || (epoch/wssPeriod) < currentWeakSubjectivityPeriod +// This helper function determines the cutoff epoch where, for all epochs before it, we should prune +// the slashing protection database. This is computed by taking in an epoch and subtracting +// SLASHING_PROTECTION_PRUNING_EPOCHS from the value. For example, if we are keeping track of 512 epochs +// in the database, if we pass in epoch 612, then we want to prune all epochs before epoch 100. +func pruningEpochCutoff(epoch types.Epoch) types.Epoch { + minEpoch := types.Epoch(0) + if epoch > params.BeaconConfig().SlashingProtectionPruningEpochs { + minEpoch = epoch - params.BeaconConfig().SlashingProtectionPruningEpochs + } + return minEpoch } diff --git a/validator/db/kv/prune_attester_protection_test.go b/validator/db/kv/prune_attester_protection_test.go index f565670750..3f244a998e 100644 --- a/validator/db/kv/prune_attester_protection_test.go +++ b/validator/db/kv/prune_attester_protection_test.go @@ -12,168 +12,70 @@ import ( bolt "go.etcd.io/bbolt" ) -func TestPruneAttestationsOlderThanCurrentWeakSubjectivity_BeforeWeakSubjectivity_NoPruning(t *testing.T) { - numEpochs := params.BeaconConfig().WeakSubjectivityPeriod +func TestPruneAttestations_NoPruning(t *testing.T) { pubKey := [48]byte{1} validatorDB := setupDB(t, [][48]byte{pubKey}) // Write attesting history for every single epoch - // since genesis to WEAK_SUBJECTIVITY_PERIOD. + // since genesis to a specified number of epochs. + numEpochs := params.BeaconConfig().SlashingProtectionPruningEpochs - 1 err := setupAttestationsForEveryEpoch(t, validatorDB, pubKey, numEpochs) require.NoError(t, err) // Next, attempt to prune and realize that we still have all epochs intact - // because the highest epoch we have written is still within the - // weak subjectivity period. - err = validatorDB.PruneAttestationsOlderThanCurrentWeakSubjectivity(context.Background()) + err = validatorDB.PruneAttestations(context.Background()) require.NoError(t, err) + startEpoch := types.Epoch(0) err = checkAttestingHistoryAfterPruning( - t, validatorDB, pubKey, numEpochs, false, /* should be pruned */ + t, + validatorDB, + pubKey, + startEpoch, + numEpochs, + false, /* should be pruned */ ) require.NoError(t, err) } -func TestPruneAttestationsOlderThanCurrentWeakSubjectivity_AfterFirstWeakSubjectivity(t *testing.T) { - numEpochs := params.BeaconConfig().WeakSubjectivityPeriod +func TestPruneAttestations_OK(t *testing.T) { pubKey := [48]byte{1} validatorDB := setupDB(t, [][48]byte{pubKey}) // Write attesting history for every single epoch - // since genesis to WEAK_SUBJECTIVITY_PERIOD. + // since genesis to SLASHING_PROTECTION_PRUNING_EPOCHS * 2. + numEpochs := params.BeaconConfig().SlashingProtectionPruningEpochs * 2 err := setupAttestationsForEveryEpoch(t, validatorDB, pubKey, numEpochs) require.NoError(t, err) - // Save a single attestation for WEAK_SUBJECTIVITY_PERIOD+1 - err = validatorDB.update(func(tx *bolt.Tx) error { - bucket := tx.Bucket(pubKeysBucket) - pkBucket := bucket.Bucket(pubKey[:]) - sourceEpochsBkt := pkBucket.Bucket(attestationSourceEpochsBucket) - signingRootsBkt := pkBucket.Bucket(attestationSigningRootsBucket) - targetEpochBytes := bytesutil.EpochToBytesBigEndian(numEpochs + 1) - sourceEpochBytes := bytesutil.EpochToBytesBigEndian(numEpochs) - if err := sourceEpochsBkt.Put(sourceEpochBytes, targetEpochBytes); err != nil { - return err - } - - var signingRoot [32]byte - copy(signingRoot[:], fmt.Sprintf("%d", targetEpochBytes)) - return signingRootsBkt.Put(targetEpochBytes, signingRoot[:]) - }) + err = validatorDB.PruneAttestations(context.Background()) require.NoError(t, err) - err = validatorDB.PruneAttestationsOlderThanCurrentWeakSubjectivity(context.Background()) - require.NoError(t, err) - - // Next, attempt to prune and realize that we pruned everything except for - // a signing root at target = WEAK_SUBJECTIVITY_PERIOD + 1. + // Next, verify that we pruned every epoch + // from genesis to SLASHING_PROTECTION_PRUNING_EPOCHS - 1. + startEpoch := types.Epoch(0) err = checkAttestingHistoryAfterPruning( - t, validatorDB, pubKey, numEpochs, true, /* should be pruned */ + t, + validatorDB, + pubKey, + startEpoch, + params.BeaconConfig().SlashingProtectionPruningEpochs-1, + true, /* should be pruned */ ) require.NoError(t, err) - err = validatorDB.view(func(tx *bolt.Tx) error { - bucket := tx.Bucket(pubKeysBucket) - pkBucket := bucket.Bucket(pubKey[:]) - sourceEpochsBkt := pkBucket.Bucket(attestationSourceEpochsBucket) - signingRootsBkt := pkBucket.Bucket(attestationSigningRootsBucket) - targetEpochBytes := bytesutil.EpochToBytesBigEndian(numEpochs + 1) - sourceEpochBytes := bytesutil.EpochToBytesBigEndian(numEpochs) - - storedTargetEpoch := sourceEpochsBkt.Get(sourceEpochBytes) - require.DeepEqual(t, numEpochs+1, bytesutil.BytesToEpochBigEndian(storedTargetEpoch)) - - var expectedSigningRoot [32]byte - copy(expectedSigningRoot[:], fmt.Sprintf("%d", targetEpochBytes)) - signingRoot := signingRootsBkt.Get(targetEpochBytes) - - // Expect the correct signing root at WEAK_SUBJECTIVITY_PERIOD + 1. - require.DeepEqual(t, expectedSigningRoot[:], signingRoot) - return nil - }) - require.NoError(t, err) -} - -func TestPruneAttestationsOlderThanCurrentWeakSubjectivity_AfterMultipleWeakSubjectivity(t *testing.T) { - numWeakSubjectivityPeriods := types.Epoch(5) - pubKeys := [][48]byte{{1}} - validatorDB := setupDB(t, pubKeys) - - // Write signing roots for epochs within multiples of WEAK_SUBJECTIVITY_PERIOD. - err := validatorDB.update(func(tx *bolt.Tx) error { - bucket := tx.Bucket(pubKeysBucket) - pkBucket, err := bucket.CreateBucketIfNotExists(pubKeys[0][:]) - if err != nil { - return err - } - sourceEpochsBkt, err := pkBucket.CreateBucketIfNotExists(attestationSourceEpochsBucket) - if err != nil { - return err - } - signingRootsBkt, err := pkBucket.CreateBucketIfNotExists(attestationSigningRootsBucket) - if err != nil { - return err - } - for i := types.Epoch(1); i <= numWeakSubjectivityPeriods; i++ { - targetEpoch := (i * params.BeaconConfig().WeakSubjectivityPeriod) + 1 - targetEpochBytes := bytesutil.EpochToBytesBigEndian(targetEpoch) - sourceEpoch := targetEpoch - 1 - sourceEpochBytes := bytesutil.EpochToBytesBigEndian(sourceEpoch) - if err := sourceEpochsBkt.Put(sourceEpochBytes, targetEpochBytes); err != nil { - return err - } - - var signingRoot [32]byte - copy(signingRoot[:], fmt.Sprintf("%d", targetEpochBytes)) - if err := signingRootsBkt.Put(targetEpochBytes, signingRoot[:]); err != nil { - return err - } - } - return nil - }) - require.NoError(t, err) - - err = validatorDB.PruneAttestationsOlderThanCurrentWeakSubjectivity(context.Background()) - require.NoError(t, err) - - // Next, attempt to prune and realize that we pruned everything except for - // a signing root in an epoch within the highest WEAK_SUBJECTIVITY_PERIOD - // multiple we wrote earlier in our test. - err = validatorDB.view(func(tx *bolt.Tx) error { - bucket := tx.Bucket(pubKeysBucket) - pkBucket := bucket.Bucket(pubKeys[0][:]) - signingRootsBkt := pkBucket.Bucket(attestationSigningRootsBucket) - sourceEpochsBkt := pkBucket.Bucket(attestationSourceEpochsBucket) - - // We check everything except for the highest weak subjectivity period - // has been pruned from the bucket. - for i := types.Epoch(1); i <= numWeakSubjectivityPeriods-1; i++ { - targetEpoch := (i * params.BeaconConfig().WeakSubjectivityPeriod) + 1 - sourceEpoch := targetEpoch - 1 - sourceEpochBytes := bytesutil.EpochToBytesBigEndian(sourceEpoch) - targetEpochBytes := bytesutil.EpochToBytesBigEndian(targetEpoch) - - storedTargetEpoch := sourceEpochsBkt.Get(sourceEpochBytes) - signingRoot := signingRootsBkt.Get(targetEpochBytes) - // We expect to have pruned all these signing roots and epochs. - require.Equal(t, true, signingRoot == nil) - require.Equal(t, true, storedTargetEpoch == nil) - } - - targetEpoch := (numWeakSubjectivityPeriods * params.BeaconConfig().WeakSubjectivityPeriod) + 1 - sourceEpoch := targetEpoch - 1 - targetEpochBytes := bytesutil.EpochToBytesBigEndian(targetEpoch) - sourceEpochBytes := bytesutil.EpochToBytesBigEndian(sourceEpoch) - var expectedSigningRoot [32]byte - copy(expectedSigningRoot[:], fmt.Sprintf("%d", targetEpochBytes)) - signingRoot := signingRootsBkt.Get(targetEpochBytes) - storedTargetEpoch := sourceEpochsBkt.Get(sourceEpochBytes) - - // Expect the correct signing root and target epoch. - require.DeepEqual(t, expectedSigningRoot[:], signingRoot) - require.DeepEqual(t, targetEpochBytes, storedTargetEpoch) - return nil - }) + // Next, verify that we pruned every epoch + // from N = SLASHING_PROTECTION_PRUNING_EPOCHS to N * 2. + startEpoch = params.BeaconConfig().SlashingProtectionPruningEpochs + endEpoch := startEpoch * 2 + err = checkAttestingHistoryAfterPruning( + t, + validatorDB, + pubKey, + startEpoch, + endEpoch, + false, /* should not be pruned */ + ) require.NoError(t, err) } @@ -194,9 +96,10 @@ func setupAttestationsForEveryEpoch(t testing.TB, validatorDB *Store, pubKey [48 if err != nil { return err } - for targetEpoch := types.Epoch(1); targetEpoch < numEpochs; targetEpoch++ { + for sourceEpoch := types.Epoch(0); sourceEpoch < numEpochs; sourceEpoch++ { + targetEpoch := sourceEpoch + 1 targetEpochBytes := bytesutil.EpochToBytesBigEndian(targetEpoch) - sourceEpochBytes := bytesutil.EpochToBytesBigEndian(targetEpoch - 1) + sourceEpochBytes := bytesutil.EpochToBytesBigEndian(sourceEpoch) // Save (source epoch, target epoch) pairs. if err := sourceEpochsBucket.Put(sourceEpochBytes, targetEpochBytes); err != nil { return err @@ -215,16 +118,22 @@ func setupAttestationsForEveryEpoch(t testing.TB, validatorDB *Store, pubKey [48 // Verifies, based on a boolean input argument, whether or not we should have // pruned all attesting history since genesis up to a specified number of epochs. func checkAttestingHistoryAfterPruning( - t testing.TB, validatorDB *Store, pubKey [48]byte, numEpochs types.Epoch, shouldBePruned bool, + t testing.TB, + validatorDB *Store, + pubKey [48]byte, + startEpoch types.Epoch, + numEpochs types.Epoch, + shouldBePruned bool, ) error { return validatorDB.view(func(tx *bolt.Tx) error { bucket := tx.Bucket(pubKeysBucket) pkBkt := bucket.Bucket(pubKey[:]) signingRootsBkt := pkBkt.Bucket(attestationSigningRootsBucket) sourceEpochsBkt := pkBkt.Bucket(attestationSourceEpochsBucket) - for targetEpoch := types.Epoch(1); targetEpoch < numEpochs; targetEpoch++ { + for sourceEpoch := startEpoch; sourceEpoch < numEpochs; sourceEpoch++ { + targetEpoch := sourceEpoch + 1 targetEpochBytes := bytesutil.EpochToBytesBigEndian(targetEpoch) - sourceEpochBytes := bytesutil.EpochToBytesBigEndian(targetEpoch - 1) + sourceEpochBytes := bytesutil.EpochToBytesBigEndian(sourceEpoch) storedTargetEpoch := sourceEpochsBkt.Get(sourceEpochBytes) signingRoot := signingRootsBkt.Get(targetEpochBytes) @@ -244,64 +153,3 @@ func checkAttestingHistoryAfterPruning( return nil }) } - -func Test_olderThanCurrentWeakSubjectivityPeriod(t *testing.T) { - wssPeriod := params.BeaconConfig().WeakSubjectivityPeriod - type args struct { - epoch types.Epoch - highestEpoch types.Epoch - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "returns true if epoch < WSS", - args: args{ - epoch: 1, - highestEpoch: wssPeriod, - }, - want: true, - }, - { - name: "returns true if epoch == WSS and 2*WSS have passed", - args: args{ - epoch: wssPeriod, - highestEpoch: 2 * wssPeriod, - }, - want: true, - }, - { - name: "returns true if epoch == 2*WSS and 3*WSS have passed", - args: args{ - epoch: 2 * wssPeriod, - highestEpoch: 3 * wssPeriod, - }, - want: true, - }, - { - name: "returns false if epoch == 3*WSS and 3*WSS have passed", - args: args{ - epoch: 3 * wssPeriod, - highestEpoch: 3 * wssPeriod, - }, - want: false, - }, - { - name: "returns false if epoch == 4*WSS and 3*WSS have passed", - args: args{ - epoch: 4 * wssPeriod, - highestEpoch: 3 * wssPeriod, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := olderThanCurrentWeakSubjectivityPeriod(tt.args.epoch, tt.args.highestEpoch); got != tt.want { - t.Errorf("olderThanCurrentWeakSubjectivityPeriod() = %v, want %v", got, tt.want) - } - }) - } -}