diff --git a/go.mod b/go.mod index be5d2e0cf..6bfcddd34 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/prysmaticlabs/prysm/v5 v5.0.3 github.com/rivo/tview v0.0.0-20230208211350-7dfff1ce7854 // DO NOT UPGRADE github.com/rocket-pool/node-manager-core v0.5.0 - github.com/rocket-pool/rocketpool-go/v2 v2.0.0-b2.0.20240709170030-c27aeb5fb99b + github.com/rocket-pool/rocketpool-go/v2 v2.0.0-b2.0.20241009181157-ebb8c8b0bf54 github.com/shirou/gopsutil/v3 v3.24.3 github.com/tyler-smith/go-bip39 v1.1.0 github.com/wealdtech/go-ens/v3 v3.6.0 diff --git a/go.sum b/go.sum index 88f992faf..631f51e65 100644 --- a/go.sum +++ b/go.sum @@ -485,8 +485,8 @@ github.com/rocket-pool/go-merkletree v1.0.1-0.20220406020931-c262d9b976dd h1:p9K github.com/rocket-pool/go-merkletree v1.0.1-0.20220406020931-c262d9b976dd/go.mod h1:UE9fof8P7iESVtLn1K9CTSkNRYVFHZHlf96RKbU33kA= github.com/rocket-pool/node-manager-core v0.5.0 h1:98PnHb67mgOKTHMQlRql5KINYM+5NGYV3n4GYChZuec= github.com/rocket-pool/node-manager-core v0.5.0/go.mod h1:Clii5aca9PvR4HoAlUs8dh2OsJbDDnJ4yL5EaQE1gSo= -github.com/rocket-pool/rocketpool-go/v2 v2.0.0-b2.0.20240709170030-c27aeb5fb99b h1:39UmJzNR71/OMIzblEY9wq+3nojGa/gQOJJpLBa6XcE= -github.com/rocket-pool/rocketpool-go/v2 v2.0.0-b2.0.20240709170030-c27aeb5fb99b/go.mod h1:pcY43H/m5pjr7zacrsKVaXnXfKKi1UV08VDPUwxbJkc= +github.com/rocket-pool/rocketpool-go/v2 v2.0.0-b2.0.20241009181157-ebb8c8b0bf54 h1:869F7HFTkCHUNxAj6WwtapWEL8DiVfy918DviPz57dY= +github.com/rocket-pool/rocketpool-go/v2 v2.0.0-b2.0.20241009181157-ebb8c8b0bf54/go.mod h1:pcY43H/m5pjr7zacrsKVaXnXfKKi1UV08VDPUwxbJkc= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= diff --git a/rocketpool-daemon/common/rewards/utils.go b/rocketpool-daemon/common/rewards/utils.go index 470229fd8..b9b1f43e6 100644 --- a/rocketpool-daemon/common/rewards/utils.go +++ b/rocketpool-daemon/common/rewards/utils.go @@ -2,6 +2,7 @@ package rewards import ( "context" + "errors" "fmt" "io" "math" @@ -374,7 +375,7 @@ func DownloadRewardsFile(cfg *config.SmartNodeConfig, i *sharedtypes.IntervalInf errBuilder.WriteString(fmt.Sprintf("Downloading files with timeout %v failed.\n", timeout)) } - return fmt.Errorf(errBuilder.String()) + return errors.New(errBuilder.String()) } // Gets the start slot for the given interval diff --git a/rocketpool-daemon/node/auto-init-voting-power.go b/rocketpool-daemon/node/auto-init-voting-power.go new file mode 100644 index 000000000..cfeb19ddd --- /dev/null +++ b/rocketpool-daemon/node/auto-init-voting-power.go @@ -0,0 +1,155 @@ +package node + +import ( + "context" + "fmt" + "log/slog" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/rocket-pool/node-manager-core/beacon" + "github.com/rocket-pool/node-manager-core/eth" + "github.com/rocket-pool/node-manager-core/log" + "github.com/rocket-pool/node-manager-core/node/wallet" + "github.com/rocket-pool/rocketpool-go/v2/node" + "github.com/rocket-pool/rocketpool-go/v2/rocketpool" + "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/gas" + "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/services" + "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/state" + "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/tx" + "github.com/rocket-pool/smartnode/v2/shared/config" + "github.com/rocket-pool/smartnode/v2/shared/keys" +) + +type AutoInitVotingPower struct { + ctx context.Context + sp *services.ServiceProvider + logger *slog.Logger + cfg *config.SmartNodeConfig + w *wallet.Wallet + rp *rocketpool.RocketPool + bc beacon.IBeaconClient + gasThreshold float64 + maxFee *big.Int + maxPriorityFee *big.Int + nodeAddress common.Address +} + +// Auto Initialize Vote Power +func NewAutoInitVotingPower(ctx context.Context, sp *services.ServiceProvider, logger *log.Logger) *AutoInitVotingPower { + cfg := sp.GetConfig() + log := logger.With(slog.String(keys.TaskKey, "Auto Initialize Vote Power")) + maxFee, maxPriorityFee := getAutoTxInfo(cfg, log) + return &AutoInitVotingPower{ + ctx: ctx, + sp: sp, + logger: log, + cfg: cfg, + w: sp.GetWallet(), + rp: sp.GetRocketPool(), + bc: sp.GetBeaconClient(), + gasThreshold: cfg.AutoInitVPThreshold.Value, + maxFee: maxFee, + maxPriorityFee: maxPriorityFee, + } +} + +func (t *AutoInitVotingPower) Run(state *state.NetworkState) error { + // Log + t.logger.Info("Checking for node initialized voting power...") + + // Create Node Binding + t.nodeAddress, _ = t.w.GetAddress() + node, err := node.NewNode(t.rp, t.nodeAddress) + if err != nil { + return fmt.Errorf("error creating node binding for node %s: %w", t.nodeAddress.Hex(), err) + } + + // Check if voting is initialized + err = t.rp.Query(nil, nil, node.IsVotingInitialized) + if err != nil { + return fmt.Errorf("error checking if voting is initialized for node %s: %w", t.nodeAddress.Hex(), err) + } + votingInitialized := node.IsVotingInitialized.Get() + + // Create the tx and submit if voting isn't initialized + if !votingInitialized { + txSubmission, err := t.createInitializeVotingTx() + if err != nil { + return fmt.Errorf("error preparing submission to initialize voting for node %s: %w", t.nodeAddress.Hex(), err) + } + err = t.initializeVotingPower(txSubmission) + if err != nil { + return fmt.Errorf("error initializing voting power for node %s: %w", t.nodeAddress.Hex(), err) + } + return nil + } + + return nil +} + +func (t *AutoInitVotingPower) createInitializeVotingTx() (*eth.TransactionSubmission, error) { + // Get transactor + opts, err := t.w.GetTransactor() + if err != nil { + return nil, err + } + + // Bindings + node, err := node.NewNode(t.rp, t.nodeAddress) + if err != nil { + return nil, fmt.Errorf("error creating node %s binding: %w", t.nodeAddress.Hex(), err) + } + // Get the tx info + txInfo, err := node.InitializeVoting(opts) + if err != nil { + return nil, fmt.Errorf("error estimating the gas required to initialize voting for node %s: %w", t.nodeAddress.Hex(), err) + } + if txInfo.SimulationResult.SimulationError != "" { + return nil, fmt.Errorf("simulating initialize voting tx for node %s failed: %s", t.nodeAddress.Hex(), txInfo.SimulationResult.SimulationError) + } + + submission, err := eth.CreateTxSubmissionFromInfo(txInfo, nil) + if err != nil { + return nil, fmt.Errorf("error creating submission to initialize voting for node %s: %w", t.nodeAddress.Hex(), err) + } + return submission, nil +} + +func (t *AutoInitVotingPower) initializeVotingPower(submission *eth.TransactionSubmission) error { + // Get transactor + opts, err := t.w.GetTransactor() + if err != nil { + return err + } + + // Get the max fee + maxFee := t.maxFee + if maxFee == nil || maxFee.Uint64() == 0 { + maxFee, err = gas.GetMaxFeeWeiForDaemon(t.logger) + if err != nil { + return err + } + } + + // Lower the priority fee when the suggested maxfee is lower than the user requested priority fee + if maxFee.Cmp(t.maxPriorityFee) < 0 { + t.maxPriorityFee = new(big.Int).Div(maxFee, big.NewInt(2)) + } + + opts.GasFeeCap = maxFee + opts.GasTipCap = t.maxPriorityFee + + // Print the gas info + if !gas.PrintAndCheckGasInfo(submission.TxInfo.SimulationResult, true, t.gasThreshold, t.logger, opts.GasFeeCap, 0) { + return nil + } + + // Print TX info and wait for them to be included in a block + err = tx.PrintAndWaitForTransaction(t.cfg, t.rp, t.logger, submission.TxInfo, opts) + if err != nil { + return err + } + + return nil +} diff --git a/rocketpool-daemon/node/node.go b/rocketpool-daemon/node/node.go index 0242dcc88..fc69cdf2e 100644 --- a/rocketpool-daemon/node/node.go +++ b/rocketpool-daemon/node/node.go @@ -75,6 +75,7 @@ type TaskLoop struct { reduceBonds *ReduceBonds defendPdaoProps *DefendPdaoProps verifyPdaoProps *VerifyPdaoProps + autoInitVotingPower *AutoInitVotingPower // Watchtower metrics scrubCollector *wc.ScrubCollector @@ -127,6 +128,11 @@ func NewTaskLoop(sp *services.ServiceProvider, wg *sync.WaitGroup) *TaskLoop { t.verifyPdaoProps = NewVerifyPdaoProps(t.ctx, t.sp, t.logger) } + // Create auto init vp if the threshold is above 0 + if t.cfg.AutoInitVPThreshold.Value != 0 { + t.autoInitVotingPower = NewAutoInitVotingPower(t.ctx, t.sp, t.logger) + } + return t } @@ -368,6 +374,15 @@ func (t *TaskLoop) runTasks() bool { return true } } + // Run the auto vote initilization check + if t.autoInitVotingPower != nil { + if err := t.autoInitVotingPower.Run(state); err != nil { + t.logger.Error(err.Error()) + } + if utils.SleepWithCancel(t.ctx, taskCooldown) { + return true + } + } // Run the minipool stake check if err := t.stakePrelaunchMinipools.Run(state); err != nil { diff --git a/shared/config/ids/ids.go b/shared/config/ids/ids.go index 47898e220..42bef07a2 100644 --- a/shared/config/ids/ids.go +++ b/shared/config/ids/ids.go @@ -28,6 +28,7 @@ const ( CheckpointRetentionLimitID string = "checkpointRetentionLimit" RecordsPathID string = "recordsPath" VerifyProposalsID string = "verifyProposals" + AutoInitVPThreshold string = "autoInitVPThreshold" // Subconfig IDs LoggingID string = "logging" diff --git a/shared/config/smartnode-config.go b/shared/config/smartnode-config.go index 86d798bea..0ff532d52 100644 --- a/shared/config/smartnode-config.go +++ b/shared/config/smartnode-config.go @@ -44,6 +44,7 @@ type SmartNodeConfig struct { CheckpointRetentionLimit config.Parameter[uint64] RecordsPath config.Parameter[string] VerifyProposals config.Parameter[bool] + AutoInitVPThreshold config.Parameter[float64] // Logging Logging *config.LoggerConfig @@ -441,6 +442,22 @@ func NewSmartNodeConfig(rpDir string, isNativeMode bool) *SmartNodeConfig { config.Network_All: false, }, }, + + AutoInitVPThreshold: config.Parameter[float64]{ + ParameterCommon: &config.ParameterCommon{ + ID: ids.AutoInitVPThreshold, + Name: "Auto-Init Vote Power Gas Threshold", + Description: "The Smartnode will regularly check if the node has initialized voting power and attempt to initialize voting power if it isn't initialized.\n\n" + + "This threshold is a limit (in gwei) you can set on this automatic transaction; your node will not attempt to initialize voting power if the network suggested fee is below this limit.\n\n" + + "A value of 0 will disable this task. Disable this if your node was registered post-houston or your vote power is already initialized.\n\n", + AffectsContainers: []config.ContainerID{config.ContainerID_Daemon}, + CanBeBlank: false, + OverwriteOnUpgrade: false, + }, + Default: map[config.Network]float64{ + config.Network_All: float64(5), + }, + }, } // Create the subconfigs @@ -477,6 +494,7 @@ func (cfg *SmartNodeConfig) GetParameters() []config.IParameter { &cfg.Network, &cfg.ClientMode, &cfg.VerifyProposals, + &cfg.AutoInitVPThreshold, &cfg.AutoTxMaxFee, &cfg.MaxPriorityFee, &cfg.AutoTxGasThreshold,