diff --git a/rocketpool/watchtower/submit-rewards-tree-rolling.go b/rocketpool/watchtower/submit-rewards-tree-rolling.go index 91d6f0ea3..400219e62 100644 --- a/rocketpool/watchtower/submit-rewards-tree-rolling.go +++ b/rocketpool/watchtower/submit-rewards-tree-rolling.go @@ -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" @@ -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 { diff --git a/shared/services/rewards/fees/fees.go b/shared/services/rewards/fees/fees.go new file mode 100644 index 000000000..9cda3e9d4 --- /dev/null +++ b/shared/services/rewards/fees/fees.go @@ -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 +} diff --git a/shared/services/rewards/generator-impl-v9-v10-rolling.go b/shared/services/rewards/generator-impl-v9-v10-rolling.go index abdc214d3..666ff9ceb 100644 --- a/shared/services/rewards/generator-impl-v9-v10-rolling.go +++ b/shared/services/rewards/generator-impl-v9-v10-rolling.go @@ -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" @@ -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 diff --git a/shared/services/rewards/generator-impl-v9-v10.go b/shared/services/rewards/generator-impl-v9-v10.go index 1ad331529..a841671d9 100644 --- a/shared/services/rewards/generator-impl-v9-v10.go +++ b/shared/services/rewards/generator-impl-v9-v10.go @@ -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" @@ -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 @@ -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) @@ -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 } @@ -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 @@ -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 } @@ -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) diff --git a/shared/services/rewards/generator.go b/shared/services/rewards/generator.go index e6678f4aa..12bfca392 100644 --- a/shared/services/rewards/generator.go +++ b/shared/services/rewards/generator.go @@ -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 diff --git a/shared/services/rewards/mock_v10_test.go b/shared/services/rewards/mock_v10_test.go index 9da3a82bc..4dcac19d2 100644 --- a/shared/services/rewards/mock_v10_test.go +++ b/shared/services/rewards/mock_v10_test.go @@ -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, } } @@ -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)) @@ -250,6 +252,14 @@ 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) @@ -257,6 +267,15 @@ func TestMockIntervalDefaultsTreegenv10(tt *testing.T) { 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()) @@ -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 { @@ -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 { @@ -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()) } @@ -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 { diff --git a/shared/services/rewards/rewards-file-v1.go b/shared/services/rewards/rewards-file-v1.go index d24568343..d0d0fbb0a 100644 --- a/shared/services/rewards/rewards-file-v1.go +++ b/shared/services/rewards/rewards-file-v1.go @@ -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 { diff --git a/shared/services/rewards/rewards-file-v2.go b/shared/services/rewards/rewards-file-v2.go index 0ef2747ed..09bfa69cb 100644 --- a/shared/services/rewards/rewards-file-v2.go +++ b/shared/services/rewards/rewards-file-v2.go @@ -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 { diff --git a/shared/services/rewards/rolling-manager.go b/shared/services/rewards/rolling-manager.go index d52199e2d..6a0dda1e1 100644 --- a/shared/services/rewards/rolling-manager.go +++ b/shared/services/rewards/rolling-manager.go @@ -19,6 +19,7 @@ import ( "github.com/rocket-pool/smartnode/shared/services/beacon" "github.com/rocket-pool/smartnode/shared/services/config" "github.com/rocket-pool/smartnode/shared/services/state" + cfgtypes "github.com/rocket-pool/smartnode/shared/types/config" "github.com/rocket-pool/smartnode/shared/utils/log" ) @@ -42,6 +43,7 @@ type RollingRecordManager struct { log *log.ColorLogger errLog *log.ColorLogger + network cfgtypes.Network logPrefix string rp RewardsExecutionClient bc RewardsBeaconClient @@ -71,6 +73,7 @@ type RollingRecordManagerSettings struct { RecordCheckpointInterval uint64 PreviousRewardsPoolAddresses []common.Address StateProvider StateProvider + Network cfgtypes.Network } // Creates a new manager for rolling records. @@ -112,7 +115,7 @@ func NewRollingRecordManager(startSlot uint64, rewardsInterval uint64, settings logPrefix := "[Rolling Record]" settings.Log.Printlnf("%s Created Rolling Record manager for start slot %d.", logPrefix, startSlot) return &RollingRecordManager{ - Record: NewRollingRecord(settings.Log, logPrefix, settings.BC, startSlot, &beaconCfg, rewardsInterval), + Record: NewRollingRecord(settings.Log, logPrefix, settings.Network, settings.BC, startSlot, &beaconCfg, rewardsInterval), log: settings.Log, errLog: settings.ErrLog, @@ -130,6 +133,7 @@ func NewRollingRecordManager(startSlot uint64, rewardsInterval uint64, settings checkpointRetentionLimit: settings.CheckpointRetentionLimit, recordCheckpointInterval: settings.RecordCheckpointInterval, previousRewardsPoolAddresses: settings.PreviousRewardsPoolAddresses, + network: settings.Network, }, nil } @@ -289,7 +293,7 @@ func (r *RollingRecordManager) LoadBestRecordFromDisk(startSlot uint64, targetSl if !exists { // There isn't a checksum file so start over r.log.Printlnf("%s Checksum file not found, creating a new record from the start of the interval.", r.logPrefix) - record := NewRollingRecord(r.log, r.logPrefix, r.bc, startSlot, &r.beaconCfg, rewardsInterval) + record := NewRollingRecord(r.log, r.logPrefix, r.network, r.bc, startSlot, &r.beaconCfg, rewardsInterval) r.Record = record r.nextEpochToSave = startSlot/r.beaconCfg.SlotsPerEpoch + r.recordCheckpointInterval - 1 return record, nil @@ -337,6 +341,12 @@ func (r *RollingRecordManager) LoadBestRecordFromDisk(startSlot uint64, targetSl continue } + // Check if it has the proper network + if record.Network != r.network { + r.log.Printlnf("%s File [%s] was for network %s instead of %s so it cannot be used, trying an earlier checkpoint.", r.logPrefix, filename, record.Network, r.network) + continue + } + // Check if it has the proper start slot if record.StartSlot != startSlot { r.log.Printlnf("%s File [%s] started on slot %d instead of %d so it cannot be used, trying an earlier checkpoint.", r.logPrefix, filename, record.StartSlot, startSlot) @@ -368,7 +378,7 @@ func (r *RollingRecordManager) LoadBestRecordFromDisk(startSlot uint64, targetSl // If we got here then none of the saved files worked so we have to make a new record r.log.Printlnf("%s None of the saved record checkpoint files were eligible for use, creating a new record from the start of the interval.", r.logPrefix) - record := NewRollingRecord(r.log, r.logPrefix, r.bc, startSlot, &r.beaconCfg, rewardsInterval) + record := NewRollingRecord(r.log, r.logPrefix, r.network, r.bc, startSlot, &r.beaconCfg, rewardsInterval) r.Record = record r.nextEpochToSave = startSlot/r.beaconCfg.SlotsPerEpoch + r.recordCheckpointInterval - 1 return record, nil @@ -654,7 +664,7 @@ func (r *RollingRecordManager) createNewRecord(state *state.NetworkState) error // Create a new record for the start slot r.log.Printlnf("%s Current record is for interval %d which has passed, creating a new record for interval %d starting on slot %d (epoch %d).", r.logPrefix, r.Record.RewardsInterval, state.NetworkDetails.RewardIndex, startSlot, newEpoch) - r.Record = NewRollingRecord(r.log, r.logPrefix, r.bc, startSlot, &r.beaconCfg, state.NetworkDetails.RewardIndex) + r.Record = NewRollingRecord(r.log, r.logPrefix, r.network, r.bc, startSlot, &r.beaconCfg, state.NetworkDetails.RewardIndex) r.startSlot = startSlot r.nextEpochToSave = startSlot/r.beaconCfg.SlotsPerEpoch + r.recordCheckpointInterval - 1 diff --git a/shared/services/rewards/rolling-record.go b/shared/services/rewards/rolling-record.go index a0d2a7d5a..da7611c3a 100644 --- a/shared/services/rewards/rolling-record.go +++ b/shared/services/rewards/rolling-record.go @@ -10,7 +10,9 @@ import ( "github.com/rocket-pool/rocketpool-go/types" "github.com/rocket-pool/smartnode/shared" "github.com/rocket-pool/smartnode/shared/services/beacon" + "github.com/rocket-pool/smartnode/shared/services/rewards/fees" "github.com/rocket-pool/smartnode/shared/services/state" + cfgtypes "github.com/rocket-pool/smartnode/shared/types/config" "github.com/rocket-pool/smartnode/shared/utils/log" "golang.org/x/sync/errgroup" ) @@ -36,6 +38,8 @@ type RollingRecord struct { RewardsInterval uint64 `json:"rewardsInterval"` SmartnodeVersion string `json:"smartnodeVersion,omitempty"` BCBalanceMap map[string]BCBalances `json:"bcBalanceMap"` + RulesetVersion uint64 `json:"rulesetVersion"` + Network cfgtypes.Network `json:"network"` // Private fields bc RewardsBeaconClient `json:"-"` @@ -47,13 +51,16 @@ type RollingRecord struct { } // Create a new rolling record wrapper -func NewRollingRecord(log *log.ColorLogger, logPrefix string, bc RewardsBeaconClient, startSlot uint64, beaconConfig *beacon.Eth2Config, rewardsInterval uint64) *RollingRecord { +func NewRollingRecord(log *log.ColorLogger, logPrefix string, network cfgtypes.Network, bc RewardsBeaconClient, startSlot uint64, beaconConfig *beacon.Eth2Config, rewardsInterval uint64) *RollingRecord { + rulesetVersion := GetRulesetVersion(network, rewardsInterval) return &RollingRecord{ StartSlot: startSlot, LastDutiesSlot: 0, ValidatorIndexMap: map[string]*MinipoolInfo{}, RewardsInterval: rewardsInterval, SmartnodeVersion: shared.RocketPoolVersion, + RulesetVersion: rulesetVersion, + Network: network, bc: bc, beaconConfig: beaconConfig, @@ -182,6 +189,8 @@ func (r *RollingRecord) Serialize() ([]byte, error) { LastDutiesSlot: r.LastDutiesSlot, RewardsInterval: r.RewardsInterval, SmartnodeVersion: r.SmartnodeVersion, + RulesetVersion: r.RulesetVersion, + Network: r.Network, ValidatorIndexMap: map[string]*MinipoolInfo{}, // Don't bother cloning the balance map since it isn't // stripped like the ValidatorIndexMap @@ -423,7 +432,19 @@ func (r *RollingRecord) processAttestationsInSlot(inclusionSlot uint64, attestat // Get the pseudoscore for this attestation details := state.MinipoolDetailsByAddress[validator.Address] - minipoolScore := details.GetMinipoolAttestationScore(blockTime) + bond, fee := details.GetMinipoolBondAndNodeFee(blockTime) + + if r.RulesetVersion >= 10 { + nnd := state.NodeDetailsByAddress[validator.NodeAddress] + eligibleBorrowedEth := state.GetEligibleBorrowedEth(nnd) + _, percentOfBorrowedEth := state.GetStakedRplValueInEthAndPercentOfBorrowedEth(eligibleBorrowedEth, nnd.RplStake) + 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 validator.AttestationScore.Add(&validator.AttestationScore.Int, minipoolScore) diff --git a/shared/services/rewards/test/mock.go b/shared/services/rewards/test/mock.go index 1a4ffcf18..9b1ad0995 100644 --- a/shared/services/rewards/test/mock.go +++ b/shared/services/rewards/test/mock.go @@ -11,6 +11,7 @@ import ( "github.com/rocket-pool/rocketpool-go/utils/eth" rpstate "github.com/rocket-pool/rocketpool-go/utils/state" "github.com/rocket-pool/smartnode/shared/services/beacon" + "github.com/rocket-pool/smartnode/shared/services/rewards/fees" "github.com/rocket-pool/smartnode/shared/services/state" ) @@ -45,6 +46,9 @@ func (h *MockHistory) GetNodeAddress() common.Address { return h.lastNodeAddress } +var oneEth = big.NewInt(1000000000000000000) +var thirtyTwoEth = big.NewInt(0).Mul(oneEth, big.NewInt(32)) + func (h *MockHistory) GetMinipoolAttestationScoreAndCount(address common.Address, state *state.NetworkState) (*big.Int, uint64) { out := big.NewInt(0) mpi := state.MinipoolDetailsByAddress[address] @@ -74,9 +78,16 @@ func (h *MockHistory) GetMinipoolAttestationScoreAndCount(address common.Address if indexInt%32 == uint64(slot%32) { count++ + bond, fee := mpi.GetMinipoolBondAndNodeFee(blockTime) // Give the minipool a score according to its fee - score := mpi.GetMinipoolAttestationScore(blockTime) - out.Add(out, score) + eligibleBorrowedEth := state.GetEligibleBorrowedEth(nodeDetails) + _, percentOfBorrowedEth := state.GetStakedRplValueInEthAndPercentOfBorrowedEth(eligibleBorrowedEth, nodeDetails.RplStake) + 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) + out.Add(out, minipoolScore) } } return out, count diff --git a/shared/services/rewards/types.go b/shared/services/rewards/types.go index 30d1bd65a..b1d7e4020 100644 --- a/shared/services/rewards/types.go +++ b/shared/services/rewards/types.go @@ -154,6 +154,7 @@ type ISmoothingPoolMinipoolPerformance interface { GetBonusEthEarned() *big.Int GetEffectiveCommission() *big.Int GetConsensusIncome() *big.Int + GetAttestationScore() *big.Int } // Small struct to test version information for rewards files during deserialization