-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Addressing smartnode-issue-572 #621
base: v2
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,11 +2,11 @@ package wallet | |||||||||
|
||||||||||
import ( | ||||||||||
"fmt" | ||||||||||
"github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils" | ||||||||||
SolezOfScience marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
"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" | ||||||||||
) | ||||||||||
|
||||||||||
|
@@ -54,41 +54,52 @@ func rebuildWallet(c *cli.Context) error { | |||||||||
}(customKeyPasswordFile) | ||||||||||
} | ||||||||||
|
||||||||||
var enablePartialRebuildValue = false | ||||||||||
if enablePartialRebuild.Name != "" { | ||||||||||
enablePartialRebuildValue = c.Bool(enablePartialRebuild.Name) | ||||||||||
} | ||||||||||
|
||||||||||
// Log | ||||||||||
fmt.Println("Rebuilding node validator keystores...") | ||||||||||
fmt.Printf("Partial rebuild enabled: %s.\n", enablePartialRebuild.Name) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Did you mean to printf the value of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for pointing this out. Frankly I found the use of these oddly confusing. For example, recover.go, it checks if the name is non-empty before using it but given the definition of the flags, I don't see how it could be empty. Regardless, I'll definitely update this log. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well that looks like a bug :( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha - once you left that comment I figured that may be the case. I'll take a note. |
||||||||||
|
||||||||||
// Rebuild wallet | ||||||||||
response, err := rp.Api.Wallet.Rebuild() | ||||||||||
if err != nil { | ||||||||||
return err | ||||||||||
response, _ := rp.Api.Wallet.Rebuild(enablePartialRebuildValue) | ||||||||||
SolezOfScience marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
// Handle and print failure reasons with associated public keys | ||||||||||
if len(response.Data.FailureReasons) > 0 { | ||||||||||
fmt.Println("Failure reasons:") | ||||||||||
SolezOfScience marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
for pubkey, reason := range response.Data.FailureReasons { | ||||||||||
fmt.Printf("Public Key: %s - Failure Reason: %s\n", pubkey.Hex(), reason) | ||||||||||
} | ||||||||||
} else { | ||||||||||
fmt.Println("No failures reported.") | ||||||||||
} | ||||||||||
|
||||||||||
// Log & return | ||||||||||
fmt.Println("The node wallet was successfully rebuilt.") | ||||||||||
if len(response.Data.ValidatorKeys) > 0 { | ||||||||||
fmt.Println("The response for rebuilding the node wallet was successfully received.") | ||||||||||
SolezOfScience marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
if len(response.Data.RebuiltValidatorKeys) > 0 { | ||||||||||
fmt.Println("Validator keys:") | ||||||||||
for _, key := range response.Data.ValidatorKeys { | ||||||||||
for _, key := range response.Data.RebuiltValidatorKeys { | ||||||||||
fmt.Println(key.Hex()) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
bit more ergonomic to enumerate the returned keys (starting with 1) so users can quickly see that the expected number of validators were recovered. |
||||||||||
} | ||||||||||
fmt.Println() | ||||||||||
} else { | ||||||||||
fmt.Println("No validator keys were found.") | ||||||||||
} | ||||||||||
|
||||||||||
if !utils.Confirm("Would you like to restart your Validator Client now so it can attest with the recovered keys?") { | ||||||||||
fmt.Println("Please restart the Validator Client manually at your earliest convenience to load the keys.") | ||||||||||
return nil | ||||||||||
} | ||||||||||
if !utils.Confirm("Would you like to restart your Validator Client now so it can attest with the recovered keys?") { | ||||||||||
fmt.Println("Please restart the Validator Client manually at your earliest convenience to load the keys.") | ||||||||||
return nil | ||||||||||
} | ||||||||||
|
||||||||||
// Restart the VC | ||||||||||
fmt.Println("Restarting Validator Client...") | ||||||||||
_, err = rp.Api.Service.RestartVc() | ||||||||||
if err != nil { | ||||||||||
fmt.Printf("Error restarting Validator Client: %s\n", err.Error()) | ||||||||||
fmt.Println("Please restart the Validator Client manually at your earliest convenience to load the keys.") | ||||||||||
return nil | ||||||||||
// Restart the VC | ||||||||||
fmt.Println("Restarting Validator Client...") | ||||||||||
_, err = rp.Api.Service.RestartVc() | ||||||||||
if err != nil { | ||||||||||
fmt.Printf("Error restarting Validator Client: %s\n", err.Error()) | ||||||||||
fmt.Println("Please restart the Validator Client manually at your earliest convenience to load the keys.") | ||||||||||
return nil | ||||||||||
} | ||||||||||
fmt.Println("Validator Client restarted successfully.") | ||||||||||
} else { | ||||||||||
fmt.Println("No validator keys were found.") | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a slight preference to see this moved up to L84 and the conditional inverted. An early return can prevent the need for an |
||||||||||
} | ||||||||||
fmt.Println("Validator Client restarted successfully.") | ||||||||||
|
||||||||||
return nil | ||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package key_recovery_manager | ||
|
||
import ( | ||
"fmt" | ||
"github.com/rocket-pool/node-manager-core/beacon" | ||
"github.com/rocket-pool/node-manager-core/utils" | ||
"github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/validator" | ||
"golang.org/x/exp/maps" | ||
"strings" | ||
) | ||
|
||
type DryRunKeyRecoveryManager struct { | ||
manager *validator.ValidatorManager | ||
} | ||
|
||
func NewDryRunKeyRecoveryManager(m *validator.ValidatorManager) *DryRunKeyRecoveryManager { | ||
return &DryRunKeyRecoveryManager{ | ||
manager: m, | ||
} | ||
} | ||
|
||
func (d *DryRunKeyRecoveryManager) RecoverMinipoolKeys() ([]beacon.ValidatorPubkey, map[beacon.ValidatorPubkey]error, error) { | ||
status, err := d.manager.GetWalletStatus() | ||
if err != nil { | ||
return []beacon.ValidatorPubkey{}, map[beacon.ValidatorPubkey]error{}, err | ||
} | ||
|
||
rpNode, mpMgr, err := d.manager.InitializeBindings(status) | ||
if err != nil { | ||
return []beacon.ValidatorPubkey{}, map[beacon.ValidatorPubkey]error{}, err | ||
} | ||
|
||
publicKeys, err := d.manager.GetMinipools(rpNode, mpMgr) | ||
if err != nil { | ||
return []beacon.ValidatorPubkey{}, map[beacon.ValidatorPubkey]error{}, err | ||
} | ||
|
||
recoveredCustomPublicKeys, unrecoverableCustomPublicKeys, _ := d.checkForAndRecoverCustomKeys(publicKeys) | ||
recoveredPublicKeys, unrecoverablePublicKeys := d.recoverConventionalKeys(publicKeys) | ||
|
||
allRecoveredPublicKeys := []beacon.ValidatorPubkey{} | ||
allRecoveredPublicKeys = append(allRecoveredPublicKeys, maps.Keys(recoveredCustomPublicKeys)...) | ||
allRecoveredPublicKeys = append(allRecoveredPublicKeys, recoveredPublicKeys...) | ||
|
||
for publicKey, err := range unrecoverablePublicKeys { | ||
unrecoverableCustomPublicKeys[publicKey] = err | ||
} | ||
|
||
return allRecoveredPublicKeys, unrecoverablePublicKeys, nil | ||
} | ||
|
||
func (d *DryRunKeyRecoveryManager) 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 := d.manager.LoadFiles() | ||
if err != nil { | ||
return recoveredKeys, recoveryFailures, err | ||
} | ||
|
||
if len(keyFiles) > 0 { | ||
SolezOfScience marked this conversation as resolved.
Show resolved
Hide resolved
|
||
passwords, err = d.manager.LoadCustomKeyPasswords() | ||
if err != nil { | ||
return recoveredKeys, recoveryFailures, err | ||
} | ||
|
||
for _, file := range keyFiles { | ||
keystore, err := d.manager.ReadCustomKeystore(file) | ||
if err != nil { | ||
continue | ||
} | ||
|
||
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 | ||
continue | ||
} | ||
|
||
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 | ||
continue | ||
} | ||
|
||
privateKey, err := d.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 | ||
continue | ||
} | ||
|
||
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 | ||
continue | ||
} | ||
|
||
recoveredKeys[reconstructedPublicKey] = true | ||
} | ||
} | ||
|
||
return recoveredKeys, recoveryFailures, nil | ||
} | ||
|
||
func (d *DryRunKeyRecoveryManager) recoverConventionalKeys(publicKeys map[beacon.ValidatorPubkey]bool) ([]beacon.ValidatorPubkey, map[beacon.ValidatorPubkey]error) { | ||
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 := d.manager.GetValidatorKeys(bucketStart, bucketEnd-bucketStart) | ||
if err != nil { | ||
continue | ||
} | ||
|
||
for _, validatorKey := range keys { | ||
if exists := publicKeys[validatorKey.PublicKey]; exists { | ||
delete(publicKeys, validatorKey.PublicKey) | ||
recoveredPublicKeys = append(recoveredPublicKeys, validatorKey.PublicKey) | ||
} else { | ||
err := fmt.Errorf("keystore for pubkey %s not found in minipool keyset", validatorKey.PublicKey) | ||
unrecoverablePublicKeys[validatorKey.PublicKey] = err | ||
continue | ||
} | ||
} | ||
|
||
if len(publicKeys) == 0 { | ||
// All keys have been recovered. | ||
break | ||
} | ||
|
||
bucketStart = bucketEnd | ||
} | ||
|
||
return recoveredPublicKeys, unrecoverablePublicKeys | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package key_recovery_manager | ||
|
||
import ( | ||
"github.com/rocket-pool/node-manager-core/beacon" | ||
) | ||
|
||
type KeyRecoveryManager interface { | ||
RecoverMinipoolKeys() ([]beacon.ValidatorPubkey, map[beacon.ValidatorPubkey]error, error) | ||
SolezOfScience marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
const ( | ||
bucketSize uint64 = 20 | ||
bucketLimit uint64 = 2000 | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interesting. bool types are native json types- do we need to use a string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe I was just following convention on this one. For example, the
skip-validator-key-recovery
parameter uses a similar code flow to translate the string to boolean.I could see the perspective that this is a bit silly since the parameter is a string, gets converted to a bool to call the method then is immediately converted back to a string for the payload. I also cringe at the idea of passing a string around which is serving as a boolean, so neither way is elegant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SendGetRequest method takes a map[string]string. Refactoring this down could make this more generic, but for now requires parameters as strings.