From 5e82bc46a36205d54880b63ad488fca49a2e85a4 Mon Sep 17 00:00:00 2001 From: thomaspanf Date: Mon, 27 Apr 2026 00:58:54 -0700 Subject: [PATCH 1/6] Always read address from disk when calling getWallet from CLI --- shared/services/services.go | 83 ++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/shared/services/services.go b/shared/services/services.go index 048cc927a..a64111dda 100644 --- a/shared/services/services.go +++ b/shared/services/services.go @@ -276,50 +276,55 @@ func getAddressManager(cfg *config.RocketPoolConfig) *wallet.AddressManager { } func getWallet(c *cli.Command, cfg *config.RocketPoolConfig, pm *passwords.PasswordManager, am *wallet.AddressManager, ignoreMasquerade bool) (wallet.Wallet, error) { - var err error - initNodeWallet.Do(func() { - var maxFee *big.Int - maxFeeFloat := c.Root().Float64("maxFee") - if maxFeeFloat == 0 { - maxFeeFloat = cfg.Smartnode.ManualMaxFee.Value.(float64) - } - if maxFeeFloat != 0 { - maxFee = eth.GweiToWei(maxFeeFloat) - } + var maxFee *big.Int + maxFeeFloat := c.Root().Float64("maxFee") + if maxFeeFloat == 0 { + maxFeeFloat = cfg.Smartnode.ManualMaxFee.Value.(float64) + } + if maxFeeFloat != 0 { + maxFee = eth.GweiToWei(maxFeeFloat) + } - var maxPriorityFee *big.Int - maxPriorityFeeFloat := c.Root().Float64("maxPrioFee") - if maxPriorityFeeFloat == 0 { - maxPriorityFeeFloat = cfg.Smartnode.PriorityFee.Value.(float64) - } - if maxPriorityFeeFloat != 0 { - maxPriorityFee = eth.GweiToWei(maxPriorityFeeFloat) - } + var maxPriorityFee *big.Int + maxPriorityFeeFloat := c.Root().Float64("maxPrioFee") + if maxPriorityFeeFloat == 0 { + maxPriorityFeeFloat = cfg.Smartnode.PriorityFee.Value.(float64) + } + if maxPriorityFeeFloat != 0 { + maxPriorityFee = eth.GweiToWei(maxPriorityFeeFloat) + } - chainId := cfg.Smartnode.GetChainID() + chainId := cfg.Smartnode.GetChainID() + keychainPath := os.ExpandEnv(cfg.Smartnode.GetValidatorKeychainPath()) - if ignoreMasquerade { + if ignoreMasquerade { + // Node/watchtower path: cached once for the daemon lifetime, always real HD-derived address. + var err error + initNodeWallet.Do(func() { nodeWallet, err = wallet.NewHdWallet(os.ExpandEnv(cfg.Smartnode.GetWalletPath()), chainId, maxFee, maxPriorityFee, 0, pm, am) - } else { - nodeWallet, err = wallet.NewWallet(os.ExpandEnv(cfg.Smartnode.GetNodeAddressPath()), os.ExpandEnv(cfg.Smartnode.GetWalletPath()), chainId, maxFee, maxPriorityFee, 0, pm, am) - } - if err != nil { - return - } + if err != nil { + return + } + nodeWallet.AddKeystore("lighthouse", lhkeystore.NewKeystore(keychainPath, pm)) + nodeWallet.AddKeystore("lodestar", lokeystore.NewKeystore(keychainPath, pm)) + nodeWallet.AddKeystore("nimbus", nmkeystore.NewKeystore(keychainPath, pm)) + nodeWallet.AddKeystore("prysm", prkeystore.NewKeystore(keychainPath, pm)) + nodeWallet.AddKeystore("teku", tkkeystore.NewKeystore(keychainPath, pm)) + }) + return nodeWallet, err + } - // Keystores - lighthouseKeystore := lhkeystore.NewKeystore(os.ExpandEnv(cfg.Smartnode.GetValidatorKeychainPath()), pm) - lodestarKeystore := lokeystore.NewKeystore(os.ExpandEnv(cfg.Smartnode.GetValidatorKeychainPath()), pm) - nimbusKeystore := nmkeystore.NewKeystore(os.ExpandEnv(cfg.Smartnode.GetValidatorKeychainPath()), pm) - prysmKeystore := prkeystore.NewKeystore(os.ExpandEnv(cfg.Smartnode.GetValidatorKeychainPath()), pm) - tekuKeystore := tkkeystore.NewKeystore(os.ExpandEnv(cfg.Smartnode.GetValidatorKeychainPath()), pm) - nodeWallet.AddKeystore("lighthouse", lighthouseKeystore) - nodeWallet.AddKeystore("lodestar", lodestarKeystore) - nodeWallet.AddKeystore("nimbus", nimbusKeystore) - nodeWallet.AddKeystore("prysm", prysmKeystore) - nodeWallet.AddKeystore("teku", tekuKeystore) - }) - return nodeWallet, err + // CLI path: fresh call so masquerade state is always current. + w, err := wallet.NewWallet(os.ExpandEnv(cfg.Smartnode.GetNodeAddressPath()), os.ExpandEnv(cfg.Smartnode.GetWalletPath()), chainId, maxFee, maxPriorityFee, 0, pm, am) + if err != nil { + return nil, err + } + w.AddKeystore("lighthouse", lhkeystore.NewKeystore(keychainPath, pm)) + w.AddKeystore("lodestar", lokeystore.NewKeystore(keychainPath, pm)) + w.AddKeystore("nimbus", nmkeystore.NewKeystore(keychainPath, pm)) + w.AddKeystore("prysm", prkeystore.NewKeystore(keychainPath, pm)) + w.AddKeystore("teku", tkkeystore.NewKeystore(keychainPath, pm)) + return w, nil } func getEthClient(c *cli.Command, cfg *config.RocketPoolConfig) (*ExecutionClientManager, error) { From d8cf9adabe313d301dfc0de152fd70228621be3b Mon Sep 17 00:00:00 2001 From: thomaspanf Date: Thu, 18 Jun 2026 20:18:52 -0700 Subject: [PATCH 2/6] Allow masquerade in node/watchtower loops via --observe flag --- rocketpool-cli/wallet/commands.go | 7 ++- rocketpool-cli/wallet/masquerade.go | 7 ++- rocketpool/api/wallet/masquerade.go | 7 ++- rocketpool/api/wallet/routes.go | 3 +- rocketpool/node/manage-fee-recipient.go | 35 +++++++++++---- rocketpool/node/node.go | 8 +++- rocketpool/watchtower/watchtower.go | 8 +++- shared/services/rocketpool/wallet.go | 8 +++- shared/services/wallet/address-manager.go | 49 ++++++++++++++++++--- shared/services/wallet/masquerade-wallet.go | 4 +- shared/services/wallet/wallet.go | 6 +-- shared/utils/rp/fee-recipient.go | 3 ++ 12 files changed, 113 insertions(+), 32 deletions(-) diff --git a/rocketpool-cli/wallet/commands.go b/rocketpool-cli/wallet/commands.go index 82b2b04d0..36fc5881d 100644 --- a/rocketpool-cli/wallet/commands.go +++ b/rocketpool-cli/wallet/commands.go @@ -292,6 +292,11 @@ func RegisterCommands(app *cli.Command, name string, aliases []string) { Aliases: []string{"a"}, Usage: "Specify an address you'd like you masquerade as", }, + &cli.BoolFlag{ + Name: "observe", + Aliases: []string{"o"}, + Usage: "Apply masquerade to the node and watchtower loops (requires daemon restart)", + }, }, Action: func(ctx context.Context, c *cli.Command) error { @@ -303,7 +308,7 @@ func RegisterCommands(app *cli.Command, name string, aliases []string) { } // Run - return masquerade(c.String("address"), c.Bool("yes")) + return masquerade(c.String("address"), c.Bool("yes"), c.Bool("observe")) }, }, diff --git a/rocketpool-cli/wallet/masquerade.go b/rocketpool-cli/wallet/masquerade.go index 9acdf9c87..226e2cb25 100644 --- a/rocketpool-cli/wallet/masquerade.go +++ b/rocketpool-cli/wallet/masquerade.go @@ -9,7 +9,7 @@ import ( "github.com/rocket-pool/smartnode/shared/utils/cli/prompt" ) -func masquerade(addressFlag string, yes bool) error { +func masquerade(addressFlag string, yes bool, observe bool) error { // Get RP client rp := rocketpool.NewClient() defer rp.Close() @@ -34,12 +34,15 @@ func masquerade(addressFlag string, yes bool) error { } // Call API - _, err = rp.Masquerade(address) + _, err = rp.Masquerade(address, observe) if err != nil { return fmt.Errorf("error running masquerade: %w", err) } fmt.Printf("Your node is now masquerading as address %s.\n", color.LightBlue(address.Hex())) + if observe { + fmt.Println("Restart the daemon for the node and watchtower loops to pick up the masquerade address.") + } return nil } diff --git a/rocketpool/api/wallet/masquerade.go b/rocketpool/api/wallet/masquerade.go index f3286fdef..d5e67f88a 100644 --- a/rocketpool/api/wallet/masquerade.go +++ b/rocketpool/api/wallet/masquerade.go @@ -10,7 +10,7 @@ import ( "github.com/rocket-pool/smartnode/shared/types/api" ) -func masquerade(c *cli.Command, address common.Address) (*api.MasqueradeResponse, error) { +func masquerade(c *cli.Command, address common.Address, observe bool) (*api.MasqueradeResponse, error) { // Get services w, err := services.GetWallet(c) @@ -18,9 +18,8 @@ func masquerade(c *cli.Command, address common.Address) (*api.MasqueradeResponse return nil, err } - err = w.MasqueradeAsAddress(address) - if err != nil { - return nil, fmt.Errorf("Error masquerading as address %s", address.Hex()) + if err := w.MasqueradeAsAddress(address, observe); err != nil { + return nil, fmt.Errorf("error masquerading as address %s: %w", address.Hex(), err) } response := api.MasqueradeResponse{} diff --git a/rocketpool/api/wallet/routes.go b/rocketpool/api/wallet/routes.go index 3d8059935..584947d11 100644 --- a/rocketpool/api/wallet/routes.go +++ b/rocketpool/api/wallet/routes.go @@ -79,7 +79,8 @@ func RegisterRoutes(mux *http.ServeMux, c *cli.Command) { mux.HandleFunc("/api/wallet/masquerade", func(w http.ResponseWriter, r *http.Request) { address := common.HexToAddress(r.FormValue("address")) - resp, err := masquerade(c, address) + observe := r.FormValue("observe") == "true" + resp, err := masquerade(c, address, observe) apiutils.WriteResponse(w, resp, err) }) diff --git a/rocketpool/node/manage-fee-recipient.go b/rocketpool/node/manage-fee-recipient.go index f4fceddde..8347bcdaa 100644 --- a/rocketpool/node/manage-fee-recipient.go +++ b/rocketpool/node/manage-fee-recipient.go @@ -23,13 +23,14 @@ import ( // Manage fee recipient task type manageFeeRecipient struct { - c *cli.Command - log log.ColorLogger - cfg *config.RocketPoolConfig - w wallet.Wallet - rp *rocketpool.RocketPool - d *client.Client - bc beacon.Client + c *cli.Command + log log.ColorLogger + cfg *config.RocketPoolConfig + w wallet.Wallet + rp *rocketpool.RocketPool + d *client.Client + bc beacon.Client + stateManager *state.NetworkStateManager } // Create manage fee recipient task @@ -58,7 +59,7 @@ func newManageFeeRecipient(c *cli.Command, logger log.ColorLogger) (*manageFeeRe } // Return task - return &manageFeeRecipient{ + task := &manageFeeRecipient{ c: c, log: logger, cfg: cfg, @@ -66,7 +67,9 @@ func newManageFeeRecipient(c *cli.Command, logger log.ColorLogger) (*manageFeeRe rp: rp, d: d, bc: bc, - }, nil + } + task.stateManager = state.NewNetworkStateManager(rp, cfg.Smartnode.GetStateManagerContracts(), bc, &task.log) + return task, nil } @@ -87,6 +90,20 @@ func (m *manageFeeRecipient) run(state *state.NetworkState) error { return err } + // Fee recipient is always managed for the real node (HD wallet on disk), not the masquerade + // address. In observe mode, the global state is keyed for the masquerade address, + // so fetch a dedicated state for the real node. + am := wallet.NewAddressManager(m.cfg.Smartnode.GetNodeAddressPath()) + masqAddress, masqErr := am.LoadAddress() + if masqErr == nil && am.IsObserve() { + m.log.Printlnf("Node is masquerading as %s; fee recipient management always targets the real node (%s) stored on disk.", masqAddress.Hex(), nodeAccount.Address.Hex()) + var stateErr error + state, stateErr = m.stateManager.GetHeadStateForNode(nodeAccount.Address) + if stateErr != nil { + return fmt.Errorf("error getting network state for real node %s: %w", nodeAccount.Address.Hex(), stateErr) + } + } + // Get the fee recipient info for the node feeRecipientInfo, err := rputils.GetFeeRecipientInfo(m.rp, m.bc, nodeAccount.Address, state) if err != nil { diff --git a/rocketpool/node/node.go b/rocketpool/node/node.go index c4caec20c..0128bcd0b 100644 --- a/rocketpool/node/node.go +++ b/rocketpool/node/node.go @@ -22,6 +22,7 @@ import ( "github.com/rocket-pool/smartnode/shared/services/alerting" "github.com/rocket-pool/smartnode/shared/services/connectivity" "github.com/rocket-pool/smartnode/shared/services/state" + "github.com/rocket-pool/smartnode/shared/services/wallet" "github.com/rocket-pool/smartnode/shared/services/wallet/keystore/lighthouse" "github.com/rocket-pool/smartnode/shared/services/wallet/keystore/nimbus" "github.com/rocket-pool/smartnode/shared/services/wallet/keystore/prysm" @@ -148,7 +149,12 @@ func run(c *cli.Command) error { if err != nil { return err } - w, err := services.GetHdWallet(c) + var w wallet.Wallet + if wallet.CheckObserveMode(cfg.Smartnode.GetNodeAddressPath()) { + w, err = services.GetWallet(c) + } else { + w, err = services.GetHdWallet(c) + } if err != nil { return err } diff --git a/rocketpool/watchtower/watchtower.go b/rocketpool/watchtower/watchtower.go index 941cad2cb..beb621d61 100644 --- a/rocketpool/watchtower/watchtower.go +++ b/rocketpool/watchtower/watchtower.go @@ -21,6 +21,7 @@ import ( "github.com/rocket-pool/smartnode/bindings/utils" "github.com/rocket-pool/smartnode/rocketpool/watchtower/collectors" "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/services/wallet" "github.com/rocket-pool/smartnode/shared/services/beacon" "github.com/rocket-pool/smartnode/shared/services/state" "github.com/rocket-pool/smartnode/shared/utils/log" @@ -120,7 +121,12 @@ func run(c *cli.Command) error { if err != nil { return err } - w, err := services.GetHdWallet(c) + var w wallet.Wallet + if wallet.CheckObserveMode(cfg.Smartnode.GetNodeAddressPath()) { + w, err = services.GetWallet(c) + } else { + w, err = services.GetHdWallet(c) + } if err != nil { return err } diff --git a/shared/services/rocketpool/wallet.go b/shared/services/rocketpool/wallet.go index f21075725..b5c41bcfe 100644 --- a/shared/services/rocketpool/wallet.go +++ b/shared/services/rocketpool/wallet.go @@ -221,8 +221,12 @@ func (c *Client) ExportWallet() (api.ExportWalletResponse, error) { } // Set the node address to an arbitrary address -func (c *Client) Masquerade(address common.Address) (api.MasqueradeResponse, error) { - responseBytes, err := c.callHTTPAPI("POST", "/api/wallet/masquerade", url.Values{"address": {address.Hex()}}) +func (c *Client) Masquerade(address common.Address, observe bool) (api.MasqueradeResponse, error) { + observeStr := "false" + if observe { + observeStr = "true" + } + responseBytes, err := c.callHTTPAPI("POST", "/api/wallet/masquerade", url.Values{"address": {address.Hex()}, "observe": {observeStr}}) if err != nil { return api.MasqueradeResponse{}, fmt.Errorf("Could not masquerade wallet: %w", err) } diff --git a/shared/services/wallet/address-manager.go b/shared/services/wallet/address-manager.go index 7c4fc29e2..3d05ebac3 100644 --- a/shared/services/wallet/address-manager.go +++ b/shared/services/wallet/address-manager.go @@ -7,16 +7,23 @@ import ( "os" "github.com/ethereum/go-ethereum/common" + "github.com/goccy/go-json" ) const ( addressFileMode fs.FileMode = 0664 ) +type addressFile struct { + Address string `json:"address"` + Observe bool `json:"observe"` +} + // Simple class to wrap the node's address file type AddressManager struct { path string address common.Address + observe bool } // Creates a new address manager @@ -26,9 +33,11 @@ func NewAddressManager(path string) *AddressManager { } } -// Gets the address saved on disk. Returns false if the address file doesn't exist. +// Gets the address saved on disk. Returns empty address if the address file doesn't exist. +// Also loads the observe flag as a side effect; check IsObserve() after calling. func (m *AddressManager) LoadAddress() (common.Address, error) { m.address = common.Address{} + m.observe = false _, err := os.Stat(m.path) if errors.Is(err, fs.ErrNotExist) { @@ -41,7 +50,15 @@ func (m *AddressManager) LoadAddress() (common.Address, error) { if err != nil { return common.Address{}, fmt.Errorf("error loading address file [%s]: %w", m.path, err) } - m.address = common.HexToAddress(string(bytes)) + + var af addressFile + if err := json.Unmarshal(bytes, &af); err != nil { + // backward compat: plain hex address written by older versions + m.address = common.HexToAddress(string(bytes)) + } else { + m.address = common.HexToAddress(af.Address) + m.observe = af.Observe + } return m.address, nil } @@ -50,12 +67,21 @@ func (m *AddressManager) GetAddress() common.Address { return m.address } -// Sets the node address and saves it to disk -func (m *AddressManager) SetAndSaveAddress(newAddress common.Address) error { +// Get the cached observe flag +func (m *AddressManager) IsObserve() bool { + return m.observe +} + +// Sets the node address and observe flag, and saves both to disk +func (m *AddressManager) SetAndSaveAddress(newAddress common.Address, observe bool) error { m.address = newAddress - bytes := []byte(newAddress.Hex()) - err := os.WriteFile(m.path, bytes, addressFileMode) + m.observe = observe + af := addressFile{Address: newAddress.Hex(), Observe: observe} + bytes, err := json.Marshal(af) if err != nil { + return fmt.Errorf("error encoding address file: %w", err) + } + if err := os.WriteFile(m.path, bytes, addressFileMode); err != nil { return fmt.Errorf("error writing address file [%s] to disk: %w", m.path, err) } return nil @@ -68,5 +94,16 @@ func (m *AddressManager) DeleteAddressFile() error { return fmt.Errorf("error deleting address file [%s]: %w", m.path, err) } m.address = common.Address{} + m.observe = false return nil } + +// CheckObserveMode reads the address file at path and returns true if observe mode is active. +// Returns false if the file doesn't exist, can't be read, or observe is not set. +func CheckObserveMode(path string) bool { + am := NewAddressManager(path) + if _, err := am.LoadAddress(); err != nil { + return false + } + return am.IsObserve() +} diff --git a/shared/services/wallet/masquerade-wallet.go b/shared/services/wallet/masquerade-wallet.go index 628bfb4c3..fcf29618a 100644 --- a/shared/services/wallet/masquerade-wallet.go +++ b/shared/services/wallet/masquerade-wallet.go @@ -76,8 +76,8 @@ func (w *masqueradeWallet) GetAddress() (common.Address, error) { } // Change the node's effective address to a different one. Node and watchtower tasks will continue to run normally using the loaded wallet. -func (w *masqueradeWallet) MasqueradeAsAddress(newAddress common.Address) error { - return w.am.SetAndSaveAddress(newAddress) +func (w *masqueradeWallet) MasqueradeAsAddress(newAddress common.Address, observe bool) error { + return w.am.SetAndSaveAddress(newAddress, observe) } // End a masquerade, restoring your node's effective address back to your wallet address if one is loaded diff --git a/shared/services/wallet/wallet.go b/shared/services/wallet/wallet.go index 1c5d813c7..9aaeee8d8 100644 --- a/shared/services/wallet/wallet.go +++ b/shared/services/wallet/wallet.go @@ -63,7 +63,7 @@ type Wallet interface { String() (string, error) TestRecoverValidatorKey(pubkey rptypes.ValidatorPubkey, startIndex uint) (uint, error) TestRecovery(derivationPath string, walletIndex uint, mnemonic string) error - MasqueradeAsAddress(address common.Address) error + MasqueradeAsAddress(address common.Address, observe bool) error EndMasquerade() error GetAddress() (common.Address, error) IsNodeMasquerading() bool @@ -202,8 +202,8 @@ func (w *hdWallet) GetAddress() (common.Address, error) { } // Change the node's effective address to a different one. Node and watchtower tasks will continue to run normally using the loaded wallet. -func (w *hdWallet) MasqueradeAsAddress(newAddress common.Address) error { - return w.am.SetAndSaveAddress(newAddress) +func (w *hdWallet) MasqueradeAsAddress(newAddress common.Address, observe bool) error { + return w.am.SetAndSaveAddress(newAddress, observe) } // End a masquerade, restoring your node's effective address back to your wallet address if one is loaded diff --git a/shared/utils/rp/fee-recipient.go b/shared/utils/rp/fee-recipient.go index af14a3d4c..220129612 100644 --- a/shared/utils/rp/fee-recipient.go +++ b/shared/utils/rp/fee-recipient.go @@ -33,6 +33,9 @@ func GetFeeRecipientInfo(rp *rocketpool.RocketPool, bc beacon.Client, nodeAddres } mpd := state.NodeDetailsByAddress[nodeAddress] + if mpd == nil { + return nil, fmt.Errorf("node %s not found in network state", nodeAddress.Hex()) + } // Get info info.SmoothingPoolAddress = state.NetworkDetails.SmoothingPoolAddress From 2e0667d7fc27f70f5ded24ddb2f6cb45631dc00d Mon Sep 17 00:00:00 2001 From: thomaspanf Date: Thu, 18 Jun 2026 20:53:13 -0700 Subject: [PATCH 3/6] Add logging to node/watchtower when observing. Add additional info and prompting to wallet masquerade and wallet status commands when observing --- rocketpool-cli/wallet/masquerade.go | 20 ++++++++++++++++---- rocketpool-cli/wallet/status.go | 14 ++++++++++++-- rocketpool/api/wallet/status.go | 6 ++++++ rocketpool/node/node.go | 10 +++++++++- rocketpool/watchtower/watchtower.go | 10 +++++++++- shared/types/api/wallet.go | 1 + 6 files changed, 53 insertions(+), 8 deletions(-) diff --git a/rocketpool-cli/wallet/masquerade.go b/rocketpool-cli/wallet/masquerade.go index 226e2cb25..fc6cd507c 100644 --- a/rocketpool-cli/wallet/masquerade.go +++ b/rocketpool-cli/wallet/masquerade.go @@ -28,9 +28,21 @@ func masquerade(addressFlag string, yes bool, observe bool) error { } // Prompt for confirmation - if !yes && !prompt.Confirm("Are you sure you want to masquerade as %s?", color.LightBlue(address.Hex())) { - fmt.Println("Cancelled.") - return nil + if observe { + fmt.Println(color.Yellow("Observe mode is enabled. Please read the following carefully:")) + fmt.Println(" - The node and watchtower will use the masquerade address instead of your real node address.") + fmt.Println(" - Your fee recipient will remain set to your real node wallet address") + fmt.Println(" - Run `rocketpool wallet end-masquerade` and restart the node/watchtower daemons when you have finished observing.") + fmt.Println() + if !yes && !prompt.Confirm("I understand the above. Enable observe mode for %s?", color.LightBlue(address.Hex())) { + fmt.Println("Cancelled.") + return nil + } + } else { + if !yes && !prompt.Confirm("Are you sure you want to masquerade as %s?", color.LightBlue(address.Hex())) { + fmt.Println("Cancelled.") + return nil + } } // Call API @@ -41,7 +53,7 @@ func masquerade(addressFlag string, yes bool, observe bool) error { fmt.Printf("Your node is now masquerading as address %s.\n", color.LightBlue(address.Hex())) if observe { - fmt.Println("Restart the daemon for the node and watchtower loops to pick up the masquerade address.") + fmt.Println("Restart the node and watchtower daemons to observe as the masquerade address.") } return nil diff --git a/rocketpool-cli/wallet/status.go b/rocketpool-cli/wallet/status.go index 7226bf9d9..60722af18 100644 --- a/rocketpool-cli/wallet/status.go +++ b/rocketpool-cli/wallet/status.go @@ -39,12 +39,23 @@ func getStatus() error { if status.IsMasquerading { if status.NodeAddress != emptyAddress { fmt.Printf("The node wallet is initialized, but you are currently masquerading as %s\n", color.LightBlue(status.AccountAddress.Hex())) - fmt.Printf("Wallet Address: %s\n", status.NodeAddress) + fmt.Printf("Wallet Address: %s\n", color.LightBlue(status.NodeAddress.Hex())) color.YellowPrintln("Due to this mismatch, the node cannot submit transactions. Use the command 'rocketpool wallet end-masquerade' to end masquerading and restore your wallet address.") } else { fmt.Printf("The node wallet has not been initialized, but you are currently masquerading as %s\n", color.LightBlue(status.AccountAddress.Hex())) color.YellowPrintln("The node cannot submit transactions. Use the command 'rocketpool wallet end-masquerade' to end masquerading.") } + if status.IsObserve { + fmt.Println() + fmt.Printf("The node is in %s, observing address %s.\n", color.Yellow("observe mode"), color.LightBlue(status.AccountAddress.Hex())) + if status.NodeAddress != emptyAddress { + fmt.Printf("Wallet Address (fee recipient): %s\n", color.LightBlue(status.NodeAddress.Hex())) + } + fmt.Println(" - The node and watchtower loops are using the masquerade address.") + fmt.Println(" - Transactions will not be submitted.") + fmt.Println(" - Your fee recipient remains set to your real node wallet address") + color.YellowPrintln("Run 'rocketpool wallet end-masquerade' and restart the node/watchtower daemons when you have finished observing.") + } } else { // Not Masquerading if status.WalletInitialized { @@ -56,7 +67,6 @@ func getStatus() error { } } - fmt.Println() return nil } diff --git a/rocketpool/api/wallet/status.go b/rocketpool/api/wallet/status.go index 2cb0a9d56..e21c19b06 100644 --- a/rocketpool/api/wallet/status.go +++ b/rocketpool/api/wallet/status.go @@ -4,12 +4,17 @@ import ( "github.com/urfave/cli/v3" "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/services/wallet" "github.com/rocket-pool/smartnode/shared/types/api" ) func getStatus(c *cli.Command) (*api.WalletStatusResponse, error) { // Get services + cfg, err := services.GetConfig(c) + if err != nil { + return nil, err + } pm, err := services.GetPasswordManager(c) if err != nil { return nil, err @@ -24,6 +29,7 @@ func getStatus(c *cli.Command) (*api.WalletStatusResponse, error) { // Get wallet type response.IsMasquerading = w.IsNodeMasquerading() + response.IsObserve = wallet.CheckObserveMode(cfg.Smartnode.GetNodeAddressPath()) // Get wallet status if response.IsMasquerading { diff --git a/rocketpool/node/node.go b/rocketpool/node/node.go index 0128bcd0b..2fa154d21 100644 --- a/rocketpool/node/node.go +++ b/rocketpool/node/node.go @@ -149,8 +149,9 @@ func run(c *cli.Command) error { if err != nil { return err } + isObserveMode := wallet.CheckObserveMode(cfg.Smartnode.GetNodeAddressPath()) var w wallet.Wallet - if wallet.CheckObserveMode(cfg.Smartnode.GetNodeAddressPath()) { + if isObserveMode { w, err = services.GetWallet(c) } else { w, err = services.GetHdWallet(c) @@ -175,6 +176,13 @@ func run(c *cli.Command) error { return fmt.Errorf("error getting node account: %w", err) } + if isObserveMode { + red := color.New(color.FgHiRed).SprintFunc() + fmt.Println(red("Node daemon is observing address " + nodeAccount.Address.Hex() + ".")) + fmt.Println(red("Transactions will not be submitted. Fee recipient management targets your real node address.")) + fmt.Println(red("Run `rocketpool wallet end-masquerade` and restart the node/watchtower daemons when you have finished observing.")) + } + // Initialize loggers errorLog := log.NewColorLogger(ErrorColor) updateLog := log.NewColorLogger(UpdateColor) diff --git a/rocketpool/watchtower/watchtower.go b/rocketpool/watchtower/watchtower.go index beb621d61..6df3638ae 100644 --- a/rocketpool/watchtower/watchtower.go +++ b/rocketpool/watchtower/watchtower.go @@ -121,8 +121,9 @@ func run(c *cli.Command) error { if err != nil { return err } + isObserveMode := wallet.CheckObserveMode(cfg.Smartnode.GetNodeAddressPath()) var w wallet.Wallet - if wallet.CheckObserveMode(cfg.Smartnode.GetNodeAddressPath()) { + if isObserveMode { w, err = services.GetWallet(c) } else { w, err = services.GetHdWallet(c) @@ -167,6 +168,13 @@ func run(c *cli.Command) error { return fmt.Errorf("error getting node account: %w", err) } + if isObserveMode { + red := color.New(color.FgHiRed).SprintFunc() + fmt.Println(red("Watchtower daemon is observing address " + nodeAccount.Address.Hex() + ".")) + fmt.Println(red("Transactions will not be submitted.")) + fmt.Println(red("Run `rocketpool wallet end-masquerade` and restart the node/watchtower daemons when you have finished observing.")) + } + // Initialize tasks respondChallenges, err := newRespondChallenges(c, log.NewColorLogger(RespondChallengesColor), m) if err != nil { diff --git a/shared/types/api/wallet.go b/shared/types/api/wallet.go index 976689f22..c42d6fbf9 100644 --- a/shared/types/api/wallet.go +++ b/shared/types/api/wallet.go @@ -29,6 +29,7 @@ type WalletStatusResponse struct { // NodeAddress always represents the address drived from the wallet stored on disk NodeAddress common.Address `json:"nodeAddress"` IsMasquerading bool `json:"isMasquerading"` + IsObserve bool `json:"isObserve"` } type SetPasswordResponse struct { From b48e9d09a792ace3e2aa25b3ca79868899b2cc1a Mon Sep 17 00:00:00 2001 From: thomaspanf Date: Mon, 22 Jun 2026 16:10:56 -0700 Subject: [PATCH 4/6] Use GetWallet instead of GetHdWallet in node/watchtower tasks --- rocketpool/node/defend-pdao-props.go | 2 +- rocketpool/node/distribute-minipools.go | 2 +- rocketpool/node/download-reward-trees.go | 2 +- rocketpool/node/metrics-exporter.go | 2 +- rocketpool/node/provision-express-tickets.go | 2 +- rocketpool/node/set-latest-delegate.go | 2 +- rocketpool/node/verify-pdao-props.go | 2 +- rocketpool/watchtower/check-solo-migrations.go | 2 +- rocketpool/watchtower/dissolve-timed-out-minipools.go | 2 +- rocketpool/watchtower/finalize-pdao-proposals.go | 2 +- rocketpool/watchtower/respond-challenges.go | 2 +- rocketpool/watchtower/submit-network-balances.go | 2 +- rocketpool/watchtower/submit-rewards-tree-stateless.go | 2 +- rocketpool/watchtower/submit-rpl-price.go | 2 +- rocketpool/watchtower/submit-scrub-minipools.go | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/rocketpool/node/defend-pdao-props.go b/rocketpool/node/defend-pdao-props.go index 815c17830..d77eebfef 100644 --- a/rocketpool/node/defend-pdao-props.go +++ b/rocketpool/node/defend-pdao-props.go @@ -54,7 +54,7 @@ func newDefendPdaoProps(c *cli.Command, logger log.ColorLogger) (*defendPdaoProp if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/node/distribute-minipools.go b/rocketpool/node/distribute-minipools.go index 241954804..140abc342 100644 --- a/rocketpool/node/distribute-minipools.go +++ b/rocketpool/node/distribute-minipools.go @@ -52,7 +52,7 @@ func newDistributeMinipools(c *cli.Command, logger log.ColorLogger) (*distribute if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/node/download-reward-trees.go b/rocketpool/node/download-reward-trees.go index e5f0b5a5f..837dbb97b 100644 --- a/rocketpool/node/download-reward-trees.go +++ b/rocketpool/node/download-reward-trees.go @@ -38,7 +38,7 @@ func newDownloadRewardsTrees(c *cli.Command, logger log.ColorLogger) (*downloadR if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/node/metrics-exporter.go b/rocketpool/node/metrics-exporter.go index 35371b742..13783c12d 100644 --- a/rocketpool/node/metrics-exporter.go +++ b/rocketpool/node/metrics-exporter.go @@ -25,7 +25,7 @@ func runMetricsServer(ctx context.Context, c *cli.Command, logger log.ColorLogge if err != nil { return err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return err } diff --git a/rocketpool/node/provision-express-tickets.go b/rocketpool/node/provision-express-tickets.go index 693a7a2c0..625078c34 100644 --- a/rocketpool/node/provision-express-tickets.go +++ b/rocketpool/node/provision-express-tickets.go @@ -43,7 +43,7 @@ func newProvisionExpressTickets(c *cli.Command, logger log.ColorLogger) (*provis if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/node/set-latest-delegate.go b/rocketpool/node/set-latest-delegate.go index 7c3644486..fd08d1da6 100644 --- a/rocketpool/node/set-latest-delegate.go +++ b/rocketpool/node/set-latest-delegate.go @@ -50,7 +50,7 @@ func newSetUseLatestDelegate(c *cli.Command, logger log.ColorLogger) (*setUseLat if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/node/verify-pdao-props.go b/rocketpool/node/verify-pdao-props.go index 46564fd16..8a96ff60e 100644 --- a/rocketpool/node/verify-pdao-props.go +++ b/rocketpool/node/verify-pdao-props.go @@ -88,7 +88,7 @@ func newVerifyPdaoProps(c *cli.Command, logger log.ColorLogger) (*verifyPdaoProp if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/watchtower/check-solo-migrations.go b/rocketpool/watchtower/check-solo-migrations.go index c69ec038f..ba1be21eb 100644 --- a/rocketpool/watchtower/check-solo-migrations.go +++ b/rocketpool/watchtower/check-solo-migrations.go @@ -55,7 +55,7 @@ func newCheckSoloMigrations(c *cli.Command, logger log.ColorLogger, errorLogger if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/watchtower/dissolve-timed-out-minipools.go b/rocketpool/watchtower/dissolve-timed-out-minipools.go index 510fcb097..fa7c66d9a 100644 --- a/rocketpool/watchtower/dissolve-timed-out-minipools.go +++ b/rocketpool/watchtower/dissolve-timed-out-minipools.go @@ -43,7 +43,7 @@ func newDissolveTimedOutMinipools(c *cli.Command, logger log.ColorLogger) (*diss if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/watchtower/finalize-pdao-proposals.go b/rocketpool/watchtower/finalize-pdao-proposals.go index cdf61b2e0..1ef42cf1a 100644 --- a/rocketpool/watchtower/finalize-pdao-proposals.go +++ b/rocketpool/watchtower/finalize-pdao-proposals.go @@ -37,7 +37,7 @@ func newFinalizePdaoProposals(c *cli.Command, logger log.ColorLogger) (*finalize if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/watchtower/respond-challenges.go b/rocketpool/watchtower/respond-challenges.go index 8fac3c09d..2a1173243 100644 --- a/rocketpool/watchtower/respond-challenges.go +++ b/rocketpool/watchtower/respond-challenges.go @@ -36,7 +36,7 @@ func newRespondChallenges(c *cli.Command, logger log.ColorLogger, m *state.Netwo if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/watchtower/submit-network-balances.go b/rocketpool/watchtower/submit-network-balances.go index 45ffe3995..d8869cc4c 100644 --- a/rocketpool/watchtower/submit-network-balances.go +++ b/rocketpool/watchtower/submit-network-balances.go @@ -158,7 +158,7 @@ func newSubmitNetworkBalances(c *cli.Command, logger log.ColorLogger, errorLogge if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/watchtower/submit-rewards-tree-stateless.go b/rocketpool/watchtower/submit-rewards-tree-stateless.go index 2f4a8f5bc..0b182d5c9 100644 --- a/rocketpool/watchtower/submit-rewards-tree-stateless.go +++ b/rocketpool/watchtower/submit-rewards-tree-stateless.go @@ -59,7 +59,7 @@ func newSubmitRewardsTree_Stateless(c *cli.Command, logger log.ColorLogger, erro if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/watchtower/submit-rpl-price.go b/rocketpool/watchtower/submit-rpl-price.go index ad8c0d08b..f6fd1842b 100644 --- a/rocketpool/watchtower/submit-rpl-price.go +++ b/rocketpool/watchtower/submit-rpl-price.go @@ -304,7 +304,7 @@ func newSubmitRplPrice(c *cli.Command, logger log.ColorLogger, errorLogger log.C if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } diff --git a/rocketpool/watchtower/submit-scrub-minipools.go b/rocketpool/watchtower/submit-scrub-minipools.go index 5c5ed9175..36a138c25 100644 --- a/rocketpool/watchtower/submit-scrub-minipools.go +++ b/rocketpool/watchtower/submit-scrub-minipools.go @@ -92,7 +92,7 @@ func newSubmitScrubMinipools(c *cli.Command, logger log.ColorLogger, errorLogger if err != nil { return nil, err } - w, err := services.GetHdWallet(c) + w, err := services.GetWallet(c) if err != nil { return nil, err } From f6eac3ea932288d9beb4a9a6232faca74f40e6b6 Mon Sep 17 00:00:00 2001 From: thomaspanf Date: Mon, 22 Jun 2026 16:10:56 -0700 Subject: [PATCH 5/6] Block oDAO members from using --observe --- rocketpool/api/wallet/masquerade.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/rocketpool/api/wallet/masquerade.go b/rocketpool/api/wallet/masquerade.go index d5e67f88a..9c16120d6 100644 --- a/rocketpool/api/wallet/masquerade.go +++ b/rocketpool/api/wallet/masquerade.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/urfave/cli/v3" + "github.com/rocket-pool/smartnode/bindings/dao/trustednode" "github.com/rocket-pool/smartnode/shared/services" "github.com/rocket-pool/smartnode/shared/types/api" ) @@ -18,6 +19,28 @@ func masquerade(c *cli.Command, address common.Address, observe bool) (*api.Masq return nil, err } + if observe { + hdw, err := services.GetHdWallet(c) + if err != nil { + return nil, err + } + rp, err := services.GetRocketPool(c) + if err != nil { + return nil, err + } + nodeAccount, err := hdw.GetNodeAccount() + if err != nil { + return nil, err + } + isMember, err := trustednode.GetMemberExists(rp, nodeAccount.Address, nil) + if err != nil { + return nil, fmt.Errorf("error checking Oracle DAO membership: %w", err) + } + if isMember { + return nil, fmt.Errorf("Observe mode is not available for Oracle DAO nodes: oDAO duties would stop running while observing") + } + } + if err := w.MasqueradeAsAddress(address, observe); err != nil { return nil, fmt.Errorf("error masquerading as address %s: %w", address.Hex(), err) } From a15c0fd600c031e779884ccdda92d57e69b5de14 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:53:33 -0300 Subject: [PATCH 6/6] Fix lint --- rocketpool/watchtower/watchtower.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocketpool/watchtower/watchtower.go b/rocketpool/watchtower/watchtower.go index 6df3638ae..6d839c067 100644 --- a/rocketpool/watchtower/watchtower.go +++ b/rocketpool/watchtower/watchtower.go @@ -21,9 +21,9 @@ import ( "github.com/rocket-pool/smartnode/bindings/utils" "github.com/rocket-pool/smartnode/rocketpool/watchtower/collectors" "github.com/rocket-pool/smartnode/shared/services" - "github.com/rocket-pool/smartnode/shared/services/wallet" "github.com/rocket-pool/smartnode/shared/services/beacon" "github.com/rocket-pool/smartnode/shared/services/state" + "github.com/rocket-pool/smartnode/shared/services/wallet" "github.com/rocket-pool/smartnode/shared/utils/log" )