mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2024-12-22 03:30:35 +00:00
ef21d3adf8
* `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.
197 lines
7.4 KiB
Go
197 lines
7.4 KiB
Go
package history
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams"
|
|
"github.com/prysmaticlabs/prysm/v5/encoding/bytesutil"
|
|
"github.com/prysmaticlabs/prysm/v5/monitoring/progress"
|
|
"github.com/prysmaticlabs/prysm/v5/validator/db"
|
|
"github.com/prysmaticlabs/prysm/v5/validator/helpers"
|
|
"github.com/prysmaticlabs/prysm/v5/validator/slashing-protection-history/format"
|
|
)
|
|
|
|
// ExportStandardProtectionJSON extracts all slashing protection data from a validator database
|
|
// and packages it into an EIP-3076 compliant, standard
|
|
func ExportStandardProtectionJSON(
|
|
ctx context.Context,
|
|
validatorDB db.Database,
|
|
filteredKeys ...[]byte,
|
|
) (*format.EIPSlashingProtectionFormat, error) {
|
|
interchangeJSON := &format.EIPSlashingProtectionFormat{}
|
|
genesisValidatorsRoot, err := validatorDB.GenesisValidatorsRoot(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not get genesis validators root from DB")
|
|
}
|
|
if genesisValidatorsRoot == nil || !bytesutil.IsValidRoot(genesisValidatorsRoot) {
|
|
return nil, errors.New(
|
|
"genesis validators root is empty, perhaps you are not connected to your beacon node",
|
|
)
|
|
}
|
|
genesisRootHex, err := helpers.RootToHexString(genesisValidatorsRoot)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not convert genesis validators root to hex string")
|
|
}
|
|
interchangeJSON.Metadata.GenesisValidatorsRoot = genesisRootHex
|
|
interchangeJSON.Metadata.InterchangeFormatVersion = format.InterchangeFormatVersion
|
|
|
|
// Allow for filtering data for the keys we wish to export.
|
|
filteredKeysMap := make(map[string]bool, len(filteredKeys))
|
|
for _, k := range filteredKeys {
|
|
filteredKeysMap[string(k)] = true
|
|
}
|
|
|
|
// Extract the existing public keys in our database.
|
|
proposedPublicKeys, err := validatorDB.ProposedPublicKeys(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not retrieve proposer public keys from DB")
|
|
}
|
|
attestedPublicKeys, err := validatorDB.AttestedPublicKeys(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not retrieve attested public keys from DB")
|
|
}
|
|
dataByPubKey := make(map[[fieldparams.BLSPubkeyLength]byte]*format.ProtectionData)
|
|
|
|
// Extract the signed proposals by public key.
|
|
bar := progress.InitializeProgressBar(
|
|
len(proposedPublicKeys), "Extracting signed blocks by validator public key",
|
|
)
|
|
for _, pubKey := range proposedPublicKeys {
|
|
if _, ok := filteredKeysMap[string(pubKey[:])]; len(filteredKeys) > 0 && !ok {
|
|
continue
|
|
}
|
|
pubKeyHex, err := helpers.PubKeyToHexString(pubKey[:])
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not convert public key to hex string")
|
|
}
|
|
signedBlocks, err := signedBlocksByPubKey(ctx, validatorDB, pubKey)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "could not retrieve signed blocks for public key %s", pubKeyHex)
|
|
}
|
|
dataByPubKey[pubKey] = &format.ProtectionData{
|
|
Pubkey: pubKeyHex,
|
|
SignedBlocks: signedBlocks,
|
|
SignedAttestations: nil,
|
|
}
|
|
if err := bar.Add(1); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Extract the signed attestations by public key.
|
|
bar = progress.InitializeProgressBar(
|
|
len(attestedPublicKeys), "Extracting signed attestations by validator public key",
|
|
)
|
|
for _, pubKey := range attestedPublicKeys {
|
|
if _, ok := filteredKeysMap[string(pubKey[:])]; len(filteredKeys) > 0 && !ok {
|
|
continue
|
|
}
|
|
pubKeyHex, err := helpers.PubKeyToHexString(pubKey[:])
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not convert public key to hex string")
|
|
}
|
|
signedAttestations, err := signedAttestationsByPubKey(ctx, validatorDB, pubKey)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "could not retrieve signed attestations for public key %s", pubKeyHex)
|
|
}
|
|
if _, ok := dataByPubKey[pubKey]; !ok {
|
|
// This should never happen
|
|
return nil, errors.Wrapf(err, "could not retrieve proposer public key from array")
|
|
}
|
|
dataByPubKey[pubKey].SignedAttestations = signedAttestations
|
|
|
|
if err := bar.Add(1); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Next we turn our map into a slice as expected by the EIP-3076 JSON standard.
|
|
dataList := make([]*format.ProtectionData, 0)
|
|
for _, item := range dataByPubKey {
|
|
if item.SignedAttestations == nil {
|
|
item.SignedAttestations = make([]*format.SignedAttestation, 0)
|
|
}
|
|
if item.SignedBlocks == nil {
|
|
item.SignedBlocks = make([]*format.SignedBlock, 0)
|
|
}
|
|
dataList = append(dataList, item)
|
|
}
|
|
sort.Slice(dataList, func(i, j int) bool {
|
|
return strings.Compare(dataList[i].Pubkey, dataList[j].Pubkey) < 0
|
|
})
|
|
interchangeJSON.Data = dataList
|
|
return interchangeJSON, nil
|
|
}
|
|
|
|
func signedAttestationsByPubKey(ctx context.Context, validatorDB db.Database, pubKey [fieldparams.BLSPubkeyLength]byte) ([]*format.SignedAttestation, error) {
|
|
// If a key does not have an attestation history in our database, we return nil.
|
|
// This way, a user will be able to export their slashing protection history
|
|
// even if one of their keys does not have a history of signed attestations.
|
|
history, err := validatorDB.AttestationHistoryForPubKey(ctx, pubKey)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "cannot get attestation history for public key")
|
|
}
|
|
if history == nil {
|
|
return nil, nil
|
|
}
|
|
signedAttestations := make([]*format.SignedAttestation, 0)
|
|
for i := 0; i < len(history); i++ {
|
|
att := history[i]
|
|
// Special edge case due to a bug in Prysm's old slashing protection schema. The bug
|
|
// manifests itself as the first entry in attester slashing protection history
|
|
// having a target epoch greater than the next entry in the list. If this manifests,
|
|
// we skip it to protect users. This check is the best trade-off we can make at
|
|
// the moment without creating any false positive slashable attestation exports.
|
|
// More information on the bug can found in https://github.com/prysmaticlabs/prysm/issues/8893.
|
|
if i == 0 && len(history) > 1 {
|
|
nextEntryTargetEpoch := history[1].Target
|
|
if att.Target > nextEntryTargetEpoch && att.Source == 0 {
|
|
continue
|
|
}
|
|
}
|
|
var root string
|
|
if len(att.SigningRoot) != 0 {
|
|
root, err = helpers.RootToHexString(att.SigningRoot)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not convert signing root to hex string")
|
|
}
|
|
}
|
|
signedAttestations = append(signedAttestations, &format.SignedAttestation{
|
|
TargetEpoch: fmt.Sprintf("%d", att.Target),
|
|
SourceEpoch: fmt.Sprintf("%d", att.Source),
|
|
SigningRoot: root,
|
|
})
|
|
}
|
|
return signedAttestations, nil
|
|
}
|
|
|
|
func signedBlocksByPubKey(ctx context.Context, validatorDB db.Database, pubKey [fieldparams.BLSPubkeyLength]byte) ([]*format.SignedBlock, error) {
|
|
// If a key does not have a lowest or highest signed proposal history
|
|
// in our database, we return an empty list. This way, a user will be able to export
|
|
// their slashing protection history even if one of their keys does not have a history
|
|
// of signed blocks.
|
|
proposalHistory, err := validatorDB.ProposalHistoryForPubKey(ctx, pubKey)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "could not get proposal history for public key: %#x", pubKey)
|
|
}
|
|
signedBlocks := make([]*format.SignedBlock, 0)
|
|
for _, proposal := range proposalHistory {
|
|
if ctx.Err() != nil {
|
|
return nil, errors.Wrap(err, "context canceled")
|
|
}
|
|
signingRootHex, err := helpers.RootToHexString(proposal.SigningRoot)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not convert signing root to hex string")
|
|
}
|
|
signedBlocks = append(signedBlocks, &format.SignedBlock{
|
|
Slot: fmt.Sprintf("%d", proposal.Slot),
|
|
SigningRoot: signingRootHex,
|
|
})
|
|
}
|
|
return signedBlocks, nil
|
|
}
|