From 97f2aad07fab762532680d1bfa0d5f85f70610d3 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:13:35 -0300 Subject: [PATCH 1/2] Rework on rpl for grafana --- rocketpool/node/collectors/node-collector.go | 63 +++++-------------- .../Rocket Pool Dashboard v1.4.1.json | 4 +- shared/services/state/network-state.go | 36 ++++++++--- 3 files changed, 44 insertions(+), 59 deletions(-) diff --git a/rocketpool/node/collectors/node-collector.go b/rocketpool/node/collectors/node-collector.go index d0b502ca9..01a0d3b89 100644 --- a/rocketpool/node/collectors/node-collector.go +++ b/rocketpool/node/collectors/node-collector.go @@ -16,7 +16,6 @@ import ( "github.com/rocket-pool/smartnode/bindings/rocketpool" "github.com/rocket-pool/smartnode/shared/math" "github.com/rocket-pool/smartnode/shared/services" - "github.com/rocket-pool/smartnode/shared/services/beacon" "github.com/rocket-pool/smartnode/shared/services/config" rprewards "github.com/rocket-pool/smartnode/shared/services/rewards" ) @@ -29,10 +28,10 @@ type NodeCollector struct { // The total amount of RPL staked on megapool on the node megapoolStakedRpl *prometheus.Desc - // The effective amount of RPL staked on the node (honoring the 150% collateral cap) + // The effective amount of RPL staked on the node effectiveStakedRpl *prometheus.Desc - // The amount of staked RPL that will be eligible for rewards (including Beacon Chain data and accounding for pending bond reductions) + // The amount of staked RPL that will be eligible for rewards (0 if the node has no validators that count towards its borrowed ETH) rewardableStakedRpl *prometheus.Desc // The cumulative RPL rewards earned by the node @@ -195,11 +194,11 @@ func NewNodeCollector(rp *rocketpool.RocketPool, bc *services.BeaconClientManage nil, nil, ), effectiveStakedRpl: prometheus.NewDesc(prometheus.BuildFQName(namespace, subsystem, "effective_staked_rpl"), - "The effective amount of RPL staked on the node (honoring the 150% collateral cap)", + "The effective amount of RPL staked on the node", nil, nil, ), rewardableStakedRpl: prometheus.NewDesc(prometheus.BuildFQName(namespace, subsystem, "rewardable_staked_rpl"), - "The amount of staked RPL that will be eligible for rewards (including Beacon Chain data and accounding for pending bond reductions)", + "The amount of staked RPL that will be eligible for rewards (0 if the node has no validators that count towards its borrowed ETH)", nil, nil, ), cumulativeRplRewards: prometheus.NewDesc(prometheus.BuildFQName(namespace, subsystem, "cumulative_rpl_rewards"), @@ -418,7 +417,7 @@ func (collector *NodeCollector) Collect(channel chan<- prometheus.Metric) { // Sync var wg errgroup.Group - nodeLegacyStakedRpl := math.WeiToEth(nd.LegacyStakedRPL) // TODO: update all metrics to account for saturn + nodeLegacyStakedRpl := math.WeiToEth(nd.LegacyStakedRPL) nodeMegapoolStakedRpl := math.WeiToEth(nd.MegapoolStakedRPL) effectiveStakedRpl := math.WeiToEth(nd.EffectiveRPLStake) megapoolQueueBond := math.WeiToEth(megapoolDetails.NodeQueuedBond) @@ -431,11 +430,10 @@ func (collector *NodeCollector) Collect(channel chan<- prometheus.Metric) { oldRplBalance := math.WeiToEth(nd.BalanceOldRPL) newRplBalance := math.WeiToEth(nd.BalanceRPL) rethBalance := math.WeiToEth(nd.BalanceRETH) - eligibleBorrowedEth := state.GetMinipoolEligibleBorrowedEth(nd) + eligibleBorrowedEth := state.GetEligibleBorrowedEth(nd) var activeMinipoolCount float64 rplPriceRaw := state.NetworkDetails.RplPrice rplPrice := math.WeiToEth(rplPriceRaw) - var beaconHead beacon.BeaconHead unclaimedEthRewards := float64(0) unclaimedRplRewards := float64(0) lowETHBalanceThreshold := collector.cfg.Alertmanager.LowETHBalanceThreshold.Value.(float64) @@ -591,16 +589,6 @@ func (collector *NodeCollector) Collect(channel chan<- prometheus.Metric) { return nil }) - // Get the beacon head - wg.Go(func() error { - _beaconHead, err := collector.bc.GetBeaconHead() - if err != nil { - return fmt.Errorf("Error getting beacon chain head: %w", err) - } - beaconHead = _beaconHead - return nil - }) - // Get the megapool details wg.Go(func() error { // Get queue sizes - these are protocol-wide metrics, independent of whether @@ -687,13 +675,17 @@ func (collector *NodeCollector) Collect(channel chan<- prometheus.Metric) { return } - // Calculate the node weight - nodeWeight := big.NewInt(0) + // Calculate the node weight, using the same inputs as the rewards tree + nodeWeight := state.GetUnscaledNodeWeight(nd) + + // All of the node's staked RPL (legacy + megapool) earns rewards + totalStakedRpl := nodeLegacyStakedRpl + nodeMegapoolStakedRpl + + rewardableStakeFloat := float64(0) if eligibleBorrowedEth.Sign() > 0 { - nodeWeight = state.GetNodeWeight(eligibleBorrowedEth, nd.LegacyStakedRPL) + rewardableStakeFloat = totalStakedRpl } - // Calculate the rewardable RPL reductionWindowStart := state.NetworkDetails.BondReductionWindowStart reductionWindowLength := state.NetworkDetails.BondReductionWindowLength reductionWindowEnd := reductionWindowStart + reductionWindowLength @@ -705,8 +697,6 @@ func (collector *NodeCollector) Collect(channel chan<- prometheus.Metric) { zero := big.NewInt(0) pendingBorrowedEth := big.NewInt(0) pendingBondedEth := big.NewInt(0) - rewardableBorrowedEth := big.NewInt(0) - rewardableBondedEth := big.NewInt(0) for _, mpd := range minipools { if mpd.Finalised { // Ignore finalized minipools in the ratio math @@ -728,27 +718,8 @@ func (collector *NodeCollector) Collect(channel chan<- prometheus.Metric) { borrowed := big.NewInt(0).Sub(math.EthToWei(32), bonded) pendingBorrowedEth.Add(pendingBorrowedEth, borrowed) pendingBondedEth.Add(pendingBondedEth, bonded) - - validator, exists := state.MinipoolValidatorDetails[mpd.Pubkey] - if !exists { - // Validator doesn't exist on Beacon yet - continue - } - if validator.ActivationEpoch > beaconHead.Epoch { - // Validator hasn't activated yet - continue - } - if validator.ExitEpoch <= beaconHead.Epoch { - // Validator exited - continue - } - - rewardableBorrowedEth.Add(rewardableBorrowedEth, borrowed) - rewardableBondedEth.Add(rewardableBondedEth, bonded) } - rewardableStakeFloat := math.WeiToEth(nd.LegacyStakedRPL) - // Calculate the estimated rewards rewardsIntervalDays := rewardsInterval.Seconds() / (60 * 60 * 24) inflationPerDay := math.WeiToEth(inflationInterval) @@ -783,8 +754,8 @@ func (collector *NodeCollector) Collect(channel chan<- prometheus.Metric) { // Calculate the RPL APR rplApr := float64(0) - if nodeLegacyStakedRpl > 0 { - rplApr = estimatedRewards / nodeLegacyStakedRpl / rewardsInterval.Hours() * (24 * 365) * 100 + if totalStakedRpl > 0 { + rplApr = estimatedRewards / totalStakedRpl / rewardsInterval.Hours() * (24 * 365) * 100 } // Calculate the total deposits and corresponding beacon chain balance share @@ -810,8 +781,6 @@ func (collector *NodeCollector) Collect(channel chan<- prometheus.Metric) { } // RPL collateral - // Use the total staked RPL (legacy + megapool) - totalStakedRpl := nodeLegacyStakedRpl + nodeMegapoolStakedRpl totalBondedEthFloat := math.WeiToEth(pendingBondedEth) + math.WeiToEth(nd.MegapoolEthBonded) var bondedCollateralRatio float64 if totalBondedEthFloat == 0 { diff --git a/shared/services/rocketpool/assets/install/dashboards/Rocket Pool Dashboard v1.4.1.json b/shared/services/rocketpool/assets/install/dashboards/Rocket Pool Dashboard v1.4.1.json index 3121d6e09..d50fbac66 100644 --- a/shared/services/rocketpool/assets/install/dashboards/Rocket Pool Dashboard v1.4.1.json +++ b/shared/services/rocketpool/assets/install/dashboards/Rocket Pool Dashboard v1.4.1.json @@ -4833,7 +4833,7 @@ "type": "prometheus", "uid": "PBFA97CFB590B2093" }, - "description": "This shows various levels of RPL stake you have:\n\n- **Total**: the total amount of RPL you have staked.\n- **Effective**: the amount of your staked RPL that is being put to use, accounting for the 10% borrowed minimum and 150% bonded maximum limits.\n- **Rewardable**: the effective amount of RPL that is eligible for earning RPL rewards at the end of each rewards period. This is based on the status of your validators on the Beacon Chain; validators that haven't been activated yet or validators that have been exited are not eligible for rewards.\n\n**Note**: this takes any *pending bond reductions* you may have into account.", + "description": "This shows various levels of RPL stake you have:\n\n- **Total**: the total amount of RPL you have staked.\n- **Effective**: the amount of your staked RPL that is being put to use.\n- **Rewardable**: the effective amount of RPL that is eligible for earning RPL rewards at the end of each rewards period. This is based on the status of your validators on the Beacon Chain; validators that haven't been activated yet or validators that have been exited are not eligible for rewards.\n\n**Note**: this takes any *pending bond reductions* you may have into account.", "fieldConfig": { "defaults": { "color": { @@ -4919,7 +4919,7 @@ "type": "stat" }, { - "description": "Your total staked RPL collateral levels. This shows your collateral relative to the amount of ETH you have *borrowed* from the staking pool to complete your validators, and the amount of ETH you have *bonded* with your own funds.\n\nIf you fall below 10% of the *borrowed* ETH, you won't be able to claim your rewards at the next checkpoint until you get back to 10%.\n\nIf you go over 150% of the *bonded* ETH, you'll only be rewarded for the first 150% of your stake.", + "description": "Your total staked RPL collateral levels.", "fieldConfig": { "defaults": { "color": { diff --git a/shared/services/state/network-state.go b/shared/services/state/network-state.go index 067fb99f9..b78a7fd64 100644 --- a/shared/services/state/network-state.go +++ b/shared/services/state/network-state.go @@ -447,6 +447,30 @@ func (s *NetworkState) GetNodeWeight(eligibleBorrowedEth *big.Int, nodeStake *bi ) } +// Get the node's total borrowed ETH that counts towards RPL rewards (minipool + megapool) +func (s *NetworkState) GetEligibleBorrowedEth(node *rpstate.NativeNodeDetails) *big.Int { + eligibleBorrowedEth := s.GetMinipoolEligibleBorrowedEth(node) + eligibleBorrowedEth.Add(eligibleBorrowedEth, s.GetMegapoolEligibleBorrowedEth(node)) + return eligibleBorrowedEth +} + +// Get the node's total staked RPL that counts towards RPL rewards (legacy + megapool) +func (s *NetworkState) GetRewardsEligibleRplStake(node *rpstate.NativeNodeDetails) *big.Int { + rplStake := big.NewInt(0).Set(node.LegacyStakedRPL) + // Megapool staked RPL counts towards RPL rewards + rplStake.Add(rplStake, node.MegapoolStakedRPL) + return rplStake +} + +// Get the node's weight before scaling on participation +func (s *NetworkState) GetUnscaledNodeWeight(node *rpstate.NativeNodeDetails) *big.Int { + eligibleBorrowedEth := s.GetEligibleBorrowedEth(node) + if eligibleBorrowedEth.Sign() <= 0 { + return big.NewInt(0) + } + return s.GetNodeWeight(eligibleBorrowedEth, s.GetRewardsEligibleRplStake(node)) +} + // Starting in v8, RPL stake is phased out and replaced with weight. // scaleByParticipation and allowRplForUnstartedValidators are hard-coded true here, since // only v8 cares about weight. @@ -466,21 +490,13 @@ func (s *NetworkState) CalculateNodeWeights() (map[common.Address]*big.Int, *big wg.SetLimit(threadLimit) for i, node := range s.NodeDetails { wg.Go(func() error { - eligibleBorrowedEth := s.GetMinipoolEligibleBorrowedEth(&node) - rplStake := big.NewInt(0).Set(node.LegacyStakedRPL) - // Megapool staked RPL counts towards RPL rewards - rplStake.Add(rplStake, node.MegapoolStakedRPL) - eligibleBorrowedEth.Add(eligibleBorrowedEth, s.GetMegapoolEligibleBorrowedEth(&node)) - // Calculate the weight - nodeWeight := big.NewInt(0) - if eligibleBorrowedEth.Sign() <= 0 { + nodeWeight := s.GetUnscaledNodeWeight(&node) + if nodeWeight.Sign() <= 0 { weightSlice[i] = nodeWeight return nil } - nodeWeight.Set(s.GetNodeWeight(eligibleBorrowedEth, rplStake)) - // Scale the node weight by the participation in the current interval // Get the timestamp of the node's registration regTimeBig := node.RegistrationTime From 7ab93c9e3b4f2e77da4542060571786624417d18 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:22:44 -0300 Subject: [PATCH 2/2] Fix getting megapool indices --- .../node/collectors/beacon-collector.go | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/rocketpool/node/collectors/beacon-collector.go b/rocketpool/node/collectors/beacon-collector.go index cdda8f536..52bec9f0b 100644 --- a/rocketpool/node/collectors/beacon-collector.go +++ b/rocketpool/node/collectors/beacon-collector.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/rocket-pool/smartnode/shared/services/beacon" + "github.com/rocket-pool/smartnode/shared/services/state" "github.com/prometheus/client_golang/prometheus" "golang.org/x/sync/errgroup" @@ -97,16 +98,10 @@ func (collector *BeaconCollector) Collect(channel chan<- prometheus.Metric) { upcomingSyncCommittee := float64(0) upcomingProposals := float64(0) - var validatorIndices []string var head beacon.BeaconHead - // Get sync committee duties - for _, mpd := range state.MinipoolDetailsByNode[collector.nodeAddress] { - validator := state.MinipoolValidatorDetails[mpd.Pubkey] - if validator.Exists { - validatorIndices = append(validatorIndices, validator.Index) - } - } + // Get the validators to check duties for + validatorIndices := getNodeValidatorIndices(state, collector.nodeAddress) head, err := collector.bc.GetBeaconHead() if err != nil { @@ -205,7 +200,37 @@ func (collector *BeaconCollector) Collect(channel chan<- prometheus.Metric) { collector.recentProposals, prometheus.GaugeValue, recentProposalCount) } +// Get the Beacon indices of all of the node's validators, both minipool and megapool +func getNodeValidatorIndices(networkState *state.NetworkState, nodeAddress common.Address) []string { + var validatorIndices []string + + for _, mpd := range networkState.MinipoolDetailsByNode[nodeAddress] { + validator := networkState.MinipoolValidatorDetails[mpd.Pubkey] + if validator.Exists { + validatorIndices = append(validatorIndices, validator.Index) + } + } + + // Megapool validators have duties too + nodeDetails, exists := networkState.NodeDetailsByAddress[nodeAddress] + if exists && nodeDetails.MegapoolDeployed { + for _, pubkey := range networkState.MegapoolToPubkeysMap[nodeDetails.MegapoolAddress] { + validator := networkState.MegapoolValidatorDetails[pubkey] + if validator.Exists { + validatorIndices = append(validatorIndices, validator.Index) + } + } + } + + return validatorIndices +} + func (collector *BeaconCollector) getProposedBlockCount(validatorIndices []string, head beacon.BeaconHead, slotsPerEpoch uint64) (float64, error) { + // Nothing to look for + if len(validatorIndices) == 0 { + return 0, nil + } + // prepare for quick lookups in event of many validators: indexLookup := make(map[string]string, len(validatorIndices)) for _, index := range validatorIndices {