diff --git a/shared/services/config/rocket-pool-config.go b/shared/services/config/rocket-pool-config.go index 50596c4e0..dffdf2a45 100644 --- a/shared/services/config/rocket-pool-config.go +++ b/shared/services/config/rocket-pool-config.go @@ -57,6 +57,9 @@ type RocketPoolConfig struct { IsNativeMode bool `yaml:"-"` + Offline config.Parameter `yaml:"offline"` + NodeAddress config.Parameter `yaml:"nodeAddress"` + // Execution client settings ExecutionClientMode config.Parameter `yaml:"executionClientMode,omitempty"` ExecutionClient config.Parameter `yaml:"executionClient,omitempty"` @@ -213,6 +216,30 @@ func NewRocketPoolConfig(rpDir string, isNativeMode bool) *RocketPoolConfig { }}, }, + Offline: config.Parameter{ + ID: "offline", + Name: "Offline Mode", + Description: "Enable this if you would like to keep your node operator key offline.", + Type: config.ParameterType_Bool, + Default: map[config.Network]interface{}{config.Network_All: false}, + AffectsContainers: []config.ContainerID{config.ContainerID_Api, config.ContainerID_Node}, + EnvironmentVariables: []string{}, + CanBeBlank: false, + OverwriteOnUpgrade: false, + }, + + NodeAddress: config.Parameter{ + ID: "nodeAddress", + Name: "Node Operator Address", + Description: "The address of the node operator key. Only used in conjunction with offline mode.", + Type: config.ParameterType_String, + Default: map[config.Network]interface{}{config.Network_All: ""}, + AffectsContainers: []config.ContainerID{config.ContainerID_Api, config.ContainerID_Node, config.ContainerID_Watchtower}, + EnvironmentVariables: []string{}, + CanBeBlank: true, + OverwriteOnUpgrade: false, + }, + UseFallbackClients: config.Parameter{ ID: "useFallbackClients", Name: "Use Fallback Clients", @@ -527,6 +554,8 @@ func (cfg *RocketPoolConfig) GetParameters() []*config.Parameter { &cfg.ReconnectDelay, &cfg.ConsensusClientMode, &cfg.ConsensusClient, + &cfg.Offline, + &cfg.NodeAddress, &cfg.ExternalConsensusClient, &cfg.EnableMetrics, &cfg.EnableODaoMetrics, diff --git a/shared/services/services.go b/shared/services/services.go index c4cd82a0e..9abf39e92 100644 --- a/shared/services/services.go +++ b/shared/services/services.go @@ -216,6 +216,17 @@ func getWallet(c *cli.Context, cfg *config.RocketPoolConfig, pm *passwords.Passw return } + // Offline mode + if cfg.Offline.Value == true { + operatorAddress := cfg.NodeAddress.Value.(string) + + if operatorAddress == "" { + fmt.Println("Offline mode enabled, but no operator address specified in config file, skipping offline setup...") + return + } + nodeWallet.SetOffline(operatorAddress) + } + // Keystores lighthouseKeystore := lhkeystore.NewKeystore(os.ExpandEnv(cfg.Smartnode.GetValidatorKeychainPath()), pm) lodestarKeystore := lokeystore.NewKeystore(os.ExpandEnv(cfg.Smartnode.GetValidatorKeychainPath()), pm) diff --git a/shared/services/wallet/node.go b/shared/services/wallet/node.go index 6619a835a..4fdd44e89 100644 --- a/shared/services/wallet/node.go +++ b/shared/services/wallet/node.go @@ -3,6 +3,7 @@ package wallet import ( "context" "crypto/ecdsa" + "encoding/json" "errors" "fmt" "math/big" @@ -10,6 +11,8 @@ import ( "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" ) @@ -21,6 +24,10 @@ func (w *Wallet) GetNodeAccount() (accounts.Account, error) { return accounts.Account{}, errors.New("Wallet is not initialized") } + if w.Offline() { + return *w.nodeAddress, nil + } + // Get private key privateKey, path, err := w.getNodePrivateKey() if err != nil { @@ -53,6 +60,25 @@ func (w *Wallet) GetNodeAccountTransactor() (*bind.TransactOpts, error) { return nil, errors.New("Wallet is not initialized") } + if w.Offline() { + var transactor bind.TransactOpts + transactor.From = w.nodeAddress.Address + transactor.Context = context.Background() + transactor.GasFeeCap = w.maxFee + transactor.GasTipCap = w.maxPriorityFee + transactor.GasLimit = w.gasLimit + transactor.Signer = func(address common.Address, tx *types.Transaction) (*types.Transaction, error) { + txJSON, err := json.MarshalIndent(tx, "", " ") + if err != nil { + return tx, err + } + fmt.Printf("Offline mode: this transaction would have been signed by %s:\n%s\n", address.String(), string(txJSON)) + return nil, fmt.Errorf("Offline mode - transaction not signed") + } + + return &transactor, nil + } + // Get private key privateKey, _, err := w.getNodePrivateKey() if err != nil { diff --git a/shared/services/wallet/wallet.go b/shared/services/wallet/wallet.go index 50dfd18f4..fd17df0b7 100644 --- a/shared/services/wallet/wallet.go +++ b/shared/services/wallet/wallet.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/google/uuid" @@ -39,6 +40,7 @@ type Wallet struct { pm *passwords.PasswordManager encryptor *eth2ks.Encryptor chainID *big.Int + offline bool // Encrypted store ws *walletStore @@ -48,6 +50,7 @@ type Wallet struct { mk *hdkeychain.ExtendedKey // Node key cache + nodeAddress *accounts.Account nodeKey *ecdsa.PrivateKey nodeKeyPath string @@ -100,6 +103,18 @@ func NewWallet(walletPath string, chainId uint, maxFee *big.Int, maxPriorityFee } +func (w *Wallet) Offline() bool { + return w.offline +} + +// This function designates a wallet as offline +func (w *Wallet) SetOffline(address string) { + w.offline = true + w.nodeAddress = &accounts.Account{ + Address: common.HexToAddress(address), + } +} + // Gets the wallet's chain ID func (w *Wallet) GetChainID() *big.Int { copy := big.NewInt(0).Set(w.chainID) @@ -113,7 +128,7 @@ func (w *Wallet) AddKeystore(name string, ks keystore.Keystore) { // Check if the wallet has been initialized func (w *Wallet) IsInitialized() bool { - return (w.ws != nil && w.seed != nil && w.mk != nil) + return w.offline || (w.ws != nil && w.seed != nil && w.mk != nil) } // Attempt to initialize the wallet if not initialized and return status