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.
337 lines
11 KiB
Go
337 lines
11 KiB
Go
package accounts
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
"github.com/prysmaticlabs/prysm/v5/crypto/bls"
|
|
"github.com/prysmaticlabs/prysm/v5/encoding/bytesutil"
|
|
"github.com/prysmaticlabs/prysm/v5/io/file"
|
|
"github.com/prysmaticlabs/prysm/v5/io/prompt"
|
|
"github.com/prysmaticlabs/prysm/v5/validator/accounts/wallet"
|
|
"github.com/prysmaticlabs/prysm/v5/validator/keymanager"
|
|
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
|
)
|
|
|
|
var derivationPathRegex = regexp.MustCompile(`m_12381_3600_(\d+)_(\d+)_(\d+)`)
|
|
|
|
// byDerivationPath implements sort.Interface based on a
|
|
// derivation path present in a keystore filename, if any. This
|
|
// will allow us to sort filenames such as keystore-m_12381_3600_1_0_0.json
|
|
// in a directory and import them nicely in order of the derivation path.
|
|
type byDerivationPath []string
|
|
|
|
// Len is the number of elements in the collection.
|
|
func (fileNames byDerivationPath) Len() int { return len(fileNames) }
|
|
|
|
// Less reports whether the element with index i must sort before the element with index j.
|
|
func (fileNames byDerivationPath) Less(i, j int) bool {
|
|
// We check if file name at index i has a derivation path
|
|
// in the filename. If it does not, then it is not less than j, and
|
|
// we should swap it towards the end of the sorted list.
|
|
if !derivationPathRegex.MatchString(fileNames[i]) {
|
|
return false
|
|
}
|
|
derivationPathA := derivationPathRegex.FindString(fileNames[i])
|
|
derivationPathB := derivationPathRegex.FindString(fileNames[j])
|
|
if derivationPathA == "" {
|
|
return false
|
|
}
|
|
if derivationPathB == "" {
|
|
return true
|
|
}
|
|
a, err := strconv.Atoi(accountIndexFromFileName(derivationPathA))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
b, err := strconv.Atoi(accountIndexFromFileName(derivationPathB))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return a < b
|
|
}
|
|
|
|
// Swap swaps the elements with indexes i and j.
|
|
func (fileNames byDerivationPath) Swap(i, j int) {
|
|
fileNames[i], fileNames[j] = fileNames[j], fileNames[i]
|
|
}
|
|
|
|
// ImportAccountsConfig defines values to run the import accounts function.
|
|
type ImportAccountsConfig struct {
|
|
Keystores []*keymanager.Keystore
|
|
Importer keymanager.Importer
|
|
AccountPassword string
|
|
}
|
|
|
|
// Import can import external, EIP-2335 compliant keystore.json files as
|
|
// new accounts into the Prysm validator wallet. This uses the CLI to extract
|
|
// values necessary to run the function.
|
|
func (acm *CLIManager) Import(ctx context.Context) error {
|
|
k, ok := acm.keymanager.(keymanager.Importer)
|
|
if !ok {
|
|
return errors.New("keymanager cannot import keystores")
|
|
}
|
|
log.Info("importing validator keystores...")
|
|
// Check if the user wishes to import a one-off, private key directly
|
|
// as an account into the Prysm validator.
|
|
if acm.importPrivateKeys {
|
|
return importPrivateKeyAsAccount(ctx, acm.wallet, k, acm.privateKeyFile)
|
|
}
|
|
|
|
keystoresImported, err := processDirectory(ctx, acm.keysDir, 0)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unable to process directory and import keys")
|
|
}
|
|
|
|
var accountsPassword string
|
|
if acm.readPasswordFile {
|
|
data, err := os.ReadFile(acm.passwordFilePath) // #nosec G304
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accountsPassword = string(data)
|
|
} else {
|
|
accountsPassword, err = prompt.PasswordPrompt(
|
|
"Enter the password for your imported accounts", prompt.NotEmpty,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not read account password: %w", err)
|
|
}
|
|
}
|
|
fmt.Println("Importing accounts, this may take a while...")
|
|
statuses, err := ImportAccounts(ctx, &ImportAccountsConfig{
|
|
Importer: k,
|
|
Keystores: keystoresImported,
|
|
AccountPassword: accountsPassword,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var successfullyImportedAccounts []string
|
|
for i, status := range statuses {
|
|
switch status.Status {
|
|
case keymanager.StatusImported:
|
|
successfullyImportedAccounts = append(successfullyImportedAccounts, keystoresImported[i].Pubkey)
|
|
case keymanager.StatusDuplicate:
|
|
log.Warnf("Duplicate key %s found in import request, skipped", keystoresImported[i].Pubkey)
|
|
case keymanager.StatusError:
|
|
log.Warnf("Could not import keystore for %s: %s", keystoresImported[i].Pubkey, status.Message)
|
|
}
|
|
}
|
|
if len(successfullyImportedAccounts) == 0 {
|
|
log.Error("no accounts were successfully imported")
|
|
} else {
|
|
log.Infof(
|
|
"Imported accounts %v, view all of them by running `accounts list`",
|
|
successfullyImportedAccounts,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Recursive function to process directories and files.
|
|
func processDirectory(ctx context.Context, dir string, depth int) ([]*keymanager.Keystore, error) {
|
|
maxdepth := 2
|
|
if depth > maxdepth {
|
|
log.Infof("stopped checking folders for keystores after max depth of %d was reached", maxdepth)
|
|
return nil, nil // Stop recursion after two levels.
|
|
}
|
|
log.Infof("checking directory for keystores: %s", dir)
|
|
isDir, err := file.HasDir(dir)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not determine if path is a directory")
|
|
}
|
|
|
|
keystoresImported := make([]*keymanager.Keystore, 0)
|
|
|
|
if isDir {
|
|
files, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not read dir")
|
|
}
|
|
if len(files) == 0 {
|
|
return nil, fmt.Errorf("directory %s has no files, cannot import from it", dir)
|
|
}
|
|
for _, f := range files {
|
|
fullPath := filepath.Join(dir, f.Name())
|
|
if f.IsDir() {
|
|
subKeystores, err := processDirectory(ctx, fullPath, depth+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keystoresImported = append(keystoresImported, subKeystores...)
|
|
} else {
|
|
keystore, err := readKeystoreFile(ctx, fullPath)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "could not decode keystore json") {
|
|
continue
|
|
}
|
|
return nil, errors.Wrapf(err, "could not import keystore at path: %s", fullPath)
|
|
}
|
|
keystoresImported = append(keystoresImported, keystore)
|
|
}
|
|
}
|
|
} else {
|
|
keystore, err := readKeystoreFile(ctx, dir)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not import keystore")
|
|
}
|
|
keystoresImported = append(keystoresImported, keystore)
|
|
}
|
|
|
|
return keystoresImported, nil
|
|
}
|
|
|
|
// ImportAccounts can import external, EIP-2335 compliant keystore.json files as
|
|
// new accounts into the Prysm validator wallet.
|
|
func ImportAccounts(ctx context.Context, cfg *ImportAccountsConfig) ([]*keymanager.KeyStatus, error) {
|
|
if cfg.AccountPassword == "" {
|
|
statuses := make([]*keymanager.KeyStatus, len(cfg.Keystores))
|
|
for i, keystore := range cfg.Keystores {
|
|
statuses[i] = &keymanager.KeyStatus{
|
|
Status: keymanager.StatusError,
|
|
Message: fmt.Sprintf(
|
|
"account password is required to import keystore %s",
|
|
keystore.Pubkey,
|
|
),
|
|
}
|
|
}
|
|
return statuses, nil
|
|
}
|
|
passwords := make([]string, len(cfg.Keystores))
|
|
for i := 0; i < len(cfg.Keystores); i++ {
|
|
passwords[i] = cfg.AccountPassword
|
|
}
|
|
return cfg.Importer.ImportKeystores(
|
|
ctx,
|
|
cfg.Keystores,
|
|
passwords,
|
|
)
|
|
}
|
|
|
|
// Imports a one-off file containing a private key as a hex string into
|
|
// the Prysm validator's accounts.
|
|
func importPrivateKeyAsAccount(ctx context.Context, wallet *wallet.Wallet, importer keymanager.Importer, privKeyFile string) error {
|
|
fullPath, err := file.ExpandPath(privKeyFile)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "could not expand file path for %s", privKeyFile)
|
|
}
|
|
|
|
exists, err := file.Exists(fullPath, file.Regular)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "could not check if file exists: %s", fullPath)
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("file %s does not exist", fullPath)
|
|
}
|
|
privKeyHex, err := os.ReadFile(fullPath) // #nosec G304
|
|
if err != nil {
|
|
return errors.Wrapf(err, "could not read private key file at path %s", fullPath)
|
|
}
|
|
privKeyString := string(privKeyHex)
|
|
if len(privKeyString) > 2 && strings.Contains(privKeyString, "0x") {
|
|
privKeyString = privKeyString[2:] // Strip the 0x prefix, if any.
|
|
}
|
|
privKeyBytes, err := hex.DecodeString(strings.TrimRight(privKeyString, "\r\n"))
|
|
if err != nil {
|
|
return errors.Wrap(
|
|
err, "could not decode file as hex string, does the file contain a valid hex string?",
|
|
)
|
|
}
|
|
privKey, err := bls.SecretKeyFromBytes(privKeyBytes)
|
|
if err != nil {
|
|
return errors.Wrap(err, "not a valid BLS private key")
|
|
}
|
|
keystore, err := createKeystoreFromPrivateKey(privKey, wallet.Password())
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not encrypt private key into a keystore file")
|
|
}
|
|
statuses, err := ImportAccounts(
|
|
ctx,
|
|
&ImportAccountsConfig{
|
|
Importer: importer,
|
|
AccountPassword: wallet.Password(),
|
|
Keystores: []*keymanager.Keystore{keystore},
|
|
},
|
|
)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not import keystore into wallet")
|
|
}
|
|
for _, status := range statuses {
|
|
switch status.Status {
|
|
case keymanager.StatusImported:
|
|
fmt.Printf(
|
|
"Imported account with public key %#x, view all accounts by running `accounts list`\n",
|
|
au.BrightMagenta(bytesutil.Trunc(privKey.PublicKey().Marshal())),
|
|
)
|
|
return nil
|
|
case keymanager.StatusError:
|
|
return fmt.Errorf("could not import keystore for %s: %s", keystore.Pubkey, status.Message)
|
|
case keymanager.StatusDuplicate:
|
|
return fmt.Errorf("duplicate key %s skipped", keystore.Pubkey)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func readKeystoreFile(_ context.Context, keystoreFilePath string) (*keymanager.Keystore, error) {
|
|
keystoreBytes, err := os.ReadFile(keystoreFilePath) // #nosec G304
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not read keystore file")
|
|
}
|
|
keystoreFile := &keymanager.Keystore{}
|
|
if err := json.Unmarshal(keystoreBytes, keystoreFile); err != nil {
|
|
return nil, errors.Wrap(err, "could not decode keystore json")
|
|
}
|
|
if keystoreFile.Pubkey == "" {
|
|
return nil, errors.New("could not decode keystore json")
|
|
}
|
|
if keystoreFile.Description == "" && keystoreFile.Name != "" {
|
|
keystoreFile.Description = keystoreFile.Name
|
|
}
|
|
return keystoreFile, nil
|
|
}
|
|
|
|
func createKeystoreFromPrivateKey(privKey bls.SecretKey, walletPassword string) (*keymanager.Keystore, error) {
|
|
encryptor := keystorev4.New()
|
|
id, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cryptoFields, err := encryptor.Encrypt(privKey.Marshal(), walletPassword)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(
|
|
err,
|
|
"could not encrypt private key with public key %#x",
|
|
privKey.PublicKey().Marshal(),
|
|
)
|
|
}
|
|
return &keymanager.Keystore{
|
|
Crypto: cryptoFields,
|
|
ID: id.String(),
|
|
Version: encryptor.Version(),
|
|
Pubkey: fmt.Sprintf("%x", privKey.PublicKey().Marshal()),
|
|
Description: encryptor.Name(),
|
|
}, nil
|
|
}
|
|
|
|
// Extracts the account index, j, from a derivation path in a file name
|
|
// with the format m_12381_3600_j_0_0.
|
|
func accountIndexFromFileName(derivationPath string) string {
|
|
derivationPath = derivationPath[13:]
|
|
accIndexEnd := strings.Index(derivationPath, "_")
|
|
return derivationPath[:accIndexEnd]
|
|
}
|