diff --git a/bindings/utils/state/megapool.go b/bindings/utils/state/megapool.go index fd79f1133..84f378537 100644 --- a/bindings/utils/state/megapool.go +++ b/bindings/utils/state/megapool.go @@ -165,6 +165,127 @@ func unpackValidatorInfoFromGlobalIndex(contract *rocketpool.Contract, data []by return validator, nil } +// Get all validators for a single megapool, via its own local index -- unlike +// GetAllMegapoolValidators, which walks the network-wide global index on RocketMegapoolManager. +func GetNodeMegapoolValidators(rp *rocketpool.RocketPool, contracts *NetworkContracts, megapoolAddress common.Address) ([]megapool.ValidatorInfoFromGlobalIndex, error) { + opts := &bind.CallOpts{ + BlockNumber: contracts.ElBlockNumber, + } + + if contracts.Multicaller == nil { + return nil, fmt.Errorf("multicaller is nil") + } + + mp, err := megapool.NewMegaPoolV1(rp, megapoolAddress, opts) + if err != nil { + return nil, fmt.Errorf("error creating megapool contract for %s: %w", megapoolAddress.Hex(), err) + } + + validatorCount, err := mp.GetValidatorCount(opts) + if err != nil { + return nil, fmt.Errorf("error getting validator count for megapool %s: %w", megapoolAddress.Hex(), err) + } + + count := int(validatorCount) + validators := make([]megapool.ValidatorInfoFromGlobalIndex, count) + if count == 0 { + return validators, nil + } + + // Capture values before launching goroutines + multicallerAddress := contracts.Multicaller.ContractAddress + megapoolContract := mp.GetContract() + + var wg errgroup.Group + wg.SetLimit(threadLimit) + for i := 0; i < count; i += megapoolValidatorsBatchSize { + i := i + m := min(i+megapoolValidatorsBatchSize, count) + + wg.Go(func() error { + mc, err := multicall.NewMultiCaller(rp.Client, multicallerAddress) + if err != nil { + return err + } + var dummy *big.Int + for j := i; j < m; j++ { + err = mc.AddCall(megapoolContract, &dummy, "getValidatorInfoAndPubkey", uint32(j)) + if err != nil { + return fmt.Errorf("error adding validator info call for local index %d: %w", j, err) + } + } + responses, err := mc.Execute(true, opts) + if err != nil { + return fmt.Errorf("error executing megapool validator multicall for %s: %w", megapoolAddress.Hex(), err) + } + for idx, response := range responses { + if !response.Status { + return fmt.Errorf("megapool validator call failed for local index %d", i+idx) + } + validator, err := unpackValidatorInfoAndPubkey(megapoolContract, response.ReturnDataRaw) + if err != nil { + return fmt.Errorf("error unpacking validator info for local index %d: %w", i+idx, err) + } + validator.MegapoolAddress = megapoolAddress + validator.ValidatorId = uint32(i + idx) + validators[i+idx] = validator + } + return nil + }) + } + if err := wg.Wait(); err != nil { + return nil, fmt.Errorf("error getting megapool validators for %s: %w", megapoolAddress.Hex(), err) + } + + return validators, nil +} + +// Manually unpack a getValidatorInfoAndPubkey response (nested structs don't work with UnpackIntoInterface) +func unpackValidatorInfoAndPubkey(contract *rocketpool.Contract, data []byte) (megapool.ValidatorInfoFromGlobalIndex, error) { + iface, err := contract.ABI.Unpack("getValidatorInfoAndPubkey", data) + if err != nil { + return megapool.ValidatorInfoFromGlobalIndex{}, err + } + + src := iface[0].(struct { + LastAssignmentTime uint32 `json:"lastAssignmentTime"` + LastRequestedValue uint32 `json:"lastRequestedValue"` + LastRequestedBond uint32 `json:"lastRequestedBond"` + DepositValue uint32 `json:"depositValue"` + + Staked bool `json:"staked"` + Exited bool `json:"exited"` + InQueue bool `json:"inQueue"` + InPrestake bool `json:"inPrestake"` + ExpressUsed bool `json:"expressUsed"` + Dissolved bool `json:"dissolved"` + Exiting bool `json:"exiting"` + Locked bool `json:"locked"` + + ExitBalance uint64 `json:"exitBalance"` + LockedTime uint64 `json:"lockedTime"` + }) + + var validator megapool.ValidatorInfoFromGlobalIndex + validator.Pubkey = iface[1].([]byte) + validator.ValidatorInfo.LastAssignmentTime = src.LastAssignmentTime + validator.ValidatorInfo.LastRequestedValue = src.LastRequestedValue + validator.ValidatorInfo.LastRequestedBond = src.LastRequestedBond + validator.ValidatorInfo.DepositValue = src.DepositValue + validator.ValidatorInfo.Staked = src.Staked + validator.ValidatorInfo.Exited = src.Exited + validator.ValidatorInfo.InQueue = src.InQueue + validator.ValidatorInfo.InPrestake = src.InPrestake + validator.ValidatorInfo.ExpressUsed = src.ExpressUsed + validator.ValidatorInfo.Dissolved = src.Dissolved + validator.ValidatorInfo.Exiting = src.Exiting + validator.ValidatorInfo.Locked = src.Locked + validator.ValidatorInfo.ExitBalance = src.ExitBalance + validator.ValidatorInfo.LockedTime = src.LockedTime + + return validator, nil +} + // Get multiple megapool details at once using batched multicalls func GetBulkMegapoolDetails(rp *rocketpool.RocketPool, contracts *NetworkContracts, megapoolAddresses []common.Address) (map[common.Address]NativeMegapoolDetails, error) { opts := &bind.CallOpts{ diff --git a/rocketpool-cli/node/rewards.go b/rocketpool-cli/node/rewards.go index 57aeb0609..0fa900031 100644 --- a/rocketpool-cli/node/rewards.go +++ b/rocketpool-cli/node/rewards.go @@ -2,13 +2,11 @@ package node import ( "fmt" - "math/big" "time" cliutils "github.com/rocket-pool/smartnode/rocketpool-cli/cli" "github.com/rocket-pool/smartnode/rocketpool-cli/cli/color" "github.com/rocket-pool/smartnode/rocketpool-cli/cli/prompt" - "github.com/rocket-pool/smartnode/shared/math" rprewards "github.com/rocket-pool/smartnode/shared/services/rewards" "github.com/rocket-pool/smartnode/shared/services/rocketpool" ) @@ -87,17 +85,6 @@ func getRewards(yes bool) error { return err } - beaconBalances, err := rp.GetValidatorMapAndBalances() - if err != nil { - return err - } - // Add the megapool unskimmed beacon rewards, if available. - // NodeBond and NodeShareOfCLBalance are nil for nodes without a megapool (legacy minipools only). - if beaconBalances.NodeBond != nil && beaconBalances.NodeShareOfCLBalance != nil { - megapoolUnskimmedRewards := new(big.Int).Sub(beaconBalances.NodeBond, beaconBalances.NodeShareOfCLBalance) - rewards.BeaconRewards = rewards.BeaconRewards + math.WeiToEth(megapoolUnskimmedRewards) - } - fmt.Println("=== ETH ===") fmt.Printf("Your share of unskimmed Beacon Chain (CL) rewards is currently %.6f ETH.\n", rewards.BeaconRewards) fmt.Printf("You have claimed %.6f ETH from the Smoothing Pool.\n", rewards.CumulativeEthRewards) diff --git a/rocketpool/api/megapool/status.go b/rocketpool/api/megapool/status.go index 120e27c94..8a2ff4f4c 100644 --- a/rocketpool/api/megapool/status.go +++ b/rocketpool/api/megapool/status.go @@ -185,7 +185,7 @@ func getValidatorMapAndBalances(c *cli.Command) (*api.MegapoolValidatorMapAndRew status, err := getStatus(c, false) if err != nil { - return nil, fmt.Errorf("Error getting the megapool status") + return nil, fmt.Errorf("Error getting the megapool status: %w", err) } // Response diff --git a/rocketpool/api/node/rewards.go b/rocketpool/api/node/rewards.go index 196568635..5726bf694 100644 --- a/rocketpool/api/node/rewards.go +++ b/rocketpool/api/node/rewards.go @@ -5,14 +5,11 @@ import ( "math/big" "time" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/rocket-pool/smartnode/bindings/dao/trustednode" - "github.com/rocket-pool/smartnode/bindings/minipool" "github.com/rocket-pool/smartnode/bindings/node" "github.com/rocket-pool/smartnode/bindings/rewards" - "github.com/rocket-pool/smartnode/bindings/rocketpool" "github.com/rocket-pool/smartnode/bindings/tokens" "github.com/rocket-pool/smartnode/bindings/types" rpstate "github.com/rocket-pool/smartnode/bindings/utils/state" @@ -21,144 +18,12 @@ import ( "github.com/urfave/cli/v3" "golang.org/x/sync/errgroup" - mpApi "github.com/rocket-pool/smartnode/rocketpool/api/minipool" "github.com/rocket-pool/smartnode/shared/services" - "github.com/rocket-pool/smartnode/shared/services/beacon" rprewards "github.com/rocket-pool/smartnode/shared/services/rewards" + "github.com/rocket-pool/smartnode/shared/services/state" "github.com/rocket-pool/smartnode/shared/types/api" ) -// Settings -const minipoolBalanceDetailsBatchSize = 20 - -// Beacon chain balance info for a minipool -type minipoolBalanceDetails struct { - nodeDeposit *big.Int - nodeBalance *big.Int -} - -// Get the balances of the minipools on the beacon chain -func getBeaconBalances(rp *rocketpool.RocketPool, bc beacon.Client, addresses []common.Address, beaconHead beacon.BeaconHead, opts *bind.CallOpts) ([]minipoolBalanceDetails, error) { - - // Get minipool validator statuses - validators, err := mpApi.GetMinipoolValidators(rp, bc, addresses, opts, &beacon.ValidatorStatusOptions{Epoch: &beaconHead.Epoch}) - if err != nil { - return []minipoolBalanceDetails{}, err - } - - // Load details in batches - details := make([]minipoolBalanceDetails, len(addresses)) - for bsi := 0; bsi < len(addresses); bsi += minipoolBalanceDetailsBatchSize { - - // Get batch start & end index - msi := bsi - mei := min(bsi+minipoolBalanceDetailsBatchSize, len(addresses)) - - // Load details - var wg errgroup.Group - for mi := msi; mi < mei; mi++ { - mi := mi - wg.Go(func() error { - address := addresses[mi] - validator := validators[address] - mpDetails, err := getMinipoolBalanceDetails(rp, address, opts, validator, beaconHead.Epoch) - if err == nil { - details[mi] = mpDetails - } - return err - }) - } - if err := wg.Wait(); err != nil { - return []minipoolBalanceDetails{}, err - } - - } - - // Return - return details, nil -} - -// Get minipool balance details -func getMinipoolBalanceDetails(rp *rocketpool.RocketPool, minipoolAddress common.Address, opts *bind.CallOpts, validator beacon.ValidatorStatus, blockEpoch uint64) (minipoolBalanceDetails, error) { - - // Create minipool - mp, err := minipool.NewMinipool(rp, minipoolAddress, opts) - if err != nil { - return minipoolBalanceDetails{}, err - } - blockBalance := math.GweiToWei(float64(validator.Balance)) - - // Data - var wg errgroup.Group - var status types.MinipoolStatus - var nodeDepositBalance *big.Int - var finalized bool - - // Load data - wg.Go(func() error { - var err error - status, err = mp.GetStatus(opts) - return err - }) - wg.Go(func() error { - var err error - nodeDepositBalance, err = mp.GetNodeDepositBalance(opts) - return err - }) - wg.Go(func() error { - var err error - finalized, err = mp.GetFinalised(opts) - return err - }) - - // Wait for data - if err := wg.Wait(); err != nil { - return minipoolBalanceDetails{}, err - } - - // Deal with pools that haven't received deposits yet so their balance is still 0 - if nodeDepositBalance == nil { - nodeDepositBalance = big.NewInt(0) - } - - // Ignore finalized minipools - if finalized { - return minipoolBalanceDetails{ - nodeDeposit: big.NewInt(0), - nodeBalance: big.NewInt(0), - }, nil - } - - // Use node deposit balance if initialized or prelaunch - if status == types.Initialized || status == types.Prelaunch { - return minipoolBalanceDetails{ - nodeDeposit: nodeDepositBalance, - nodeBalance: nodeDepositBalance, - }, nil - } - - // Use node deposit balance if validator not yet active on beacon chain at block - if !validator.Exists || validator.ActivationEpoch >= blockEpoch { - return minipoolBalanceDetails{ - nodeDeposit: nodeDepositBalance, - nodeBalance: nodeDepositBalance, - }, nil - } - - // Get node balance at block - nodeBalance, err := mp.CalculateNodeShare(blockBalance, opts) - if err != nil { - return minipoolBalanceDetails{}, err - } - - // Return - return minipoolBalanceDetails{ - nodeDeposit: nodeDepositBalance, - nodeBalance: nodeBalance, - }, nil - -} - func getRewards(c *cli.Command) (*api.NodeRewardsResponse, error) { // Get services @@ -179,7 +44,7 @@ func getRewards(c *cli.Command) (*api.NodeRewardsResponse, error) { if err != nil { return nil, err } - bc, err := services.GetBeaconClient(c) + stateProvider, err := services.GetNetworkStateProvider(c) if err != nil { return nil, err } @@ -205,8 +70,7 @@ func getRewards(c *cli.Command) (*api.NodeRewardsResponse, error) { var trustedNodeOperatorRewardsPercent float64 var totalDepositBalance float64 var totalNodeShare float64 - var addresses []common.Address - var beaconHead beacon.BeaconHead + var networkState *state.NetworkState // Sync var wg errgroup.Group @@ -364,23 +228,13 @@ func getRewards(c *cli.Command) (*api.NodeRewardsResponse, error) { return nil }) - // Get the list of minipool addresses for this node - wg.Go(func() error { - _addresses, err := minipool.GetNodeMinipoolAddresses(rp, nodeAccount.Address, nil) - if err != nil { - return fmt.Errorf("Error getting node minipool addresses: %w", err) - } - addresses = _addresses - return nil - }) - - // Get the beacon head + // Get the network state, filtered to this node's validators wg.Go(func() error { - _beaconHead, err := bc.GetBeaconHead() + _networkState, err := stateProvider.GetHeadStateForNode(nodeAccount.Address) if err != nil { - return fmt.Errorf("Error getting beacon chain head: %w", err) + return fmt.Errorf("Error getting network state: %w", err) } - beaconHead = _beaconHead + networkState = _networkState return nil }) @@ -390,16 +244,76 @@ func getRewards(c *cli.Command) (*api.NodeRewardsResponse, error) { } // Calculate the total deposits and corresponding beacon chain balance share - minipoolDetails, err := getBeaconBalances(rp, bc, addresses, beaconHead, nil) - if err != nil { - return nil, err - } - for _, minipool := range minipoolDetails { - totalDepositBalance += math.WeiToEth(minipool.nodeDeposit) - totalNodeShare += math.WeiToEth(minipool.nodeBalance) + intervalEndEpoch := networkState.BeaconSlotNumber / networkState.BeaconConfig.SlotsPerEpoch + for _, mpd := range networkState.MinipoolDetailsByNode[nodeAccount.Address] { + if mpd.Finalised { + // Finalized minipools contribute nothing + continue + } + + nodeDeposit := mpd.NodeDepositBalance + if nodeDeposit == nil { + nodeDeposit = big.NewInt(0) + } + + // Default to the deposit balance until the validator is confirmed active on Beacon + nodeShare := nodeDeposit + if mpd.Status != types.Initialized && mpd.Status != types.Prelaunch { + validator, exists := networkState.MinipoolValidatorDetails[mpd.Pubkey] + if exists && validator.Exists && validator.ActivationEpoch < intervalEndEpoch { + if validator.ExitEpoch <= intervalEndEpoch { + // Exited but not finalized -- funds already swept to the minipool's own balance + nodeShare = mpd.NodeShareOfBalanceIncludingBeacon + } else { + nodeShare = mpd.NodeShareOfBeaconBalance + } + } + } + + totalDepositBalance += math.WeiToEth(nodeDeposit) + totalNodeShare += math.WeiToEth(nodeShare) } response.BeaconRewards = totalNodeShare - totalDepositBalance + // Add the megapool's unskimmed CL rewards + nodeDetails, exists := networkState.NodeDetailsByAddress[nodeAccount.Address] + if exists && nodeDetails.MegapoolDeployed { + megapoolDetails, mdExists := networkState.MegapoolDetails[nodeDetails.MegapoolAddress] + if mdExists && !megapoolDetails.DelegateExpired { + var totalBeaconBalance, totalEffectiveBeaconBalance uint64 + for _, pubkey := range networkState.MegapoolToPubkeysMap[nodeDetails.MegapoolAddress] { + info, infoExists := networkState.GetMegapoolValidatorInfo(nodeDetails.MegapoolAddress, pubkey) + if !infoExists { + continue + } + vi := info.ValidatorInfo + if !vi.Staked || vi.Exited || vi.Exiting { + continue + } + beaconStatus, statusExists := networkState.MegapoolValidatorDetails[pubkey] + if statusExists && beaconStatus.Exists && intervalEndEpoch > beaconStatus.ActivationEpoch { + totalBeaconBalance += beaconStatus.Balance + totalEffectiveBeaconBalance += beaconStatus.EffectiveBalance + } + } + + megapoolReward := big.NewInt(0) + if totalBeaconBalance > totalEffectiveBeaconBalance { + weiPerGwei := big.NewInt(int64(math.WeiPerGwei)) + totalBeaconBalanceWei := new(big.Int).Mul(new(big.Int).SetUint64(totalBeaconBalance), weiPerGwei) + totalEffectiveBeaconBalanceWei := new(big.Int).Mul(new(big.Int).SetUint64(totalEffectiveBeaconBalance), weiPerGwei) + toBeSkimmed := new(big.Int).Sub(totalBeaconBalanceWei, totalEffectiveBeaconBalanceWei) + + rewardSplit, err := services.CalculateRewards(rp, toBeSkimmed, nodeAccount.Address) + if err != nil { + return nil, fmt.Errorf("Error calculating megapool rewards split for amount %s: %w", toBeSkimmed.String(), err) + } + megapoolReward = rewardSplit.RewardSplit.NodeRewards + } + response.BeaconRewards += math.WeiToEth(megapoolReward) + } + } + // Calculate the estimated rewards rewardsIntervalDays := response.RewardsInterval.Seconds() / (60 * 60 * 24) inflationPerDay := math.WeiToEth(inflationInterval) diff --git a/shared/services/state/network-state.go b/shared/services/state/network-state.go index 0a9abb31d..f547e233a 100644 --- a/shared/services/state/network-state.go +++ b/shared/services/state/network-state.go @@ -325,9 +325,23 @@ func (m *NetworkStateManager) createNetworkState(slotNumber uint64, nodeAddresse state.MinipoolDetailsByNode[details.NodeAddress] = nodeList } - state.MegapoolValidatorGlobalIndex, err = rpstate.GetAllMegapoolValidators(m.rp, contracts) - if err != nil { - return nil, fmt.Errorf("error getting all megapool validator details: %w", err) + if allNodes { + state.MegapoolValidatorGlobalIndex, err = rpstate.GetAllMegapoolValidators(m.rp, contracts) + if err != nil { + return nil, fmt.Errorf("error getting all megapool validator details: %w", err) + } + } else { + state.MegapoolValidatorGlobalIndex = []megapool.ValidatorInfoFromGlobalIndex{} + for _, nd := range state.NodeDetails { + if !nd.MegapoolDeployed { + continue + } + validators, err := rpstate.GetNodeMegapoolValidators(m.rp, contracts, nd.MegapoolAddress) + if err != nil { + return nil, fmt.Errorf("error getting megapool validator details for %s: %w", nd.MegapoolAddress.Hex(), err) + } + state.MegapoolValidatorGlobalIndex = append(state.MegapoolValidatorGlobalIndex, validators...) + } } currentStep++ m.logLine("%d/%d - Retrieved megapool validator global index (%s so far)", currentStep, steps, time.Since(start))