Skip to content

Commit

Permalink
Fix fee calculation to account for bonus commission, update RR to be …
Browse files Browse the repository at this point in the history
…ruleset aware
  • Loading branch information
jshufro committed Nov 12, 2024
1 parent 0511b6d commit 71950f9
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 38 deletions.
2 changes: 2 additions & 0 deletions rocketpool/watchtower/submit-rewards-tree-rolling.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
rprewards "github.com/rocket-pool/smartnode/shared/services/rewards"
"github.com/rocket-pool/smartnode/shared/services/state"
"github.com/rocket-pool/smartnode/shared/services/wallet"
cfgtypes "github.com/rocket-pool/smartnode/shared/types/config"
"github.com/rocket-pool/smartnode/shared/utils/api"
"github.com/rocket-pool/smartnode/shared/utils/eth1"
hexutil "github.com/rocket-pool/smartnode/shared/utils/hex"
Expand Down Expand Up @@ -143,6 +144,7 @@ func newSubmitRewardsTree_Rolling(c *cli.Context, logger log.ColorLogger, errorL
RecordCheckpointInterval: cfg.Smartnode.RecordCheckpointInterval.Value.(uint64),
PreviousRewardsPoolAddresses: cfg.Smartnode.GetPreviousRewardsPoolAddresses(),
StateProvider: stateMgr,
Network: cfg.Smartnode.Network.Value.(cfgtypes.Network),
}
recordMgr, err := rprewards.NewRollingRecordManager(startSlot, currentIndex, settings)
if err != nil {
Expand Down
29 changes: 29 additions & 0 deletions shared/services/rewards/fees/fees.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package fees

import (
"math/big"
)

var oneEth = big.NewInt(1000000000000000000)
var tenEth = big.NewInt(0).Mul(oneEth, big.NewInt(10))
var pointOhFourEth = big.NewInt(40000000000000000)
var pointOneEth = big.NewInt(0).Div(oneEth, big.NewInt(10))
var sixteenEth = big.NewInt(0).Mul(oneEth, big.NewInt(16))

func GetMinipoolFeeWithBonus(bond, fee, percentOfBorrowedEth *big.Int) *big.Int {
if bond.Cmp(sixteenEth) >= 0 {
return fee
}
// fee = max(fee, 0.10 Eth + (0.04 Eth * min(10 Eth, percentOfBorrowedETH) / 10 Eth))
_min := big.NewInt(0).Set(tenEth)
if _min.Cmp(percentOfBorrowedEth) > 0 {
_min.Set(percentOfBorrowedEth)
}
dividend := _min.Mul(_min, pointOhFourEth)
divResult := dividend.Div(dividend, tenEth)
feeWithBonus := divResult.Add(divResult, pointOneEth)
if fee.Cmp(feeWithBonus) >= 0 {
return fee
}
return feeWithBonus
}
12 changes: 3 additions & 9 deletions shared/services/rewards/generator-impl-v9-v10-rolling.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/rocket-pool/rocketpool-go/utils/eth"
"github.com/rocket-pool/smartnode/shared/services/beacon"
"github.com/rocket-pool/smartnode/shared/services/config"
"github.com/rocket-pool/smartnode/shared/services/rewards/fees"
"github.com/rocket-pool/smartnode/shared/services/rewards/ssz_types"
sszbig "github.com/rocket-pool/smartnode/shared/services/rewards/ssz_types/big"
"github.com/rocket-pool/smartnode/shared/services/state"
Expand Down Expand Up @@ -544,20 +545,13 @@ func (r *treeGeneratorImpl_v9_v10_rolling) calculateNodeBonuses() (*big.Int, err
_, percentOfBorrowedEth := r.networkState.GetStakedRplValueInEthAndPercentOfBorrowedEth(eligibleBorrowedEth, nodeDetails.RplStake)
for _, mpd := range nsd.Minipools {
mpi := r.networkState.MinipoolDetailsByAddress[mpd.Address]
fee := mpi.NodeFee
if !mpi.IsEligibleForBonuses(r.elEndTime) {
mpd.MinipoolBonus = nil
mpd.ConsensusIncome = nil
continue
}
// fee = max(fee, 0.10 Eth + (0.04 Eth * min(10 Eth, percentOfBorrowedETH) / 10 Eth))
_min := big.NewInt(0).Set(tenEth)
if _min.Cmp(percentOfBorrowedEth) > 0 {
_min.Set(percentOfBorrowedEth)
}
dividend := _min.Mul(_min, pointOhFourEth)
divResult := dividend.Div(dividend, tenEth)
feeWithBonus := divResult.Add(divResult, pointOneEth)
bond, fee := mpi.GetMinipoolBondAndNodeFee(r.elEndTime)
feeWithBonus := fees.GetMinipoolFeeWithBonus(bond, fee, percentOfBorrowedEth)
if fee.Cmp(feeWithBonus) >= 0 {
// This minipool won't get any bonuses, so skip it
mpd.MinipoolBonus = nil
Expand Down
39 changes: 25 additions & 14 deletions shared/services/rewards/generator-impl-v9-v10.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/rocket-pool/rocketpool-go/utils/eth"
"github.com/rocket-pool/smartnode/shared/services/beacon"
"github.com/rocket-pool/smartnode/shared/services/config"
"github.com/rocket-pool/smartnode/shared/services/rewards/fees"
"github.com/rocket-pool/smartnode/shared/services/rewards/ssz_types"
sszbig "github.com/rocket-pool/smartnode/shared/services/rewards/ssz_types/big"
"github.com/rocket-pool/smartnode/shared/services/state"
Expand Down Expand Up @@ -512,12 +513,18 @@ func (r *treeGeneratorImpl_v9_v10) calculateEthRewards(checkBeaconPerformance bo
for _, nodeInfo := range r.nodeDetails {
// Check if the node is currently opted in for simplicity
if nodeInfo.IsEligible && nodeInfo.IsOptedIn && r.elEndTime.After(nodeInfo.OptInTime) {
nnd := r.networkState.NodeDetailsByAddress[nodeInfo.Address]
eligibleBorrowedEth := r.networkState.GetEligibleBorrowedEth(nnd)
_, percentOfBorrowedEth := r.networkState.GetStakedRplValueInEthAndPercentOfBorrowedEth(eligibleBorrowedEth, nnd.RplStake)
for _, minipool := range nodeInfo.Minipools {
minipool.CompletedAttestations = map[uint64]bool{0: true}

// Make up an attestation
details := r.networkState.MinipoolDetailsByAddress[minipool.Address]
bond, fee := details.GetMinipoolBondAndNodeFee(r.elEndTime)
if r.rewardsFile.RulesetVersion >= 10 {
fee = fees.GetMinipoolFeeWithBonus(bond, fee, percentOfBorrowedEth)
}
minipoolScore := big.NewInt(0).Sub(oneEth, fee) // 1 - fee
minipoolScore.Mul(minipoolScore, bond) // Multiply by bond
minipoolScore.Div(minipoolScore, validatorReq) // Divide by 32 to get the bond as a fraction of a total validator
Expand Down Expand Up @@ -609,11 +616,9 @@ func (r *treeGeneratorImpl_v9_v10) calculateEthRewards(checkBeaconPerformance bo
}

var oneEth = big.NewInt(1000000000000000000)
var pointOneEth = big.NewInt(0).Div(oneEth, big.NewInt(10))
var tenEth = big.NewInt(0).Mul(oneEth, big.NewInt(10))
var eightEth = big.NewInt(0).Mul(oneEth, big.NewInt(8))
var fourteenPercentEth = big.NewInt(14e16)
var thirtyTwoEth = big.NewInt(0).Mul(oneEth, big.NewInt(32))
var pointOhFourEth = big.NewInt(40000000000000000)

func (r *treeGeneratorImpl_v9_v10) calculateNodeBonuses() (*big.Int, error) {
totalConsensusBonus := big.NewInt(0)
Expand All @@ -634,7 +639,6 @@ func (r *treeGeneratorImpl_v9_v10) calculateNodeBonuses() (*big.Int, error) {
_, percentOfBorrowedEth := r.networkState.GetStakedRplValueInEthAndPercentOfBorrowedEth(eligibleBorrowedEth, nodeDetails.RplStake)
for _, mpd := range nsd.Minipools {
mpi := r.networkState.MinipoolDetailsByAddress[mpd.Address]
fee := mpi.NodeFee
if !mpi.IsEligibleForBonuses(eligibleEnd) {
continue
}
Expand All @@ -643,14 +647,8 @@ func (r *treeGeneratorImpl_v9_v10) calculateNodeBonuses() (*big.Int, error) {
// Validators with no balance at the end of the interval don't get any bonus commission
continue
}
// fee = max(fee, 0.10 Eth + (0.04 Eth * min(10 Eth, percentOfBorrowedETH) / 10 Eth))
_min := big.NewInt(0).Set(tenEth)
if _min.Cmp(percentOfBorrowedEth) > 0 {
_min.Set(percentOfBorrowedEth)
}
dividend := _min.Mul(_min, pointOhFourEth)
divResult := dividend.Div(dividend, tenEth)
feeWithBonus := divResult.Add(divResult, pointOneEth)
bond, fee := mpi.GetMinipoolBondAndNodeFee(eligibleEnd)
feeWithBonus := fees.GetMinipoolFeeWithBonus(bond, fee, percentOfBorrowedEth)
if fee.Cmp(feeWithBonus) >= 0 {
// This minipool won't get any bonuses, so skip it
continue
Expand Down Expand Up @@ -947,7 +945,7 @@ func (r *treeGeneratorImpl_v9_v10) getValidatorBalancesAtStartAndEnd() error {
}
}

r.log.Printlnf("%s Finished updating validator balances for %d slots in %d seconds", r.logPrefix, total, time.Since(startTime).Seconds())
r.log.Printlnf("%s Finished updating validator balances for %d slots in %s", r.logPrefix, total, time.Since(startTime).String())

return nil
}
Expand Down Expand Up @@ -1130,12 +1128,25 @@ func (r *treeGeneratorImpl_v9_v10) checkAttestations(attestations []beacon.Attes
continue
}

nnd := r.networkState.NodeDetailsByAddress[validator.NodeAddress]
eligibleBorrowedEth := r.networkState.GetEligibleBorrowedEth(nnd)
_, percentOfBorrowedEth := r.networkState.GetStakedRplValueInEthAndPercentOfBorrowedEth(eligibleBorrowedEth, nnd.RplStake)

// Mark this duty as completed
validator.CompletedAttestations[attestation.SlotIndex] = true

// Get the pseudoscore for this attestation
details := r.networkState.MinipoolDetailsByAddress[validator.Address]
minipoolScore := details.GetMinipoolAttestationScore(blockTime)
bond, fee := details.GetMinipoolBondAndNodeFee(blockTime)

if r.rewardsFile.RulesetVersion >= 10 {
fee = fees.GetMinipoolFeeWithBonus(bond, fee, percentOfBorrowedEth)
}

minipoolScore := big.NewInt(0).Sub(oneEth, fee) // 1 - fee
minipoolScore.Mul(minipoolScore, bond) // Multiply by bond
minipoolScore.Div(minipoolScore, thirtyTwoEth) // Divide by 32 to get the bond as a fraction of a total validator
minipoolScore.Add(minipoolScore, fee) // Total = fee + (bond/32)(1 - fee)

// Add it to the minipool's score and the total score
validator.AttestationScore.Add(&validator.AttestationScore.Int, minipoolScore)
Expand Down
33 changes: 33 additions & 0 deletions shared/services/rewards/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,39 @@ const (
HoleskyV10Interval uint64 = 300
)

func GetMainnetRulesetVersion(interval uint64) uint64 {
if interval >= MainnetV10Interval {
return 10
}
if interval >= MainnetV9Interval {
return 9
}
return 8
}

func GetHoleskyRulesetVersion(interval uint64) uint64 {
if interval >= HoleskyV10Interval {
return 10
}
if interval >= HoleskyV9Interval {
return 9
}
return 8
}

func GetRulesetVersion(network cfgtypes.Network, interval uint64) uint64 {
switch network {
case cfgtypes.Network_Mainnet:
return GetMainnetRulesetVersion(interval)
case cfgtypes.Network_Holesky:
return GetHoleskyRulesetVersion(interval)
case cfgtypes.Network_Devnet:
return 10
default:
return 10
}
}

type TreeGenerator struct {
rewardsIntervalInfos map[uint64]rewardsIntervalInfo
logger *log.ColorLogger
Expand Down
33 changes: 26 additions & 7 deletions shared/services/rewards/mock_v10_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ func getRollingRecord(history *test.MockHistory, state *state.NetworkState, defa
LastDutiesSlot: history.BeaconConfig.LastSlotOfEpoch(history.EndEpoch),
ValidatorIndexMap: validatorIndexMap,
RewardsInterval: history.NetworkDetails.RewardIndex,
SmartnodeVersion: shared.RocketPoolVersion,
RulesetVersion: 10,

SmartnodeVersion: shared.RocketPoolVersion,
}
}

Expand Down Expand Up @@ -237,7 +239,7 @@ func TestMockIntervalDefaultsTreegenv10(tt *testing.T) {
expectedEthAmount := big.NewInt(0)
if node.SmoothingPoolRegistrationState {
if node.Class == "single_eight_eth_sp" {
expectedEthAmount.SetString("1330515055467511885", 10)
expectedEthAmount.SetString("1450562599049128367", 10)
// There should be a bonus for these nodes' minipools
if len(node.Minipools) != 1 {
t.Fatalf("Expected 1 minipool for node %s, got %d", node.Notes, len(node.Minipools))
Expand All @@ -250,13 +252,30 @@ func TestMockIntervalDefaultsTreegenv10(tt *testing.T) {
if minipoolPerf.GetBonusEthEarned().Cmp(expectedBonusEthEarned) != 0 {
t.Fatalf("Minipool %s bonus does not match expected value: %s != %s", node.Minipools[0].Address.Hex(), minipoolPerf.GetBonusEthEarned().String(), expectedBonusEthEarned.String())
}
expectedAttestationScore := big.NewInt(0).Sub(oneEth, big.NewInt(14e16))
expectedAttestationScore.Mul(expectedAttestationScore, eightEth)
expectedAttestationScore.Div(expectedAttestationScore, thirtyTwoEth)
expectedAttestationScore.Add(expectedAttestationScore, big.NewInt(14e16))
expectedAttestationScore.Mul(expectedAttestationScore, big.NewInt(101)) // there are 101 epochs in the interval
if minipoolPerf.GetAttestationScore().Cmp(expectedAttestationScore) != 0 {
t.Fatalf("Minipool %s attestation score does not match expected value: %s != %s", node.Minipools[0].Address.Hex(), minipoolPerf.GetAttestationScore().String(), expectedAttestationScore.String())
}
} else {
// 16-eth minipools earn more eth! A bit less than double.
expectedEthAmount.SetString("2200871632329635499", 10)
if len(node.Minipools) != 1 {
t.Fatalf("Expected 1 minipool for node %s, got %d", node.Notes, len(node.Minipools))
}
minipoolPerf, _ := minipoolPerformanceFile.GetSmoothingPoolPerformance(node.Minipools[0].Address)
// The 16 eth minipools earn 10% on 24/32.
expectedAttestationScore := big.NewInt(0).Sub(oneEth, big.NewInt(1e17))
expectedAttestationScore.Mul(expectedAttestationScore, sixteenEth)
expectedAttestationScore.Div(expectedAttestationScore, thirtyTwoEth)
expectedAttestationScore.Add(expectedAttestationScore, big.NewInt(1e17))
expectedAttestationScore.Mul(expectedAttestationScore, big.NewInt(101)) // there are 101 epochs in the interval
if minipoolPerf.GetAttestationScore().Cmp(expectedAttestationScore) != 0 {
t.Fatalf("Minipool %s attestation score does not match expected value: %s != %s", node.Minipools[0].Address.Hex(), minipoolPerf.GetAttestationScore().String(), expectedAttestationScore.String())
}
// 16 eth minipools earn no bonus.
if minipoolPerf.GetBonusEthEarned().Sign() != 0 {
t.Fatalf("Minipool %s bonus does not match expected value: %s != 0", node.Minipools[0].Address.Hex(), minipoolPerf.GetBonusEthEarned().String())
Expand Down Expand Up @@ -307,7 +326,7 @@ func TestMockIntervalDefaultsTreegenv10(tt *testing.T) {
expectedEthAmount := big.NewInt(0)
if node.Class == "single_eight_eth_opted_in_quarter" {
// About 3/4 what the full nodes got
expectedEthAmount.SetString("1001105388272583201", 10)
expectedEthAmount.SetString("1091438193343898573", 10)
// Earns 3/4 the bonus of a node that was in for the whole interval
expectedBonusEthEarned, _ := big.NewInt(0).SetString("22500000000000000", 10)
if perf.GetBonusEthEarned().Cmp(expectedBonusEthEarned) != 0 {
Expand Down Expand Up @@ -357,7 +376,7 @@ func TestMockIntervalDefaultsTreegenv10(tt *testing.T) {
expectedEthAmount := big.NewInt(0)
if node.Class == "single_eight_eth_opted_out_three_quarters" {
// About 3/4 what the full nodes got
expectedEthAmount.SetString("988229001584786053", 10)
expectedEthAmount.SetString("1077373217115689381", 10)
// Earns 3/4 the bonus of a node that was in for the whole interval
expectedBonusEthEarned, _ := big.NewInt(0).SetString("22500000000000000", 10)
if perf.GetBonusEthEarned().Cmp(expectedBonusEthEarned) != 0 {
Expand Down Expand Up @@ -401,7 +420,7 @@ func TestMockIntervalDefaultsTreegenv10(tt *testing.T) {

// Make sure it got reduced ETH
ethAmount := rewardsFile.GetNodeSmoothingPoolEth(node.Address)
expectedEthAmount, _ := big.NewInt(0).SetString("1860285261489698890", 10)
expectedEthAmount, _ := big.NewInt(0).SetString("1920903328050713153", 10)
if ethAmount.Cmp(expectedEthAmount) != 0 {
t.Fatalf("ETH amount does not match expected value for node %s: %s != %s", node.Notes, ethAmount.String(), expectedEthAmount.String())
}
Expand Down Expand Up @@ -444,12 +463,12 @@ func TestMockIntervalDefaultsTreegenv10(tt *testing.T) {
v10MerkleRoot := v10Artifacts.RewardsFile.GetMerkleRoot()

// Expected merkle root:
// 0x3fa097234425378acf21030870e4abb0a8c56a39e07b4a59a28d648d51781f0e
// 0x176bba15231cb82edb5c34c8882af09dfb77a2ee31a96b623bffd8e48cedf18b
//
// If this does not match, it implies either you updated the set of default mock nodes,
// or you introduced a regression in treegen.
// DO NOT update this value unless you know what you are doing.
expectedMerkleRoot := "0x3fa097234425378acf21030870e4abb0a8c56a39e07b4a59a28d648d51781f0e"
expectedMerkleRoot := "0x176bba15231cb82edb5c34c8882af09dfb77a2ee31a96b623bffd8e48cedf18b"
if !strings.EqualFold(v10MerkleRoot, expectedMerkleRoot) {
t.Fatalf("Merkle root does not match expected value %s != %s", v10MerkleRoot, expectedMerkleRoot)
} else {
Expand Down
3 changes: 3 additions & 0 deletions shared/services/rewards/rewards-file-v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ func (p *SmoothingPoolMinipoolPerformance_v1) GetEffectiveCommission() *big.Int
func (p *SmoothingPoolMinipoolPerformance_v1) GetConsensusIncome() *big.Int {
return big.NewInt(0)
}
func (p *SmoothingPoolMinipoolPerformance_v1) GetAttestationScore() *big.Int {
return big.NewInt(0)
}

// Node operator rewards
type NodeRewardsInfo_v1 struct {
Expand Down
3 changes: 3 additions & 0 deletions shared/services/rewards/rewards-file-v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ func (p *SmoothingPoolMinipoolPerformance_v2) GetConsensusIncome() *big.Int {
}
return &p.ConsensusIncome.Int
}
func (p *SmoothingPoolMinipoolPerformance_v2) GetAttestationScore() *big.Int {
return &p.AttestationScore.Int
}

// Node operator rewards
type NodeRewardsInfo_v2 struct {
Expand Down
Loading

0 comments on commit 71950f9

Please sign in to comment.