Skip to content

Commit 9bd5510

Browse files
Addressing smartnode-issue-572
This is commit 1/3 to address smartnode-issue-572. This commit includes changes to: 1) add an enable-partial-rebuild flag to the rebuild flow, 2) return error data per key from the API and 3) add logging to inform the user of the outcomes. Update recover-keys.go Update go.mod
1 parent b3fdb9d commit 9bd5510

File tree

13 files changed

+707
-42
lines changed

13 files changed

+707
-42
lines changed

client/wallet.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ func (r *WalletRequester) Masquerade(address common.Address) (*types.ApiResponse
8282
}
8383

8484
// Rebuild the validator keys associated with the wallet
85-
func (r *WalletRequester) Rebuild() (*types.ApiResponse[api.WalletRebuildData], error) {
86-
return client.SendGetRequest[api.WalletRebuildData](r, "rebuild", "Rebuild", nil)
85+
func (r *WalletRequester) Rebuild(enablePartialRebuild bool) (*types.ApiResponse[api.WalletRebuildData], error) {
86+
return client.SendGetRequest[api.WalletRebuildData](r, "rebuild", "Rebuild", map[string]string{"enable-partial-rebuild": strconv.FormatBool(enablePartialRebuild)})
8787
}
8888

8989
// Recover wallet

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ require (
173173
rsc.io/tmplfunc v0.0.3 // indirect
174174
)
175175

176+
require golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
177+
176178
replace github.com/wealdtech/go-merkletree v1.0.1-0.20190605192610-2bb163c2ea2a => github.com/rocket-pool/go-merkletree v1.0.1-0.20220406020931-c262d9b976dd
177179

178180
//replace github.com/rocket-pool/node-manager-core => ../node-manager-core

rocketpool-cli/commands/wallet/commands.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ func RegisterCommands(app *cli.App, name string, aliases []string) {
9494
Name: "rebuild",
9595
Aliases: []string{"b"},
9696
Usage: "Rebuild validator keystores from derived keys",
97+
Flags: []cli.Flag{
98+
enablePartialRebuild,
99+
},
97100
Action: func(c *cli.Context) error {
98101
// Validate args
99102
utils.ValidateArgCount(c, 0)

rocketpool-cli/commands/wallet/rebuild.go

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package wallet
22

33
import (
44
"fmt"
5+
"github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils"
56
"os"
67

78
"github.com/rocket-pool/node-manager-core/wallet"
89
"github.com/rocket-pool/smartnode/v2/rocketpool-cli/client"
9-
"github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils"
1010
"github.com/urfave/cli/v2"
1111
)
1212

@@ -54,41 +54,52 @@ func rebuildWallet(c *cli.Context) error {
5454
}(customKeyPasswordFile)
5555
}
5656

57+
var enablePartialRebuildValue = false
58+
if enablePartialRebuild.Name != "" {
59+
enablePartialRebuildValue = c.Bool(enablePartialRebuild.Name)
60+
}
61+
5762
// Log
5863
fmt.Println("Rebuilding node validator keystores...")
64+
fmt.Printf("Partial rebuild enabled: %s.\n", enablePartialRebuild.Name)
5965

6066
// Rebuild wallet
61-
response, err := rp.Api.Wallet.Rebuild()
62-
if err != nil {
63-
return err
67+
response, _ := rp.Api.Wallet.Rebuild(enablePartialRebuildValue)
68+
69+
// Handle and print failure reasons with associated public keys
70+
if len(response.Data.FailureReasons) > 0 {
71+
fmt.Println("Failure reasons:")
72+
for pubkey, reason := range response.Data.FailureReasons {
73+
fmt.Printf("Public Key: %s - Failure Reason: %s\n", pubkey.Hex(), reason)
74+
}
75+
} else {
76+
fmt.Println("No failures reported.")
6477
}
6578

66-
// Log & return
67-
fmt.Println("The node wallet was successfully rebuilt.")
68-
if len(response.Data.ValidatorKeys) > 0 {
79+
fmt.Println("The response for rebuilding the node wallet was successfully received.")
80+
if len(response.Data.RebuiltValidatorKeys) > 0 {
6981
fmt.Println("Validator keys:")
70-
for _, key := range response.Data.ValidatorKeys {
82+
for _, key := range response.Data.RebuiltValidatorKeys {
7183
fmt.Println(key.Hex())
7284
}
73-
fmt.Println()
74-
} else {
75-
fmt.Println("No validator keys were found.")
76-
}
7785

78-
if !utils.Confirm("Would you like to restart your Validator Client now so it can attest with the recovered keys?") {
79-
fmt.Println("Please restart the Validator Client manually at your earliest convenience to load the keys.")
80-
return nil
81-
}
86+
if !utils.Confirm("Would you like to restart your Validator Client now so it can attest with the recovered keys?") {
87+
fmt.Println("Please restart the Validator Client manually at your earliest convenience to load the keys.")
88+
return nil
89+
}
8290

83-
// Restart the VC
84-
fmt.Println("Restarting Validator Client...")
85-
_, err = rp.Api.Service.RestartVc()
86-
if err != nil {
87-
fmt.Printf("Error restarting Validator Client: %s\n", err.Error())
88-
fmt.Println("Please restart the Validator Client manually at your earliest convenience to load the keys.")
89-
return nil
91+
// Restart the VC
92+
fmt.Println("Restarting Validator Client...")
93+
_, err = rp.Api.Service.RestartVc()
94+
if err != nil {
95+
fmt.Printf("Error restarting Validator Client: %s\n", err.Error())
96+
fmt.Println("Please restart the Validator Client manually at your earliest convenience to load the keys.")
97+
return nil
98+
}
99+
fmt.Println("Validator Client restarted successfully.")
100+
} else {
101+
fmt.Println("No validator keys were found.")
90102
}
91-
fmt.Println("Validator Client restarted successfully.")
92103

93104
return nil
94105
}

rocketpool-cli/commands/wallet/utils.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ var (
5757
Aliases: []string{"a"},
5858
Usage: "If you are recovering a wallet that was not generated by the Smartnode and don't know the derivation path or index of it, enter the address here. The Smartnode will search through its library of paths and indices to try to find it.",
5959
}
60+
enablePartialRebuild = &cli.StringSliceFlag{
61+
Name: "enable-partial-rebuild",
62+
Aliases: []string{"epr"},
63+
Usage: "Allows the wallet rebuild process to partially succeed, responding with public keys for successfully rebuilt targets and errors for rebuild failures",
64+
}
6065
)
6166

6267
// Prompt for a new wallet password

rocketpool-daemon/api/wallet/rebuild.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package wallet
22

33
import (
44
"fmt"
5-
"net/url"
6-
75
"github.com/ethereum/go-ethereum/accounts/abi/bind"
86
"github.com/gorilla/mux"
7+
"github.com/rocket-pool/node-manager-core/utils/input"
8+
key_recovery_manager "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/validator/key-recovery-manager"
9+
"net/url"
910

1011
"github.com/rocket-pool/node-manager-core/api/server"
1112
"github.com/rocket-pool/node-manager-core/api/types"
@@ -24,7 +25,9 @@ func (f *walletRebuildContextFactory) Create(args url.Values) (*walletRebuildCon
2425
c := &walletRebuildContext{
2526
handler: f.handler,
2627
}
27-
return c, nil
28+
inputError := server.ValidateOptionalArg("enable-partial-rebuild", args, input.ValidateBool, &c.enablePartialRebuild, nil)
29+
30+
return c, inputError
2831
}
2932

3033
func (f *walletRebuildContextFactory) RegisterRoute(router *mux.Router) {
@@ -38,12 +41,15 @@ func (f *walletRebuildContextFactory) RegisterRoute(router *mux.Router) {
3841
// ===============
3942

4043
type walletRebuildContext struct {
41-
handler *WalletHandler
44+
handler *WalletHandler
45+
enablePartialRebuild bool
4246
}
4347

4448
func (c *walletRebuildContext) PrepareData(data *api.WalletRebuildData, opts *bind.TransactOpts) (types.ResponseStatus, error) {
4549
sp := c.handler.serviceProvider
4650
vMgr := sp.GetValidatorManager()
51+
partialKeyRecoveryManager := key_recovery_manager.NewPartialRecoveryManager(vMgr)
52+
strictKeyRecoveryManager := key_recovery_manager.NewStrictRecoveryManager(vMgr)
4753

4854
// Requirements
4955
err := sp.RequireWalletReady()
@@ -56,7 +62,11 @@ func (c *walletRebuildContext) PrepareData(data *api.WalletRebuildData, opts *bi
5662
}
5763

5864
// Recover validator keys
59-
data.ValidatorKeys, err = vMgr.RecoverMinipoolKeys(false)
65+
if c.enablePartialRebuild {
66+
data.RebuiltValidatorKeys, data.FailureReasons, err = partialKeyRecoveryManager.RecoverMinipoolKeys()
67+
} else {
68+
data.RebuiltValidatorKeys, data.FailureReasons, err = strictKeyRecoveryManager.RecoverMinipoolKeys()
69+
}
6070
if err != nil {
6171
return types.ResponseStatus_Error, fmt.Errorf("error recovering minipool keys: %w", err)
6272
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package key_recovery_manager
2+
3+
import (
4+
"fmt"
5+
"github.com/rocket-pool/node-manager-core/beacon"
6+
"github.com/rocket-pool/node-manager-core/utils"
7+
"github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/validator"
8+
"golang.org/x/exp/maps"
9+
"strings"
10+
)
11+
12+
type DryRunKeyRecoveryManager struct {
13+
manager *validator.ValidatorManager
14+
}
15+
16+
func NewDryRunKeyRecoveryManager(m *validator.ValidatorManager) *DryRunKeyRecoveryManager {
17+
return &DryRunKeyRecoveryManager{
18+
manager: m,
19+
}
20+
}
21+
22+
func (d *DryRunKeyRecoveryManager) RecoverMinipoolKeys() ([]beacon.ValidatorPubkey, map[beacon.ValidatorPubkey]error, error) {
23+
status, err := d.manager.GetWalletStatus()
24+
if err != nil {
25+
return []beacon.ValidatorPubkey{}, map[beacon.ValidatorPubkey]error{}, err
26+
}
27+
28+
rpNode, mpMgr, err := d.manager.InitializeBindings(status)
29+
if err != nil {
30+
return []beacon.ValidatorPubkey{}, map[beacon.ValidatorPubkey]error{}, err
31+
}
32+
33+
publicKeys, err := d.manager.GetMinipools(rpNode, mpMgr)
34+
if err != nil {
35+
return []beacon.ValidatorPubkey{}, map[beacon.ValidatorPubkey]error{}, err
36+
}
37+
38+
recoveredCustomPublicKeys, unrecoverableCustomPublicKeys, _ := d.checkForAndRecoverCustomKeys(publicKeys)
39+
recoveredPublicKeys, unrecoverablePublicKeys := d.recoverConventionalKeys(publicKeys)
40+
41+
allRecoveredPublicKeys := []beacon.ValidatorPubkey{}
42+
allRecoveredPublicKeys = append(allRecoveredPublicKeys, maps.Keys(recoveredCustomPublicKeys)...)
43+
allRecoveredPublicKeys = append(allRecoveredPublicKeys, recoveredPublicKeys...)
44+
45+
for publicKey, err := range unrecoverablePublicKeys {
46+
unrecoverableCustomPublicKeys[publicKey] = err
47+
}
48+
49+
return allRecoveredPublicKeys, unrecoverablePublicKeys, nil
50+
}
51+
52+
func (d *DryRunKeyRecoveryManager) checkForAndRecoverCustomKeys(publicKeys map[beacon.ValidatorPubkey]bool) (map[beacon.ValidatorPubkey]bool, map[beacon.ValidatorPubkey]error, error) {
53+
54+
recoveredKeys := make(map[beacon.ValidatorPubkey]bool)
55+
recoveryFailures := make(map[beacon.ValidatorPubkey]error)
56+
var passwords map[string]string
57+
58+
keyFiles, err := d.manager.LoadFiles()
59+
if err != nil {
60+
return recoveredKeys, recoveryFailures, err
61+
}
62+
63+
if len(keyFiles) > 0 {
64+
passwords, err = d.manager.LoadCustomKeyPasswords()
65+
if err != nil {
66+
return recoveredKeys, recoveryFailures, err
67+
}
68+
69+
for _, file := range keyFiles {
70+
keystore, err := d.manager.ReadCustomKeystore(file)
71+
if err != nil {
72+
continue
73+
}
74+
75+
if _, exists := publicKeys[keystore.Pubkey]; !exists {
76+
err := fmt.Errorf("custom keystore for pubkey %s not found in minipool keyset", keystore.Pubkey.Hex())
77+
recoveryFailures[keystore.Pubkey] = err
78+
continue
79+
}
80+
81+
formattedPublicKey := strings.ToUpper(utils.RemovePrefix(keystore.Pubkey.Hex()))
82+
password, exists := passwords[formattedPublicKey]
83+
if !exists {
84+
err := fmt.Errorf("custom keystore for pubkey %s needs a password, but none was provided", keystore.Pubkey.Hex())
85+
recoveryFailures[keystore.Pubkey] = err
86+
continue
87+
}
88+
89+
privateKey, err := d.manager.DecryptCustomKeystore(keystore, password)
90+
if err != nil {
91+
err := fmt.Errorf("error recreating private key for validator %s: %w", keystore.Pubkey.Hex(), err)
92+
recoveryFailures[keystore.Pubkey] = err
93+
continue
94+
}
95+
96+
reconstructedPublicKey := beacon.ValidatorPubkey(privateKey.PublicKey().Marshal())
97+
if reconstructedPublicKey != keystore.Pubkey {
98+
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())
99+
recoveryFailures[keystore.Pubkey] = err
100+
continue
101+
}
102+
103+
recoveredKeys[reconstructedPublicKey] = true
104+
}
105+
}
106+
107+
return recoveredKeys, recoveryFailures, nil
108+
}
109+
110+
func (d *DryRunKeyRecoveryManager) recoverConventionalKeys(publicKeys map[beacon.ValidatorPubkey]bool) ([]beacon.ValidatorPubkey, map[beacon.ValidatorPubkey]error) {
111+
recoveredPublicKeys := []beacon.ValidatorPubkey{}
112+
unrecoverablePublicKeys := map[beacon.ValidatorPubkey]error{}
113+
114+
bucketStart := uint64(0)
115+
for {
116+
if bucketStart >= bucketLimit {
117+
break
118+
}
119+
bucketEnd := bucketStart + bucketSize
120+
if bucketEnd > bucketLimit {
121+
bucketEnd = bucketLimit
122+
}
123+
124+
keys, err := d.manager.GetValidatorKeys(bucketStart, bucketEnd-bucketStart)
125+
if err != nil {
126+
continue
127+
}
128+
129+
for _, validatorKey := range keys {
130+
if exists := publicKeys[validatorKey.PublicKey]; exists {
131+
delete(publicKeys, validatorKey.PublicKey)
132+
recoveredPublicKeys = append(recoveredPublicKeys, validatorKey.PublicKey)
133+
} else {
134+
err := fmt.Errorf("keystore for pubkey %s not found in minipool keyset", validatorKey.PublicKey)
135+
unrecoverablePublicKeys[validatorKey.PublicKey] = err
136+
continue
137+
}
138+
}
139+
140+
if len(publicKeys) == 0 {
141+
// All keys have been recovered.
142+
break
143+
}
144+
145+
bucketStart = bucketEnd
146+
}
147+
148+
return recoveredPublicKeys, unrecoverablePublicKeys
149+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package key_recovery_manager
2+
3+
import (
4+
"github.com/rocket-pool/node-manager-core/beacon"
5+
)
6+
7+
type KeyRecoveryManager interface {
8+
RecoverMinipoolKeys() ([]beacon.ValidatorPubkey, map[beacon.ValidatorPubkey]error, error)
9+
}
10+
11+
const (
12+
bucketSize uint64 = 20
13+
bucketLimit uint64 = 2000
14+
)

0 commit comments

Comments
 (0)