diff --git a/components/restapi/core/accounts.go b/components/restapi/core/accounts.go index d6d379fd5..f324acc29 100644 --- a/components/restapi/core/accounts.go +++ b/components/restapi/core/accounts.go @@ -185,7 +185,7 @@ func rewardsByOutputID(c echo.Context) (*api.ManaRewardsResponse, error) { } var reward iotago.Mana - var actualStart, actualEnd iotago.EpochIndex + var firstRewardEpoch, lastRewardEpoch iotago.EpochIndex switch utxoOutput.OutputType() { case iotago.OutputAccount: //nolint:forcetypeassert @@ -198,33 +198,38 @@ func rewardsByOutputID(c echo.Context) (*api.ManaRewardsResponse, error) { //nolint:forcetypeassert stakingFeature := feature.(*iotago.StakingFeature) + apiForSlot := deps.Protocol.APIForSlot(slotIndex) + futureBoundedSlotIndex := slotIndex + apiForSlot.ProtocolParameters().MinCommittableAge() + claimingEpoch := apiForSlot.TimeProvider().EpochFromSlot(futureBoundedSlotIndex) + // check if the account is a validator - reward, actualStart, actualEnd, err = deps.Protocol.Engines.Main.Get().SybilProtection.ValidatorReward( + reward, firstRewardEpoch, lastRewardEpoch, err = deps.Protocol.Engines.Main.Get().SybilProtection.ValidatorReward( accountOutput.AccountID, - stakingFeature.StakedAmount, - stakingFeature.StartEpoch, - stakingFeature.EndEpoch, + stakingFeature, + claimingEpoch, ) case iotago.OutputDelegation: //nolint:forcetypeassert delegationOutput := utxoOutput.Output().(*iotago.DelegationOutput) delegationEnd := delegationOutput.EndEpoch + apiForSlot := deps.Protocol.APIForSlot(slotIndex) + futureBoundedSlotIndex := slotIndex + apiForSlot.ProtocolParameters().MinCommittableAge() + claimingEpoch := apiForSlot.TimeProvider().EpochFromSlot(futureBoundedSlotIndex) // If Delegation ID is zeroed, the output is in delegating state, which means its End Epoch is not set and we must use the // "last epoch" for the rewards calculation. // In this case the calculation must be consistent with the rewards calculation at execution time, so a client can specify // a slot index explicitly, which should be equal to the slot it uses as the commitment input for the claiming transaction. if delegationOutput.DelegationID.Empty() { - apiForSlot := deps.Protocol.APIForSlot(slotIndex) - futureBoundedSlotIndex := slotIndex + apiForSlot.ProtocolParameters().MinCommittableAge() - delegationEnd = apiForSlot.TimeProvider().EpochFromSlot(futureBoundedSlotIndex) - iotago.EpochIndex(1) + delegationEnd = claimingEpoch - iotago.EpochIndex(1) } - reward, actualStart, actualEnd, err = deps.Protocol.Engines.Main.Get().SybilProtection.DelegatorReward( + reward, firstRewardEpoch, lastRewardEpoch, err = deps.Protocol.Engines.Main.Get().SybilProtection.DelegatorReward( delegationOutput.ValidatorAddress.AccountID(), delegationOutput.DelegatedAmount, delegationOutput.StartEpoch, delegationEnd, + claimingEpoch, ) } if err != nil { @@ -232,8 +237,8 @@ func rewardsByOutputID(c echo.Context) (*api.ManaRewardsResponse, error) { } return &api.ManaRewardsResponse{ - StartEpoch: actualStart, - EndEpoch: actualEnd, + StartEpoch: firstRewardEpoch, + EndEpoch: lastRewardEpoch, Rewards: reward, }, nil } diff --git a/pkg/protocol/engine/ledger/ledger/vm.go b/pkg/protocol/engine/ledger/ledger/vm.go index f86162e61..78fb952fd 100644 --- a/pkg/protocol/engine/ledger/ledger/vm.go +++ b/pkg/protocol/engine/ledger/ledger/vm.go @@ -97,9 +97,13 @@ func (v *VM) ValidateSignatures(signedTransaction mempool.SignedTransaction, res accountID = iotago.AccountIDFromOutputID(outputID) } - reward, _, _, rewardErr := v.ledger.sybilProtection.ValidatorReward(accountID, stakingFeature.StakedAmount, stakingFeature.StartEpoch, stakingFeature.EndEpoch) + apiForSlot := v.ledger.apiProvider.APIForSlot(commitmentInput.Slot) + futureBoundedSlotIndex := commitmentInput.Slot + apiForSlot.ProtocolParameters().MinCommittableAge() + claimingEpoch := apiForSlot.TimeProvider().EpochFromSlot(futureBoundedSlotIndex) + + reward, _, _, rewardErr := v.ledger.sybilProtection.ValidatorReward(accountID, stakingFeature, claimingEpoch) if rewardErr != nil { - return nil, ierrors.Wrapf(iotago.ErrFailedToClaimStakingReward, "failed to get Validator reward for AccountOutput %s at index %d (StakedAmount: %d, StartEpoch: %d, EndEpoch: %d", outputID, inp.Index, stakingFeature.StakedAmount, stakingFeature.StartEpoch, stakingFeature.EndEpoch) + return nil, ierrors.Wrapf(iotago.ErrFailedToClaimStakingReward, "failed to get Validator reward for AccountOutput %s at index %d (StakedAmount: %d, StartEpoch: %d, EndEpoch: %d, claimingEpoch: %d", outputID, inp.Index, stakingFeature.StakedAmount, stakingFeature.StartEpoch, stakingFeature.EndEpoch, claimingEpoch) } rewardInputSet[accountID] = reward @@ -108,17 +112,19 @@ func (v *VM) ValidateSignatures(signedTransaction mempool.SignedTransaction, res delegationID := castOutput.DelegationID delegationEnd := castOutput.EndEpoch + apiForSlot := v.ledger.apiProvider.APIForSlot(commitmentInput.Slot) + futureBoundedSlotIndex := commitmentInput.Slot + apiForSlot.ProtocolParameters().MinCommittableAge() + claimingEpoch := apiForSlot.TimeProvider().EpochFromSlot(futureBoundedSlotIndex) + if delegationID.Empty() { delegationID = iotago.DelegationIDFromOutputID(outputID) // If Delegation ID is zeroed, the output is in delegating state, which means its End Epoch is not set and we must use the // "last epoch", which is the epoch index corresponding to the future bounded slot index minus 1. - apiForSlot := v.ledger.apiProvider.APIForSlot(commitmentInput.Slot) - futureBoundedSlotIndex := commitmentInput.Slot + apiForSlot.ProtocolParameters().MinCommittableAge() - delegationEnd = apiForSlot.TimeProvider().EpochFromSlot(futureBoundedSlotIndex) - iotago.EpochIndex(1) + delegationEnd = claimingEpoch - iotago.EpochIndex(1) } - reward, _, _, rewardErr := v.ledger.sybilProtection.DelegatorReward(castOutput.ValidatorAddress.AccountID(), castOutput.DelegatedAmount, castOutput.StartEpoch, delegationEnd) + reward, _, _, rewardErr := v.ledger.sybilProtection.DelegatorReward(castOutput.ValidatorAddress.AccountID(), castOutput.DelegatedAmount, castOutput.StartEpoch, delegationEnd, claimingEpoch) if rewardErr != nil { return nil, ierrors.Wrapf(iotago.ErrFailedToClaimDelegationReward, "failed to get Delegator reward for DelegationOutput %s at index %d (StakedAmount: %d, StartEpoch: %d, EndEpoch: %d", outputID, inp.Index, castOutput.DelegatedAmount, castOutput.StartEpoch, castOutput.EndEpoch) } diff --git a/pkg/protocol/sybilprotection/sybilprotection.go b/pkg/protocol/sybilprotection/sybilprotection.go index 147e8580c..675ee1f4a 100644 --- a/pkg/protocol/sybilprotection/sybilprotection.go +++ b/pkg/protocol/sybilprotection/sybilprotection.go @@ -16,12 +16,18 @@ type SybilProtection interface { EligibleValidators(epoch iotago.EpochIndex) (accounts.AccountsData, error) OrderedRegisteredCandidateValidatorsList(epoch iotago.EpochIndex) ([]*api.ValidatorResponse, error) IsCandidateActive(validatorID iotago.AccountID, epoch iotago.EpochIndex) (bool, error) - // ValidatorReward returns the amount of mana that a validator has earned in a given epoch range. - // The actual used epoch range is returned, only until usedEnd the decay was applied. - ValidatorReward(validatorID iotago.AccountID, stakeAmount iotago.BaseToken, epochStart, epochEnd iotago.EpochIndex) (validatorReward iotago.Mana, decayedStart, decayedEnd iotago.EpochIndex, err error) + // ValidatorReward returns the amount of mana that a validator with the given staking feature has earned in the feature's epoch range. + // + // The first epoch in which rewards existed is returned (firstRewardEpoch). + // Since the validator may still be active and the EndEpoch might be in the future, the epoch until which rewards were calculated is returned in addition to the first epoch in which rewards existed (lastRewardEpoch). + // The rewards are decayed until claimingEpoch, which should be set to the epoch in which the rewards would be claimed. + ValidatorReward(validatorID iotago.AccountID, stakingFeature *iotago.StakingFeature, claimingEpoch iotago.EpochIndex) (validatorReward iotago.Mana, firstRewardEpoch iotago.EpochIndex, lastRewardEpoch iotago.EpochIndex, err error) // DelegatorReward returns the amount of mana that a delegator has earned in a given epoch range. - // The actual used epoch range is returned, only until usedEnd the decay was applied. - DelegatorReward(validatorID iotago.AccountID, delegatedAmount iotago.BaseToken, epochStart, epochEnd iotago.EpochIndex) (delegatorsReward iotago.Mana, decayedStart, decayedEnd iotago.EpochIndex, err error) + // + // The first epoch in which rewards existed is returned (firstRewardEpoch). + // Since the Delegation Output's EndEpoch might be unset due to an ongoing delegation, the epoch until which rewards were calculated is also returned (lastRewardEpoch). + // The rewards are decayed until claimingEpoch, which should be set to the epoch in which the rewards would be claimed. + DelegatorReward(validatorID iotago.AccountID, delegatedAmount iotago.BaseToken, epochStart iotago.EpochIndex, epochEnd iotago.EpochIndex, claimingEpoch iotago.EpochIndex) (delegatorReward iotago.Mana, firstRewardEpoch iotago.EpochIndex, lastRewardEpoch iotago.EpochIndex, err error) SeatManager() seatmanager.SeatManager CommitSlot(iotago.SlotIndex) (iotago.Identifier, iotago.Identifier, error) Import(io.ReadSeeker) error diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/rewards.go b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/rewards.go index 31d13e58d..271a6fc76 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/rewards.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/rewards.go @@ -20,18 +20,21 @@ func (t *Tracker) RewardsRoot(epoch iotago.EpochIndex) (iotago.Identifier, error return m.Root(), nil } -func (t *Tracker) ValidatorReward(validatorID iotago.AccountID, stakeAmount iotago.BaseToken, epochStart iotago.EpochIndex, epochEnd iotago.EpochIndex) (iotago.Mana, iotago.EpochIndex, iotago.EpochIndex, error) { +func (t *Tracker) ValidatorReward(validatorID iotago.AccountID, stakingFeature *iotago.StakingFeature, claimingEpoch iotago.EpochIndex) (validatorReward iotago.Mana, firstRewardEpoch iotago.EpochIndex, lastRewardEpoch iotago.EpochIndex, err error) { t.mutex.RLock() defer t.mutex.RUnlock() - var validatorReward iotago.Mana + validatorReward = 0 + stakedAmount := stakingFeature.StakedAmount + firstRewardEpoch = stakingFeature.StartEpoch + lastRewardEpoch = stakingFeature.EndEpoch - // limit looping to committed epochs - if epochEnd > t.latestAppliedEpoch { - epochEnd = t.latestAppliedEpoch + // Limit reward fetching only to committed epochs. + if lastRewardEpoch > t.latestAppliedEpoch { + lastRewardEpoch = t.latestAppliedEpoch } - for epoch := epochStart; epoch <= epochEnd; epoch++ { + for epoch := firstRewardEpoch; epoch <= lastRewardEpoch; epoch++ { rewardsForAccountInEpoch, exists, err := t.rewardsForAccount(validatorID, epoch) if err != nil { return 0, 0, 0, ierrors.Wrapf(err, "failed to get rewards for account %s in epoch %d", validatorID, epoch) @@ -39,8 +42,8 @@ func (t *Tracker) ValidatorReward(validatorID iotago.AccountID, stakeAmount iota if !exists || rewardsForAccountInEpoch.PoolStake == 0 { // updating epoch start for beginning epochs without the reward - if epoch < epochEnd && epochStart == epoch { - epochStart = epoch + 1 + if epoch < lastRewardEpoch && firstRewardEpoch == epoch { + firstRewardEpoch = epoch + 1 } continue @@ -55,7 +58,7 @@ func (t *Tracker) ValidatorReward(validatorID iotago.AccountID, stakeAmount iota return 0, 0, 0, ierrors.Errorf("pool stats for epoch %d and validator accountID %s are nil", epoch, validatorID) } - // if validator's fixed cost is greater than earned reward, all reward goes for delegators + // If a validator's fixed cost is greater than the earned reward, all rewards go to the delegators. if rewardsForAccountInEpoch.PoolRewards < rewardsForAccountInEpoch.FixedCost { continue } @@ -79,7 +82,7 @@ func (t *Tracker) ValidatorReward(validatorID iotago.AccountID, stakeAmount iota return 0, 0, 0, ierrors.Wrapf(err, "failed to calculate profit margin factor due to overflow for epoch %d and validator accountID %s", epoch, validatorID) } - residualValidatorFactor, err := safemath.Safe64MulDiv(result>>profitMarginExponent, uint64(stakeAmount), uint64(rewardsForAccountInEpoch.PoolStake)) + residualValidatorFactor, err := safemath.Safe64MulDiv(result>>profitMarginExponent, uint64(stakedAmount), uint64(rewardsForAccountInEpoch.PoolStake)) if err != nil { return 0, 0, 0, ierrors.Wrapf(err, "failed to calculate residual validator factor due to overflow for epoch %d and validator accountID %s", epoch, validatorID) } @@ -89,13 +92,13 @@ func (t *Tracker) ValidatorReward(validatorID iotago.AccountID, stakeAmount iota return 0, 0, 0, ierrors.Wrapf(err, "failed to calculate un-decayed epoch reward due to overflow for epoch %d and validator accountID %s", epoch, validatorID) } - unDecayedEpochRewards, err := safemath.SafeAdd(result, residualValidatorFactor) + undecayedEpochRewards, err := safemath.SafeAdd(result, residualValidatorFactor) if err != nil { return 0, 0, 0, ierrors.Wrapf(err, "failed to calculate un-decayed epoch rewards due to overflow for epoch %d and validator accountID %s", epoch, validatorID) } decayProvider := t.apiProvider.APIForEpoch(epoch).ManaDecayProvider() - decayedEpochRewards, err := decayProvider.DecayManaByEpochs(iotago.Mana(unDecayedEpochRewards), epoch, epochEnd) + decayedEpochRewards, err := decayProvider.DecayManaByEpochs(iotago.Mana(undecayedEpochRewards), epoch, claimingEpoch) if err != nil { return 0, 0, 0, ierrors.Wrapf(err, "failed to calculate rewards with decay for epoch %d and validator accountID %s", epoch, validatorID) } @@ -106,21 +109,24 @@ func (t *Tracker) ValidatorReward(validatorID iotago.AccountID, stakeAmount iota } } - return validatorReward, epochStart, epochEnd, nil + return validatorReward, firstRewardEpoch, lastRewardEpoch, nil } -func (t *Tracker) DelegatorReward(validatorID iotago.AccountID, delegatedAmount iotago.BaseToken, epochStart iotago.EpochIndex, epochEnd iotago.EpochIndex) (iotago.Mana, iotago.EpochIndex, iotago.EpochIndex, error) { +func (t *Tracker) DelegatorReward(validatorID iotago.AccountID, delegatedAmount iotago.BaseToken, epochStart iotago.EpochIndex, epochEnd iotago.EpochIndex, claimingEpoch iotago.EpochIndex) (delegatorReward iotago.Mana, firstRewardEpoch iotago.EpochIndex, lastRewardEpoch iotago.EpochIndex, err error) { t.mutex.RLock() defer t.mutex.RUnlock() var delegatorsReward iotago.Mana + firstRewardEpoch = epochStart + lastRewardEpoch = epochEnd + // limit looping to committed epochs - if epochEnd > t.latestAppliedEpoch { - epochEnd = t.latestAppliedEpoch + if lastRewardEpoch > t.latestAppliedEpoch { + lastRewardEpoch = t.latestAppliedEpoch } - for epoch := epochStart; epoch <= epochEnd; epoch++ { + for epoch := firstRewardEpoch; epoch <= lastRewardEpoch; epoch++ { rewardsForAccountInEpoch, exists, err := t.rewardsForAccount(validatorID, epoch) if err != nil { return 0, 0, 0, ierrors.Wrapf(err, "failed to get rewards for account %s in epoch %d", validatorID, epoch) @@ -128,8 +134,8 @@ func (t *Tracker) DelegatorReward(validatorID iotago.AccountID, delegatedAmount if !exists || rewardsForAccountInEpoch.PoolStake == 0 { // updating epoch start for beginning epochs without the reward - if epochStart == epoch { - epochStart = epoch + 1 + if firstRewardEpoch == epoch { + firstRewardEpoch = epoch + 1 } continue @@ -166,13 +172,13 @@ func (t *Tracker) DelegatorReward(validatorID iotago.AccountID, delegatedAmount return 0, 0, 0, ierrors.Wrapf(err, "failed to calculate unDecayedEpochRewards due to overflow for epoch %d and validator accountID %s", epoch, validatorID) } - unDecayedEpochRewards, err := safemath.SafeDiv(result, uint64(rewardsForAccountInEpoch.PoolStake)) + undecayedEpochRewards, err := safemath.SafeDiv(result, uint64(rewardsForAccountInEpoch.PoolStake)) if err != nil { return 0, 0, 0, ierrors.Wrapf(err, "failed to calculate unDecayedEpochRewards due to overflow for epoch %d and validator accountID %s", epoch, validatorID) } decayProvider := t.apiProvider.APIForEpoch(epoch).ManaDecayProvider() - decayedEpochRewards, err := decayProvider.DecayManaByEpochs(iotago.Mana(unDecayedEpochRewards), epoch, epochEnd) + decayedEpochRewards, err := decayProvider.DecayManaByEpochs(iotago.Mana(undecayedEpochRewards), epoch, claimingEpoch) if err != nil { return 0, 0, 0, ierrors.Wrapf(err, "failed to calculate rewards with decay for epoch %d and validator accountID %s", epoch, validatorID) } @@ -180,7 +186,7 @@ func (t *Tracker) DelegatorReward(validatorID iotago.AccountID, delegatedAmount delegatorsReward += decayedEpochRewards } - return delegatorsReward, epochStart, epochEnd, nil + return delegatorsReward, firstRewardEpoch, lastRewardEpoch, nil } func (t *Tracker) rewardsMap(epoch iotago.EpochIndex) (ads.Map[iotago.Identifier, iotago.AccountID, *model.PoolRewards], error) { diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/testsuite_test.go b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/testsuite_test.go index 77c48a11c..6f47f856a 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/testsuite_test.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/testsuite_test.go @@ -165,13 +165,19 @@ func (t *TestSuite) AssertEpochRewards(epoch iotago.EpochIndex, actions map[stri expectedValidatorReward := t.validatorReward(alias, epoch, t.epochStats[epoch].ProfitMargin, uint64(poolRewards), uint64(action.ValidatorStake), uint64(action.PoolStake), uint64(action.FixedCost), action) accountID := t.Account(alias, true) - actualValidatorReward, _, _, err := t.Instance.ValidatorReward(accountID, actions[alias].ValidatorStake, epoch, epoch) + actualValidatorReward, _, _, err := t.Instance.ValidatorReward(accountID, + &iotago.StakingFeature{ + StakedAmount: actions[alias].ValidatorStake, + StartEpoch: epoch, + EndEpoch: epoch, + }, + epoch) require.NoError(t.T, err) require.Equal(t.T, expectedValidatorReward, actualValidatorReward) for delegatedAmount := range action.Delegators { expectedDelegatorReward := t.delegatorReward(epoch, t.epochStats[epoch].ProfitMargin, uint64(poolRewards), uint64(delegatedAmount), uint64(action.PoolStake), uint64(action.FixedCost), action) - actualDelegatorReward, _, _, err := t.Instance.DelegatorReward(accountID, iotago.BaseToken(delegatedAmount), epoch, epoch) + actualDelegatorReward, _, _, err := t.Instance.DelegatorReward(accountID, iotago.BaseToken(delegatedAmount), epoch, epoch, epoch) require.NoError(t.T, err) require.Equal(t.T, expectedDelegatorReward, actualDelegatorReward) } @@ -181,13 +187,19 @@ func (t *TestSuite) AssertEpochRewards(epoch iotago.EpochIndex, actions map[stri func (t *TestSuite) AssertNoReward(alias string, epoch iotago.EpochIndex, actions map[string]*EpochActions) { accID := t.Account(alias, false) - actualValidatorReward, _, _, err := t.Instance.ValidatorReward(accID, actions[alias].ValidatorStake, epoch, epoch) + actualValidatorReward, _, _, err := t.Instance.ValidatorReward(accID, + &iotago.StakingFeature{ + StakedAmount: actions[alias].ValidatorStake, + StartEpoch: epoch, + EndEpoch: epoch, + }, + epoch) require.NoError(t.T, err) require.Equal(t.T, iotago.Mana(0), actualValidatorReward) action, exists := actions[alias] require.True(t.T, exists) for delegatedAmount := range action.Delegators { - actualDelegatorReward, _, _, err := t.Instance.DelegatorReward(accID, iotago.BaseToken(delegatedAmount), epoch, epoch) + actualDelegatorReward, _, _, err := t.Instance.DelegatorReward(accID, iotago.BaseToken(delegatedAmount), epoch, epoch, epoch) require.NoError(t.T, err) require.Equal(t.T, iotago.Mana(0), actualDelegatorReward) } @@ -195,14 +207,20 @@ func (t *TestSuite) AssertNoReward(alias string, epoch iotago.EpochIndex, action func (t *TestSuite) AssertRewardForDelegatorsOnly(alias string, epoch iotago.EpochIndex, actions map[string]*EpochActions) { accID := t.Account(alias, false) - actualValidatorReward, _, _, err := t.Instance.ValidatorReward(accID, actions[alias].ValidatorStake, epoch, epoch) + actualValidatorReward, _, _, err := t.Instance.ValidatorReward(accID, + &iotago.StakingFeature{ + StakedAmount: actions[alias].ValidatorStake, + StartEpoch: epoch, + EndEpoch: epoch, + }, + epoch) require.NoError(t.T, err) require.Equal(t.T, iotago.Mana(0), actualValidatorReward) action, exists := actions[alias] require.True(t.T, exists) for delegatedAmount := range action.Delegators { - actualDelegatorReward, _, _, err := t.Instance.DelegatorReward(accID, iotago.BaseToken(delegatedAmount), epoch, epoch) + actualDelegatorReward, _, _, err := t.Instance.DelegatorReward(accID, iotago.BaseToken(delegatedAmount), epoch, epoch, epoch) expectedDelegatorReward := t.delegatorReward(epoch, t.epochStats[epoch].ProfitMargin, uint64(t.poolRewards[epoch][alias].PoolRewards), uint64(delegatedAmount), uint64(action.PoolStake), uint64(action.FixedCost), action) require.NoError(t.T, err) @@ -320,12 +338,18 @@ func (t *TestSuite) calculateExpectedRewards(epochsCount int, epochActions map[s delegatorRewardPerAccount[epoch] = make(map[string]iotago.Mana) validatorRewardPerAccount[epoch] = make(map[string]iotago.Mana) for aliasAccount := range epochActions { - reward, _, _, err := t.Instance.DelegatorReward(t.Account(aliasAccount, false), 1, epoch, epoch) + reward, _, _, err := t.Instance.DelegatorReward(t.Account(aliasAccount, false), 1, epoch, epoch, epoch) require.NoError(t.T, err) delegatorRewardPerAccount[epoch][aliasAccount] = reward } for aliasAccount := range epochActions { - reward, _, _, err := t.Instance.ValidatorReward(t.Account(aliasAccount, true), 1, epoch, epoch) + reward, _, _, err := t.Instance.ValidatorReward(t.Account(aliasAccount, true), + &iotago.StakingFeature{ + StakedAmount: 1, + StartEpoch: epoch, + EndEpoch: epoch, + }, + epoch) require.NoError(t.T, err) validatorRewardPerAccount[epoch][aliasAccount] = reward } @@ -335,11 +359,23 @@ func (t *TestSuite) calculateExpectedRewards(epochsCount int, epochActions map[s func (t *TestSuite) AssertValidatorRewardGreaterThan(alias1 string, alias2 string, epoch iotago.EpochIndex, actions map[string]*EpochActions) { accID1 := t.Account(alias1, false) - actualValidatorReward1, _, _, err := t.Instance.ValidatorReward(accID1, actions[alias1].ValidatorStake, epoch, epoch) + actualValidatorReward1, _, _, err := t.Instance.ValidatorReward(accID1, + &iotago.StakingFeature{ + StakedAmount: actions[alias1].ValidatorStake, + StartEpoch: epoch, + EndEpoch: epoch, + }, + epoch) require.NoError(t.T, err) accID2 := t.Account(alias2, false) - actualValidatorReward2, _, _, err := t.Instance.ValidatorReward(accID2, actions[alias2].ValidatorStake, epoch, epoch) + actualValidatorReward2, _, _, err := t.Instance.ValidatorReward(accID2, + &iotago.StakingFeature{ + StakedAmount: actions[alias2].ValidatorStake, + StartEpoch: epoch, + EndEpoch: epoch, + }, + epoch) require.NoError(t.T, err) require.Greater(t.T, actualValidatorReward1, actualValidatorReward2) diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go b/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go index 487c1e0f3..a7b78e92c 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go @@ -246,12 +246,12 @@ func (o *SybilProtection) SeatManager() seatmanager.SeatManager { return o.seatManager } -func (o *SybilProtection) ValidatorReward(validatorID iotago.AccountID, stakeAmount iotago.BaseToken, epochStart iotago.EpochIndex, epochEnd iotago.EpochIndex) (validatorReward iotago.Mana, actualEpochStart iotago.EpochIndex, actualEpochEnd iotago.EpochIndex, err error) { - return o.performanceTracker.ValidatorReward(validatorID, stakeAmount, epochStart, epochEnd) +func (o *SybilProtection) ValidatorReward(validatorID iotago.AccountID, stakingFeature *iotago.StakingFeature, claimingEpoch iotago.EpochIndex) (validatorReward iotago.Mana, firstRewardEpoch iotago.EpochIndex, lastRewardEpoch iotago.EpochIndex, err error) { + return o.performanceTracker.ValidatorReward(validatorID, stakingFeature, claimingEpoch) } -func (o *SybilProtection) DelegatorReward(validatorID iotago.AccountID, delegatedAmount iotago.BaseToken, epochStart iotago.EpochIndex, epochEnd iotago.EpochIndex) (delegatorsReward iotago.Mana, actualEpochStart iotago.EpochIndex, actualEpochEnd iotago.EpochIndex, err error) { - return o.performanceTracker.DelegatorReward(validatorID, delegatedAmount, epochStart, epochEnd) +func (o *SybilProtection) DelegatorReward(validatorID iotago.AccountID, delegatedAmount iotago.BaseToken, epochStart iotago.EpochIndex, epochEnd iotago.EpochIndex, claimingEpoch iotago.EpochIndex) (delegatorReward iotago.Mana, firstRewardEpoch iotago.EpochIndex, lastRewardEpoch iotago.EpochIndex, err error) { + return o.performanceTracker.DelegatorReward(validatorID, delegatedAmount, epochStart, epochEnd, claimingEpoch) } func (o *SybilProtection) Import(reader io.ReadSeeker) error { diff --git a/pkg/testsuite/mock/wallet_transactions.go b/pkg/testsuite/mock/wallet_transactions.go index 911ba4622..62f489aad 100644 --- a/pkg/testsuite/mock/wallet_transactions.go +++ b/pkg/testsuite/mock/wallet_transactions.go @@ -453,17 +453,20 @@ func (w *Wallet) ClaimValidatorRewards(transactionName string, inputName string) panic(fmt.Sprintf("output with alias %s is not *iotago.AccountOutput", inputName)) } + apiForSlot := w.Node.Protocol.APIForSlot(w.currentSlot) + latestCommittedSlot := w.Node.Protocol.Chains.Main.Get().LatestCommitment.Get().Slot() + futureBoundedSlotIndex := latestCommittedSlot + apiForSlot.ProtocolParameters().MinCommittableAge() + claimingEpoch := apiForSlot.TimeProvider().EpochFromSlot(futureBoundedSlotIndex) + rewardMana, _, _, err := w.Node.Protocol.Engines.Main.Get().SybilProtection.ValidatorReward( inputAccount.AccountID, - inputAccount.FeatureSet().Staking().StakedAmount, - inputAccount.FeatureSet().Staking().StartEpoch, - inputAccount.FeatureSet().Staking().EndEpoch, + inputAccount.FeatureSet().Staking(), + claimingEpoch, ) if err != nil { panic(fmt.Sprintf("failed to calculate reward for output %s: %s", inputName, err)) } - apiForSlot := w.Node.Protocol.APIForSlot(w.currentSlot) potentialMana := w.PotentialMana(apiForSlot, input) storedMana := w.StoredMana(apiForSlot, input) @@ -541,12 +544,14 @@ func (w *Wallet) ClaimDelegatorRewards(transactionName string, inputName string) } apiForSlot := w.Node.Protocol.APIForSlot(w.currentSlot) + futureBoundedSlotIndex := w.currentSlot + apiForSlot.ProtocolParameters().MinCommittableAge() + claimingEpoch := apiForSlot.TimeProvider().EpochFromSlot(futureBoundedSlotIndex) + delegationEnd := inputDelegation.EndEpoch // If Delegation ID is zeroed, the output is in delegating state, which means its End Epoch is not set and we must use the // "last epoch" for the rewards calculation. if inputDelegation.DelegationID.Empty() { - futureBoundedSlotIndex := w.currentSlot + apiForSlot.ProtocolParameters().MinCommittableAge() - delegationEnd = apiForSlot.TimeProvider().EpochFromSlot(futureBoundedSlotIndex) - iotago.EpochIndex(1) + delegationEnd = claimingEpoch - iotago.EpochIndex(1) } rewardMana, _, _, err := w.Node.Protocol.Engines.Main.Get().SybilProtection.DelegatorReward( @@ -554,7 +559,9 @@ func (w *Wallet) ClaimDelegatorRewards(transactionName string, inputName string) inputDelegation.DelegatedAmount, inputDelegation.StartEpoch, delegationEnd, + claimingEpoch, ) + if err != nil { panic(fmt.Sprintf("failed to calculate reward for output %s: %s", inputName, err)) }