Files
prysm/validator/db/kv/attester_protection_test.go
Manu NALEPA ef21d3adf8 Implement EIP-3076 minimal slashing protection, using a filesystem database (#13360)
* `EpochFromString`: Use already defined `Uint64FromString` function.

* `Test_uint64FromString` => `Test_FromString`

This test function tests more functions than `Uint64FromString`.

* Slashing protection history: Remove unreachable code.

The function `NewKVStore` creates, via `kv.UpdatePublicKeysBuckets`,
a new item in the `proposal-history-bucket-interchange`.

IMO there is no real reason to prefer `proposal` than `attestation`
as a prefix for this bucket, but this is the way it is done right now
and renaming the bucket will probably be backward incompatible.

An `attestedPublicKey` cannot exist without
the corresponding `proposedPublicKey`.

Thus, the `else` portion of code removed in this commit is not reachable.
We raise an error if we get there.

This is also probably the reason why the removed `else` portion was not
tested.

* `NewKVStore`: Switch items in `createBuckets`.

So the order corresponds to `schema.go`

* `slashableAttestationCheck`: Fix comments and logs.

* `ValidatorClient.db`: Use `iface.ValidatorDB`.

* BoltDB database: Implement `GraffitiFileHash`.

* Filesystem database: Creates `db.go`.

This file defines the following structs:
- `Store`
- `Graffiti`
- `Configuration`
- `ValidatorSlashingProtection`

This files implements the following public functions:
- `NewStore`
- `Close`
- `Backup`
- `DatabasePath`
- `ClearDB`
- `UpdatePublicKeysBuckets`

This files implements the following private functions:
- `slashingProtectionDirPath`
- `configurationFilePath`
- `configuration`
- `saveConfiguration`
- `validatorSlashingProtection`
- `saveValidatorSlashingProtection`
- `publicKeys`

* Filesystem database: Creates `genesis.go`.

This file defines the following public functions:
- `GenesisValidatorsRoot`
- `SaveGenesisValidatorsRoot`

* Filesystem database: Creates `graffiti.go`.

This file defines the following public functions:
- `SaveGraffitiOrderedIndex`
- `GraffitiOrderedIndex`

* Filesystem database: Creates `migration.go`.

This file defines the following public functions:
- `RunUpMigrations`
- `RunDownMigrations`

* Filesystem database: Creates proposer_settings.go.

This file defines the following public functions:
- `ProposerSettings`
- `ProposerSettingsExists`
- `SaveProposerSettings`

* Filesystem database: Creates `attester_protection.go`.

This file defines the following public functions:
- `EIPImportBlacklistedPublicKeys`
- `SaveEIPImportBlacklistedPublicKeys`
- `SigningRootAtTargetEpoch`
- `LowestSignedTargetEpoch`
- `LowestSignedSourceEpoch`
- `AttestedPublicKeys`
- `CheckSlashableAttestation`
- `SaveAttestationForPubKey`
- `SaveAttestationsForPubKey`
- `AttestationHistoryForPubKey`

* Filesystem database: Creates `proposer_protection.go`.

This file defines the following public functions:
- `HighestSignedProposal`
- `LowestSignedProposal`
- `ProposalHistoryForPubKey`
- `ProposalHistoryForSlot`
- `ProposedPublicKeys`

* Ensure that the filesystem store implements the `ValidatorDB` interface.

* `slashableAttestationCheck`: Check the database type.

* `slashableProposalCheck`: Check the database type.

* `slashableAttestationCheck`: Allow usage of minimal slashing protection.

* `slashableProposalCheck`: Allow usage of minimal slashing protection.

* `ImportStandardProtectionJSON`: Check the database type.

* `ImportStandardProtectionJSON`: Allow usage of min slashing protection.

* Implement `RecursiveDirFind`.

* Implement minimal<->complete DB conversion.

3 public functions are implemented:
- `IsCompleteDatabaseExisting`
- `IsMinimalDatabaseExisting`
- `ConvertDatabase`

* `setupDB`: Add `isSlashingProtectionMinimal` argument.

The feature addition is located in `validator/node/node_test.go`.
The rest of this commit consists in minimal slashing protection testing.

* `setupWithKey`: Add `isSlashingProtectionMinimal` argument.

The feature addition is located in `validator/client/propose_test.go`.

The rest of this commit consists in tests wrapping.

* `setup`: Add `isSlashingProtectionMinimal` argument.

The added feature is located in the `validator/client/propose_test.go`
file.

The rest of this commit consists in tests wrapping.

* `initializeFromCLI` and `initializeForWeb`: Factorize db init.

* Add `convert-complete-to-minimal` command.

* Creates `--enable-minimal-slashing-protection` flag.

* `importSlashingProtectionJSON`: Check database type.

* `exportSlashingProtectionJSON`: Check database type.

* `TestClearDB`: Test with minimal slashing protection.

* KeyManager: Test with minimal slashing protection.

* RPC: KeyManager: Test with minimal slashing protection.

* `convert-complete-to-minimal`: Change option names.

Options were:
- `--source` (for source data directory), and
- `--target` (for target data directory)

However, since this command deals with slashing protection, which has
source (epochs) and target (epochs), the initial option names may confuse
the user.

In this commit:
`--source` ==> `--source-data-dir`
`--target` ==> `--target-data-dir`

* Set `SlashableAttestationCheck` as an iface method.

And delete `CheckSlashableAttestation` from iface.

* Move helpers functions in a more general directory.

No functional change.

* Extract common structs out of `kv`.

==> `filesystem` does not depend anymore on `kv`.
==> `iface` does not depend anymore on `kv`.
==> `slashing-protection` does not depend anymore on `kv`.

* Move `ValidateMetadata` in `validator/helpers`.

* `ValidateMetadata`: Test with mock.

This way, we can:
- Avoid any circular import for tests.
- Implement once for all `iface.ValidatorDB` implementations
  the `ValidateMetadata`function.
- Have tests (and coverage) of `ValidateMetadata`in
  its own package.

The ideal solution would have been to implement `ValidateMetadata` as
a method with the `iface.ValidatorDB`receiver.
Unfortunately, golang does not allow that.

* `iface.ValidatorDB`: Implement ImportStandardProtectionJSON.

The whole purpose of this commit is to avoid the `switch validatorDB.(type)`
in `ImportStandardProtectionJSON`.

* `iface.ValidatorDB`: Implement `SlashableProposalCheck`.

* Remove now useless `slashableProposalCheck`.

* Delete useless `ImportStandardProtectionJSON`.

* `file.Exists`: Detect directories and return an error.

Before, `Exists` was only able to detect if a file exists.
Now, this function takes an extra `File` or `Directory` argument.
It detects either if a file or a directory exists.

Before, if an error was returned by `os.Stat`, the the file was
considered as non existing.
Now, it is treated as a real error.

* Replace `os.Stat` by `file.Exists`.

* Remove `Is{Complete,Minimal}DatabaseExisting`.

* `publicKeys`: Add log if unexpected file found.

* Move `{Source,Target}DataDirFlag`in `db.go`.

* `failedAttLocalProtectionErr`: `var`==> `const`

* `signingRoot`: `32`==> `fieldparams.RootLength`.

* `validatorClientData`==> `validator-client-data`.

To be consistent with `slashing-protection`.

* Add progress bars for `import` and `convert`.

* `parseBlocksForUniquePublicKeys`: Move in `db/kv`.

* helpers: Remove unused `initializeProgressBar` function.
2024-03-05 15:27:15 +00:00

620 lines
20 KiB
Go

package kv
import (
"context"
"fmt"
"path/filepath"
"sync"
"testing"
fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams"
"github.com/prysmaticlabs/prysm/v5/consensus-types/primitives"
"github.com/prysmaticlabs/prysm/v5/crypto/bls"
"github.com/prysmaticlabs/prysm/v5/encoding/bytesutil"
ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v5/testing/assert"
"github.com/prysmaticlabs/prysm/v5/testing/require"
"github.com/prysmaticlabs/prysm/v5/validator/db/common"
logTest "github.com/sirupsen/logrus/hooks/test"
bolt "go.etcd.io/bbolt"
)
func TestPendingAttestationRecords_Flush(t *testing.T) {
queue := NewQueuedAttestationRecords()
// Add 5 atts
num := 5
for i := 0; i < num; i++ {
queue.Append(&common.AttestationRecord{
Target: primitives.Epoch(i),
})
}
res := queue.Flush()
assert.Equal(t, len(res), num, "Wrong number of flushed attestations")
assert.Equal(t, len(queue.records), 0, "Records were not cleared/flushed")
}
func TestPendingAttestationRecords_Len(t *testing.T) {
queue := NewQueuedAttestationRecords()
assert.Equal(t, queue.Len(), 0)
queue.Append(&common.AttestationRecord{})
assert.Equal(t, queue.Len(), 1)
queue.Flush()
assert.Equal(t, queue.Len(), 0)
}
func TestStore_CheckSlashableAttestation_DoubleVote(t *testing.T) {
ctx := context.Background()
numValidators := 1
pubKeys := make([][fieldparams.BLSPubkeyLength]byte, numValidators)
validatorDB := setupDB(t, pubKeys)
tests := []struct {
name string
existingAttestation *ethpb.IndexedAttestation
existingSigningRoot [32]byte
incomingAttestation *ethpb.IndexedAttestation
incomingSigningRoot [32]byte
want bool
}{
{
name: "different signing root at same target equals a double vote",
existingAttestation: createAttestation(0, 1 /* Target */),
existingSigningRoot: [32]byte{1},
incomingAttestation: createAttestation(0, 1 /* Target */),
incomingSigningRoot: [32]byte{2},
want: true,
},
{
name: "same signing root at same target is safe",
existingAttestation: createAttestation(0, 1 /* Target */),
existingSigningRoot: [32]byte{1},
incomingAttestation: createAttestation(0, 1 /* Target */),
incomingSigningRoot: [32]byte{1},
want: false,
},
{
name: "different signing root at different target is safe",
existingAttestation: createAttestation(0, 1 /* Target */),
existingSigningRoot: [32]byte{1},
incomingAttestation: createAttestation(0, 2 /* Target */),
incomingSigningRoot: [32]byte{2},
want: false,
},
{
name: "no data stored at target should not be considered a double vote",
existingAttestation: createAttestation(0, 1 /* Target */),
existingSigningRoot: [32]byte{1},
incomingAttestation: createAttestation(0, 2 /* Target */),
incomingSigningRoot: [32]byte{1},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validatorDB.SaveAttestationForPubKey(
ctx,
pubKeys[0],
tt.existingSigningRoot,
tt.existingAttestation,
)
require.NoError(t, err)
slashingKind, err := validatorDB.CheckSlashableAttestation(
ctx,
pubKeys[0],
tt.incomingSigningRoot[:],
tt.incomingAttestation,
)
if tt.want {
require.NotNil(t, err)
assert.Equal(t, DoubleVote, slashingKind)
} else {
require.NoError(t, err)
}
})
}
}
func TestStore_CheckSlashableAttestation_SurroundVote_MultipleTargetsPerSource(t *testing.T) {
ctx := context.Background()
numValidators := 1
pubKeys := make([][fieldparams.BLSPubkeyLength]byte, numValidators)
validatorDB := setupDB(t, pubKeys)
// Create an attestation with source 1 and target 50, save it.
firstAtt := createAttestation(1, 50)
err := validatorDB.SaveAttestationForPubKey(ctx, pubKeys[0], [32]byte{0}, firstAtt)
require.NoError(t, err)
// Create an attestation with source 1 and target 100, save it.
secondAtt := createAttestation(1, 100)
err = validatorDB.SaveAttestationForPubKey(ctx, pubKeys[0], [32]byte{1}, secondAtt)
require.NoError(t, err)
// Create an attestation with source 0 and target 51, which should surround
// our first attestation. Given there can be multiple attested target epochs per
// source epoch, we expect our logic to be able to catch this slashable offense.
evilAtt := createAttestation(firstAtt.Data.Source.Epoch-1, firstAtt.Data.Target.Epoch+1)
slashable, err := validatorDB.CheckSlashableAttestation(ctx, pubKeys[0], []byte{2}, evilAtt)
require.NotNil(t, err)
assert.Equal(t, SurroundingVote, slashable)
}
func TestStore_CheckSlashableAttestation_SurroundVote_54kEpochs(t *testing.T) {
ctx := context.Background()
numValidators := 1
numEpochs := primitives.Epoch(54000)
pubKeys := make([][fieldparams.BLSPubkeyLength]byte, numValidators)
validatorDB := setupDB(t, pubKeys)
// Attest to every (source = epoch, target = epoch + 1) sequential pair
// since genesis up to and including the weak subjectivity period epoch (54,000).
err := validatorDB.update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(pubKeysBucket)
pkBucket, err := bucket.CreateBucketIfNotExists(pubKeys[0][:])
if err != nil {
return err
}
sourceEpochsBucket, err := pkBucket.CreateBucketIfNotExists(attestationSourceEpochsBucket)
if err != nil {
return err
}
for epoch := primitives.Epoch(1); epoch < numEpochs; epoch++ {
att := createAttestation(epoch-1, epoch)
sourceEpoch := bytesutil.EpochToBytesBigEndian(att.Data.Source.Epoch)
targetEpoch := bytesutil.EpochToBytesBigEndian(att.Data.Target.Epoch)
if err := sourceEpochsBucket.Put(sourceEpoch, targetEpoch); err != nil {
return err
}
}
return nil
})
require.NoError(t, err)
tests := []struct {
name string
signingRoot []byte
attestation *ethpb.IndexedAttestation
want SlashingKind
}{
{
name: "surround vote at half of the weak subjectivity period",
signingRoot: []byte{},
attestation: createAttestation(numEpochs/2, numEpochs),
want: SurroundingVote,
},
{
name: "spanning genesis to weak subjectivity period surround vote",
signingRoot: []byte{},
attestation: createAttestation(0, numEpochs),
want: SurroundingVote,
},
{
name: "simple surround vote at end of weak subjectivity period",
signingRoot: []byte{},
attestation: createAttestation(numEpochs-3, numEpochs),
want: SurroundingVote,
},
{
name: "non-slashable vote",
signingRoot: []byte{},
attestation: createAttestation(numEpochs, numEpochs+1),
want: NotSlashable,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
slashingKind, err := validatorDB.CheckSlashableAttestation(ctx, pubKeys[0], tt.signingRoot, tt.attestation)
if tt.want != NotSlashable {
require.NotNil(t, err)
}
assert.Equal(t, tt.want, slashingKind)
})
}
}
func TestLowestSignedSourceEpoch_SaveRetrieve(t *testing.T) {
ctx := context.Background()
validatorDB, err := NewKVStore(ctx, t.TempDir(), &Config{})
require.NoError(t, err, "Failed to instantiate DB")
t.Cleanup(func() {
require.NoError(t, validatorDB.Close(), "Failed to close database")
require.NoError(t, validatorDB.ClearDB(), "Failed to clear database")
})
p0 := [fieldparams.BLSPubkeyLength]byte{0}
p1 := [fieldparams.BLSPubkeyLength]byte{1}
// Can save.
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(100, 101)),
)
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(200, 201)),
)
got, _, err := validatorDB.LowestSignedSourceEpoch(ctx, p0)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(100), got)
got, _, err = validatorDB.LowestSignedSourceEpoch(ctx, p1)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(200), got)
// Can replace.
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(99, 100)),
)
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(199, 200)),
)
got, _, err = validatorDB.LowestSignedSourceEpoch(ctx, p0)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(99), got)
got, _, err = validatorDB.LowestSignedSourceEpoch(ctx, p1)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(199), got)
// Can not replace.
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(100, 101)),
)
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(200, 201)),
)
got, _, err = validatorDB.LowestSignedSourceEpoch(ctx, p0)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(99), got)
got, _, err = validatorDB.LowestSignedSourceEpoch(ctx, p1)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(199), got)
}
func TestLowestSignedTargetEpoch_SaveRetrieveReplace(t *testing.T) {
ctx := context.Background()
validatorDB, err := NewKVStore(ctx, t.TempDir(), &Config{})
require.NoError(t, err, "Failed to instantiate DB")
t.Cleanup(func() {
require.NoError(t, validatorDB.Close(), "Failed to close database")
require.NoError(t, validatorDB.ClearDB(), "Failed to clear database")
})
p0 := [fieldparams.BLSPubkeyLength]byte{0}
p1 := [fieldparams.BLSPubkeyLength]byte{1}
// Can save.
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(99, 100)),
)
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(199, 200)),
)
got, _, err := validatorDB.LowestSignedTargetEpoch(ctx, p0)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(100), got)
got, _, err = validatorDB.LowestSignedTargetEpoch(ctx, p1)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(200), got)
// Can replace.
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(98, 99)),
)
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(198, 199)),
)
got, _, err = validatorDB.LowestSignedTargetEpoch(ctx, p0)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(99), got)
got, _, err = validatorDB.LowestSignedTargetEpoch(ctx, p1)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(199), got)
// Can not replace.
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(99, 100)),
)
require.NoError(
t,
validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(199, 200)),
)
got, _, err = validatorDB.LowestSignedTargetEpoch(ctx, p0)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(99), got)
got, _, err = validatorDB.LowestSignedTargetEpoch(ctx, p1)
require.NoError(t, err)
require.Equal(t, primitives.Epoch(199), got)
}
func TestStore_SaveAttestationsForPubKey(t *testing.T) {
ctx := context.Background()
numValidators := 1
pubKeys := make([][fieldparams.BLSPubkeyLength]byte, numValidators)
validatorDB := setupDB(t, pubKeys)
atts := make([]*ethpb.IndexedAttestation, 0)
signingRoots := make([][]byte, 0)
for i := primitives.Epoch(1); i < 10; i++ {
atts = append(atts, createAttestation(i-1, i))
var sr []byte
copy(sr, fmt.Sprintf("%d", i))
signingRoots = append(signingRoots, sr)
}
err := validatorDB.SaveAttestationsForPubKey(
ctx,
pubKeys[0],
signingRoots[:1],
atts,
)
require.ErrorContains(t, "does not match number of attestations", err)
err = validatorDB.SaveAttestationsForPubKey(
ctx,
pubKeys[0],
signingRoots,
atts,
)
require.NoError(t, err)
for _, att := range atts {
// Ensure the same attestations but different signing root lead to double votes.
slashingKind, err := validatorDB.CheckSlashableAttestation(
ctx,
pubKeys[0],
[]byte{},
att,
)
require.NotNil(t, err)
require.Equal(t, DoubleVote, slashingKind)
}
}
func TestSaveAttestationForPubKey_BatchWrites_FullCapacity(t *testing.T) {
hook := logTest.NewGlobal()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
numValidators := attestationBatchCapacity
pubKeys := make([][fieldparams.BLSPubkeyLength]byte, numValidators)
validatorDB := setupDB(t, pubKeys)
// For each public key, we attempt to save an attestation with signing root.
var wg sync.WaitGroup
for i, pubKey := range pubKeys {
wg.Add(1)
go func(j primitives.Epoch, pk [fieldparams.BLSPubkeyLength]byte, w *sync.WaitGroup) {
defer w.Done()
var signingRoot [32]byte
copy(signingRoot[:], fmt.Sprintf("%d", j))
att := createAttestation(j, j+1)
err := validatorDB.SaveAttestationForPubKey(ctx, pk, signingRoot, att)
require.NoError(t, err)
}(primitives.Epoch(i), pubKey, &wg)
}
wg.Wait()
// We verify that we reached the max capacity of batched attestations
// before we are required to force flush them to the DB.
require.LogsContain(t, hook, "Reached max capacity of batched attestation records")
require.LogsDoNotContain(t, hook, "Batched attestation records write interval reached")
require.LogsContain(t, hook, "Successfully flushed batched attestations to DB")
require.Equal(t, 0, validatorDB.batchedAttestations.Len())
// We then verify all the data we wanted to save is indeed saved to disk.
err := validatorDB.view(func(tx *bolt.Tx) error {
bucket := tx.Bucket(pubKeysBucket)
for i, pubKey := range pubKeys {
var signingRoot [32]byte
copy(signingRoot[:], fmt.Sprintf("%d", i))
pkBucket := bucket.Bucket(pubKey[:])
signingRootsBucket := pkBucket.Bucket(attestationSigningRootsBucket)
sourceEpochsBucket := pkBucket.Bucket(attestationSourceEpochsBucket)
source := bytesutil.Uint64ToBytesBigEndian(uint64(i))
target := bytesutil.Uint64ToBytesBigEndian(uint64(i) + 1)
savedSigningRoot := signingRootsBucket.Get(target)
require.DeepEqual(t, signingRoot[:], savedSigningRoot)
savedTarget := sourceEpochsBucket.Get(source)
require.DeepEqual(t, signingRoot[:], savedSigningRoot)
require.DeepEqual(t, target, savedTarget)
}
return nil
})
require.NoError(t, err)
}
func TestSaveAttestationForPubKey_BatchWrites_LowCapacity_TimerReached(t *testing.T) {
hook := logTest.NewGlobal()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Number of validators equal to half the total capacity
// of batch attestation processing. This will allow us to
// test force flushing to the DB based on a timer instead
// of the max capacity being reached.
numValidators := attestationBatchCapacity / 2
pubKeys := make([][fieldparams.BLSPubkeyLength]byte, numValidators)
validatorDB := setupDB(t, pubKeys)
// For each public key, we attempt to save an attestation with signing root.
var wg sync.WaitGroup
for i, pubKey := range pubKeys {
wg.Add(1)
go func(j primitives.Epoch, pk [fieldparams.BLSPubkeyLength]byte, w *sync.WaitGroup) {
defer w.Done()
var signingRoot [32]byte
copy(signingRoot[:], fmt.Sprintf("%d", j))
att := createAttestation(j, j+1)
err := validatorDB.SaveAttestationForPubKey(ctx, pk, signingRoot, att)
require.NoError(t, err)
}(primitives.Epoch(i), pubKey, &wg)
}
wg.Wait()
// We verify that we reached a timer interval for force flushing records
// before we are required to force flush them to the DB.
require.LogsDoNotContain(t, hook, "Reached max capacity of batched attestation records")
require.LogsContain(t, hook, "Batched attestation records write interval reached")
require.LogsContain(t, hook, "Successfully flushed batched attestations to DB")
require.Equal(t, 0, validatorDB.batchedAttestations.Len())
// We then verify all the data we wanted to save is indeed saved to disk.
err := validatorDB.view(func(tx *bolt.Tx) error {
bucket := tx.Bucket(pubKeysBucket)
for i, pubKey := range pubKeys {
var signingRoot [32]byte
copy(signingRoot[:], fmt.Sprintf("%d", i))
pkBucket := bucket.Bucket(pubKey[:])
signingRootsBucket := pkBucket.Bucket(attestationSigningRootsBucket)
sourceEpochsBucket := pkBucket.Bucket(attestationSourceEpochsBucket)
source := bytesutil.Uint64ToBytesBigEndian(uint64(i))
target := bytesutil.Uint64ToBytesBigEndian(uint64(i) + 1)
savedSigningRoot := signingRootsBucket.Get(target)
require.DeepEqual(t, signingRoot[:], savedSigningRoot)
savedTarget := sourceEpochsBucket.Get(source)
require.DeepEqual(t, signingRoot[:], savedSigningRoot)
require.DeepEqual(t, target, savedTarget)
}
return nil
})
require.NoError(t, err)
}
func BenchmarkStore_CheckSlashableAttestation_Surround_SafeAttestation_54kEpochs(b *testing.B) {
numValidators := 1
numEpochs := primitives.Epoch(54000)
pubKeys := make([][fieldparams.BLSPubkeyLength]byte, numValidators)
benchCheckSurroundVote(b, pubKeys, numEpochs, false /* surround */)
}
func BenchmarkStore_CheckSurroundVote_Surround_Slashable_54kEpochs(b *testing.B) {
numValidators := 1
numEpochs := primitives.Epoch(54000)
pubKeys := make([][fieldparams.BLSPubkeyLength]byte, numValidators)
benchCheckSurroundVote(b, pubKeys, numEpochs, true /* surround */)
}
func benchCheckSurroundVote(
b *testing.B,
pubKeys [][fieldparams.BLSPubkeyLength]byte,
numEpochs primitives.Epoch,
shouldSurround bool,
) {
ctx := context.Background()
validatorDB, err := NewKVStore(ctx, filepath.Join(b.TempDir(), "benchsurroundvote"), &Config{
PubKeys: pubKeys,
})
require.NoError(b, err, "Failed to instantiate DB")
defer func() {
require.NoError(b, validatorDB.Close(), "Failed to close database")
require.NoError(b, validatorDB.ClearDB(), "Failed to clear database")
}()
// Every validator will have attested every (source, target) sequential pair
// since genesis up to and including the weak subjectivity period epoch (54,000).
err = validatorDB.update(func(tx *bolt.Tx) error {
for _, pubKey := range pubKeys {
bucket := tx.Bucket(pubKeysBucket)
pkBucket, err := bucket.CreateBucketIfNotExists(pubKey[:])
if err != nil {
return err
}
sourceEpochsBucket, err := pkBucket.CreateBucketIfNotExists(attestationSourceEpochsBucket)
if err != nil {
return err
}
for epoch := primitives.Epoch(1); epoch < numEpochs; epoch++ {
att := createAttestation(epoch-1, epoch)
sourceEpoch := bytesutil.EpochToBytesBigEndian(att.Data.Source.Epoch)
targetEpoch := bytesutil.EpochToBytesBigEndian(att.Data.Target.Epoch)
if err := sourceEpochsBucket.Put(sourceEpoch, targetEpoch); err != nil {
return err
}
}
}
return nil
})
require.NoError(b, err)
// Will surround many attestations.
var surroundingVote *ethpb.IndexedAttestation
if shouldSurround {
surroundingVote = createAttestation(numEpochs/2, numEpochs)
} else {
surroundingVote = createAttestation(numEpochs+1, numEpochs+2)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, pubKey := range pubKeys {
slashingKind, err := validatorDB.CheckSlashableAttestation(ctx, pubKey, []byte{}, surroundingVote)
if shouldSurround {
require.NotNil(b, err)
assert.Equal(b, SurroundingVote, slashingKind)
} else {
require.NoError(b, err)
}
}
}
}
func TestStore_flushAttestationRecords_InProgress(t *testing.T) {
s := &Store{}
s.batchedAttestationsFlushInProgress.Set()
hook := logTest.NewGlobal()
s.flushAttestationRecords(context.Background(), nil)
assert.LogsContain(t, hook, "Attempted to flush attestation records when already in progress")
}
func BenchmarkStore_SaveAttestationForPubKey(b *testing.B) {
var wg sync.WaitGroup
ctx := context.Background()
// Create pubkeys
pubkeys := make([][fieldparams.BLSPubkeyLength]byte, 10)
for i := range pubkeys {
validatorKey, err := bls.RandKey()
require.NoError(b, err, "RandKey should not return an error")
copy(pubkeys[i][:], validatorKey.PublicKey().Marshal())
}
signingRoot := [32]byte{1}
attestation := &ethpb.IndexedAttestation{
Data: &ethpb.AttestationData{
Source: &ethpb.Checkpoint{
Epoch: 42,
},
Target: &ethpb.Checkpoint{
Epoch: 43,
},
},
}
validatorDB, err := NewKVStore(ctx, b.TempDir(), &Config{PubKeys: pubkeys})
require.NoError(b, err)
for i := 0; i < b.N; i++ {
b.StopTimer()
err := validatorDB.ClearDB()
require.NoError(b, err)
for _, pubkey := range pubkeys {
wg.Add(1)
go func(pk [fieldparams.BLSPubkeyLength]byte) {
defer wg.Done()
err := validatorDB.SaveAttestationForPubKey(ctx, pk, signingRoot, attestation)
require.NoError(b, err)
}(pubkey)
}
b.StartTimer()
wg.Wait()
}
err = validatorDB.Close()
require.NoError(b, err)
}