Skip to content

Commit

Permalink
Merge pull request #621 from iotaledger/fix-validator-reward
Browse files Browse the repository at this point in the history
Fix decay not being applied until "current epoch" in reward calculation
  • Loading branch information
PhilippGackstatter authored Dec 8, 2023
2 parents a377997 + 960c535 commit 2ab4636
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 64 deletions.
27 changes: 16 additions & 11 deletions components/restapi/core/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -198,42 +198,47 @@ 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 {
return nil, ierrors.Wrapf(echo.ErrInternalServerError, "failed to calculate reward for output %s: %s", outputID.ToHex(), err)
}

return &api.ManaRewardsResponse{
StartEpoch: actualStart,
EndEpoch: actualEnd,
StartEpoch: firstRewardEpoch,
EndEpoch: lastRewardEpoch,
Rewards: reward,
}, nil
}
Expand Down
18 changes: 12 additions & 6 deletions pkg/protocol/engine/ledger/ledger/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
16 changes: 11 additions & 5 deletions pkg/protocol/sybilprotection/sybilprotection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,30 @@ 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)
}

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
Expand All @@ -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
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -106,30 +109,33 @@ 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)
}

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
Expand Down Expand Up @@ -166,21 +172,21 @@ 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)
}

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) {
Expand Down
Loading

0 comments on commit 2ab4636

Please sign in to comment.