Skip to content

Commit

Permalink
Refactoring ValidatorManager and parameterizing KeyRecoveryManager to…
Browse files Browse the repository at this point in the history
… support test and partial recovery modes.

This change addresses all comments from the first draft of the PR to introduce partial recovery support. This removes multiple implementations of a KeyRecoveryManager interface and instead uses class fields to determine how to appropriately enact the recover code flow.
  • Loading branch information
SolezOfScience committed Aug 30, 2024
1 parent 9bd5510 commit 3aa32ab
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 557 deletions.
12 changes: 8 additions & 4 deletions rocketpool-cli/commands/wallet/rebuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package wallet

import (
"fmt"
"github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils"
"os"

"github.com/rocket-pool/node-manager-core/wallet"
"github.com/rocket-pool/smartnode/v2/rocketpool-cli/client"
"github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils"
"github.com/urfave/cli/v2"
)

Expand Down Expand Up @@ -61,13 +61,18 @@ func rebuildWallet(c *cli.Context) error {

// Log
fmt.Println("Rebuilding node validator keystores...")
fmt.Printf("Partial rebuild enabled: %s.\n", enablePartialRebuild.Name)
fmt.Printf("Partial rebuild enabled: %s.\n", enablePartialRebuild.Value)

// Rebuild wallet
response, _ := rp.Api.Wallet.Rebuild(enablePartialRebuildValue)
response, err := rp.Api.Wallet.Rebuild(enablePartialRebuildValue)
if err != nil {
return err
}

// Handle and print failure reasons with associated public keys
if len(response.Data.FailureReasons) > 0 {
fmt.Println("Some keys could not be recovered. You may need to import them manually, as they are not " +
"associated with your node wallet mnemonic. See the documentation for more details.")
fmt.Println("Failure reasons:")
for pubkey, reason := range response.Data.FailureReasons {
fmt.Printf("Public Key: %s - Failure Reason: %s\n", pubkey.Hex(), reason)
Expand All @@ -76,7 +81,6 @@ func rebuildWallet(c *cli.Context) error {
fmt.Println("No failures reported.")
}

fmt.Println("The response for rebuilding the node wallet was successfully received.")
if len(response.Data.RebuiltValidatorKeys) > 0 {
fmt.Println("Validator keys:")
for _, key := range response.Data.RebuiltValidatorKeys {
Expand Down
2 changes: 1 addition & 1 deletion rocketpool-cli/commands/wallet/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ var (
}
enablePartialRebuild = &cli.StringSliceFlag{
Name: "enable-partial-rebuild",
Aliases: []string{"epr"},
Aliases: []string{"p"},
Usage: "Allows the wallet rebuild process to partially succeed, responding with public keys for successfully rebuilt targets and errors for rebuild failures",
}
)
Expand Down
11 changes: 3 additions & 8 deletions rocketpool-daemon/api/wallet/rebuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/gorilla/mux"
"github.com/rocket-pool/node-manager-core/utils/input"
key_recovery_manager "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/validator/key-recovery-manager"
"github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/validator"
"net/url"

"github.com/rocket-pool/node-manager-core/api/server"
Expand Down Expand Up @@ -48,8 +48,7 @@ type walletRebuildContext struct {
func (c *walletRebuildContext) PrepareData(data *api.WalletRebuildData, opts *bind.TransactOpts) (types.ResponseStatus, error) {
sp := c.handler.serviceProvider
vMgr := sp.GetValidatorManager()
partialKeyRecoveryManager := key_recovery_manager.NewPartialRecoveryManager(vMgr)
strictKeyRecoveryManager := key_recovery_manager.NewStrictRecoveryManager(vMgr)
keyRecoveryManager := validator.NewKeyRecoveryManager(vMgr, c.enablePartialRebuild, false)

// Requirements
err := sp.RequireWalletReady()
Expand All @@ -62,11 +61,7 @@ func (c *walletRebuildContext) PrepareData(data *api.WalletRebuildData, opts *bi
}

// Recover validator keys
if c.enablePartialRebuild {
data.RebuiltValidatorKeys, data.FailureReasons, err = partialKeyRecoveryManager.RecoverMinipoolKeys()
} else {
data.RebuiltValidatorKeys, data.FailureReasons, err = strictKeyRecoveryManager.RecoverMinipoolKeys()
}
data.RebuiltValidatorKeys, data.FailureReasons, err = keyRecoveryManager.RecoverMinipoolKeys()
if err != nil {
return types.ResponseStatus_Error, fmt.Errorf("error recovering minipool keys: %w", err)
}
Expand Down
188 changes: 188 additions & 0 deletions rocketpool-daemon/common/validator/key-recovery-manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package validator

import (
"fmt"
"github.com/rocket-pool/node-manager-core/beacon"
"github.com/rocket-pool/node-manager-core/utils"
"golang.org/x/exp/maps"
"strings"
)

type KeyRecoveryManager struct {
manager *ValidatorManager
partialEnabled bool
testOnly bool
}

func NewKeyRecoveryManager(m *ValidatorManager, partialEnabled bool, testOnly bool) *KeyRecoveryManager {
return &KeyRecoveryManager{
manager: m,
partialEnabled: partialEnabled,
testOnly: testOnly,
}
}

func (s *KeyRecoveryManager) RecoverMinipoolKeys() ([]beacon.ValidatorPubkey, map[beacon.ValidatorPubkey]error, error) {
publicKeys, err := s.manager.GetMinipools()
if err != nil {
return []beacon.ValidatorPubkey{}, map[beacon.ValidatorPubkey]error{}, err
}

recoveredCustomPublicKeys, unrecoverableCustomPublicKeys, err := s.checkForAndRecoverCustomKeys(publicKeys)
if err != nil && !s.partialEnabled {
return maps.Keys(recoveredCustomPublicKeys), unrecoverableCustomPublicKeys, err
}

recoveredConventionalPublicKeys, unrecoveredPublicKeys := s.recoverConventionalKeys(publicKeys)

var allRecoveredPublicKeys []beacon.ValidatorPubkey
allRecoveredPublicKeys = append(allRecoveredPublicKeys, maps.Keys(recoveredCustomPublicKeys)...)
allRecoveredPublicKeys = append(allRecoveredPublicKeys, recoveredConventionalPublicKeys...)

for publicKey, err := range unrecoveredPublicKeys {
unrecoverableCustomPublicKeys[publicKey] = err
}

return allRecoveredPublicKeys, unrecoveredPublicKeys, nil
}

func (s *KeyRecoveryManager) checkForAndRecoverCustomKeys(
publicKeys map[beacon.ValidatorPubkey]bool,
) (map[beacon.ValidatorPubkey]bool, map[beacon.ValidatorPubkey]error, error) {

recoveredKeys := make(map[beacon.ValidatorPubkey]bool)
recoveryFailures := make(map[beacon.ValidatorPubkey]error)
var passwords map[string]string

keyFiles, err := s.manager.LoadFiles()
if err != nil {
return recoveredKeys, recoveryFailures, err
}

if len(keyFiles) == 0 {
return recoveredKeys, recoveryFailures, nil
}

passwords, err = s.manager.LoadCustomKeyPasswords()
if err != nil {
return recoveredKeys, recoveryFailures, err
}

for _, file := range keyFiles {
keystore, err := s.manager.ReadCustomKeystore(file)
if err != nil {
if s.partialEnabled {
continue
}
return recoveredKeys, recoveryFailures, err
}

if _, exists := publicKeys[keystore.Pubkey]; !exists {
err := fmt.Errorf("custom keystore for pubkey %s not found in minipool keyset", keystore.Pubkey.Hex())
recoveryFailures[keystore.Pubkey] = err
if s.partialEnabled {
continue
}
return recoveredKeys, recoveryFailures, err
}

formattedPublicKey := strings.ToUpper(utils.RemovePrefix(keystore.Pubkey.Hex()))
password, exists := passwords[formattedPublicKey]
if !exists {
err := fmt.Errorf("custom keystore for pubkey %s needs a password, but none was provided", keystore.Pubkey.Hex())
recoveryFailures[keystore.Pubkey] = err
if s.partialEnabled {
continue
}
return recoveredKeys, recoveryFailures, err
}

privateKey, err := s.manager.DecryptCustomKeystore(keystore, password)
if err != nil {
err := fmt.Errorf("error recreating private key for validator %s: %w", keystore.Pubkey.Hex(), err)
recoveryFailures[keystore.Pubkey] = err
if s.partialEnabled {
continue
}
return recoveredKeys, recoveryFailures, err
}

reconstructedPublicKey := beacon.ValidatorPubkey(privateKey.PublicKey().Marshal())
if reconstructedPublicKey != keystore.Pubkey {
err := fmt.Errorf("private keystore file %s claims to be for validator %s but it's for validator %s", file.Name(), keystore.Pubkey.Hex(), reconstructedPublicKey.Hex())
recoveryFailures[keystore.Pubkey] = err
if s.partialEnabled {
continue
}
return recoveredKeys, recoveryFailures, err
}

if !s.testOnly {
if err := s.manager.StoreValidatorKey(&privateKey, keystore.Path); err != nil {
recoveryFailures[keystore.Pubkey] = err
if s.partialEnabled {
continue
}
return recoveredKeys, recoveryFailures, err
}
}
recoveredKeys[reconstructedPublicKey] = true

delete(publicKeys, keystore.Pubkey)
}

return recoveredKeys, recoveryFailures, nil
}

func (s *KeyRecoveryManager) recoverConventionalKeys(publicKeys map[beacon.ValidatorPubkey]bool) ([]beacon.ValidatorPubkey, map[beacon.ValidatorPubkey]error) {
var recoveredPublicKeys []beacon.ValidatorPubkey
unrecoverablePublicKeys := map[beacon.ValidatorPubkey]error{}

bucketStart := uint64(0)
for {
if bucketStart >= bucketLimit {
break
}
bucketEnd := bucketStart + bucketSize
if bucketEnd > bucketLimit {
bucketEnd = bucketLimit
}

keys, err := s.manager.GetValidatorKeys(bucketStart, bucketEnd-bucketStart)
if err != nil {
return recoveredPublicKeys, map[beacon.ValidatorPubkey]error{beacon.ValidatorPubkey{}: fmt.Errorf("error getting node's validator keys")}
}

for _, validatorKey := range keys {
if exists := publicKeys[validatorKey.PublicKey]; exists {
delete(publicKeys, validatorKey.PublicKey)
if !s.testOnly {
if err := s.manager.SaveValidatorKey(validatorKey); err != nil {
unrecoverablePublicKeys[validatorKey.PublicKey] = err
if s.partialEnabled {
continue
}
return recoveredPublicKeys, unrecoverablePublicKeys
}
}
recoveredPublicKeys = append(recoveredPublicKeys, validatorKey.PublicKey)

} else {
err := fmt.Errorf("keystore for pubkey %s not found in minipool keyset", validatorKey.PublicKey)
unrecoverablePublicKeys[validatorKey.PublicKey] = err
if !s.partialEnabled {
return recoveredPublicKeys, unrecoverablePublicKeys
}
}
}

if len(publicKeys) == 0 {
// All keys have been recovered.
break
}

bucketStart = bucketEnd
}

return recoveredPublicKeys, unrecoverablePublicKeys
}
Loading

0 comments on commit 3aa32ab

Please sign in to comment.