org.cardanofoundation.rewards.calculation.PoolRewardsCalculation Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cf-rewards-calculation Show documentation
Show all versions of cf-rewards-calculation Show documentation
This project aims to be a cardano reward calculation, java formula implementation and edge case
documentation
The newest version!
package org.cardanofoundation.rewards.calculation;
import org.cardanofoundation.rewards.calculation.config.NetworkConfig;
import org.cardanofoundation.rewards.calculation.domain.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;
import static org.cardanofoundation.rewards.calculation.util.BigNumberUtils.*;
import static org.cardanofoundation.rewards.calculation.util.BigNumberUtils.divide;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PoolRewardsCalculation {
/*
* https://github.com/input-output-hk/cardano-ledger/releases/latest/download/shelley-delegation.pdf
*
* Calculate the apparent pool performance with the formula (shelley-delegation.pdf page 36):
*
* performance = relativeBlocksCreatedInEpoch / relativeActiveStake
*
* hint: shelley-delegation.pdf 3.8.3
* As long as we have d >= 0.8, we set the apparent performance of any pool to 1
*
* See Haskell implementation: https://github.com/input-output-hk/cardano-ledger/blob/64459cc87094331c79d11880e0a4c81b9a721ab0/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rewards.hs#L87C32-L87C44
*/
public static BigDecimal calculateApparentPoolPerformance(final BigInteger activePoolStake, final BigInteger totalActiveEpochStake, final int blocksMintedByPool, final int blocksMintedByStakePools, final BigDecimal decentralizationParam) {
BigDecimal poolStake = new BigDecimal(activePoolStake);
BigDecimal totalEpochStake = new BigDecimal(totalActiveEpochStake);
if (decentralizationParam.compareTo(BigDecimal.valueOf(0.8)) >= 0) {
return BigDecimal.ONE;
} else if (isZero(poolStake) || isZero(totalEpochStake)) {
return BigDecimal.ZERO;
} else {
final BigDecimal relativeBlocksCreatedInEpoch = divide(blocksMintedByPool, blocksMintedByStakePools);
final BigDecimal relativeActiveStake = divide(poolStake, totalEpochStake);
return divide(relativeBlocksCreatedInEpoch, relativeActiveStake);
}
}
/*
* https://github.com/input-output-hk/cardano-ledger/releases/latest/download/shelley-delegation.pdf
*
* Calculate the pool reward with the formula (shelley-delegation.pdf 5.5.3):
*
* optimalPoolCount (nOpt "k") and influence (a0) are
* protocol parameters: https://cips.cardano.org/cips/cip9/
*
* sizeOfASaturatedPool = 1 / optimalPoolCount
* cappedRelativeStake = min(relativeStakeOfPool, sizeOfASaturatedPool)
* cappedRelativeStakeOfPoolOwner = min(relativeStakeOfPoolOwner, sizeOfASaturatedPool)
*
* rewards = (totalAvailableRewards / (1 + influence)) * (
* cappedRelativeStake +
* cappedRelativeStakeOfPoolOwner * influence * ((
* cappedRelativeStake - cappedRelativeStakeOfPoolOwner *
* (( sizeOfASaturatedPool - cappedRelativeStake ) / sizeOfASaturatedPool))
* / sizeOfASaturatedPool)
* )
*
* See the Haskell implementation: https://github.com/input-output-hk/cardano-ledger/blob/e722881568155fc39550a8dfabda3efeb263a1e5/shelley/chain-and-ledger/executable-spec/src/Shelley/Spec/Ledger/EpochBoundary.hs#L111
*/
public static BigInteger calculateOptimalPoolReward(BigInteger totalAvailableRewards, int optimalPoolCount, BigDecimal influence, BigDecimal relativeStakeOfPool, BigDecimal relativeStakeOfPoolOwner) {
BigDecimal sizeOfASaturatedPool = divide(BigDecimal.ONE, optimalPoolCount);
BigDecimal cappedRelativeStake = min(relativeStakeOfPool, sizeOfASaturatedPool);
BigDecimal cappedRelativeStakeOfPoolOwner = min(relativeStakeOfPoolOwner, sizeOfASaturatedPool);
// R / (1 + a0)
// "R are the total available rewards for the epoch (in ada)." (shelley-delegation.pdf 5.5.3)
BigDecimal rewardsDividedByOnePlusInfluence = divide(totalAvailableRewards, add(BigDecimal.ONE, influence));
// (z0 - sigma') / z0
BigDecimal relativeStakeOfSaturatedPool = divide(subtract(sizeOfASaturatedPool, cappedRelativeStake), sizeOfASaturatedPool);
// (sigma' - s' * relativeStakeOfSaturatedPool) / z0
BigDecimal saturatedPoolWeight = divide(subtract(cappedRelativeStake, multiply(cappedRelativeStakeOfPoolOwner, relativeStakeOfSaturatedPool)), sizeOfASaturatedPool);
// R / (1+a0) * (sigma' + s' * a0 * saturatedPoolWeight)
return floor(multiply(rewardsDividedByOnePlusInfluence,
add(cappedRelativeStake, multiply(cappedRelativeStakeOfPoolOwner, influence, saturatedPoolWeight))));
}
/*
* Calculate the pool reward with the formula (shelley-delegation.pdf 5.5.3 page 37 below):
* actualRewards = poolPerformance * optimalPoolReward
*/
public static BigInteger calculatePoolReward(BigInteger optimalPoolReward, BigDecimal poolPerformance) {
return floor(multiply(optimalPoolReward, poolPerformance));
}
/*
* This method calculates the pool operator reward regarding the formula described
* in the shelly-ledger.pdf p. 61, figure 47
*/
public static BigInteger calculateLeaderReward(BigInteger poolReward, double margin, BigInteger poolCost,
BigDecimal relativeOwnerStake, BigDecimal relativeStakeOfPool) {
if (isLowerOrEquals(poolReward, poolCost)) {
return poolReward;
}
return add(poolCost, floor(multiply(subtract(poolReward, poolCost),
add(margin, multiply((1 - margin), divide(relativeOwnerStake, relativeStakeOfPool))))));
}
/*
* This method calculates the pool member reward regarding the formula described
* in the shelly-ledger.pdf p. 61, figure 47
*
* See Haskell implementation: https://github.com/input-output-hk/cardano-ledger/blob/aed5dde9cd1096cfc2e255879cd617c0d64f8d9d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rewards.hs#L117
*/
public static BigInteger calculateMemberReward(BigInteger poolReward, double margin, BigInteger poolCost,
BigDecimal relativeMemberStake, BigDecimal relativeStakeOfPool) {
if (isLowerOrEquals(poolReward, poolCost)) {
return BigInteger.ZERO;
}
return floor(divide(multiply(
poolReward.subtract(poolCost),
subtract(BigDecimal.ONE, margin),
relativeMemberStake), relativeStakeOfPool));
}
public static PoolRewardCalculationResult calculatePoolRewardInEpoch(final String poolId, final PoolState poolStateCurrentEpoch,
final int totalBlocksInEpoch, final ProtocolParameters protocolParameters,
final BigInteger adaInCirculation, final BigInteger activeStakeInEpoch, BigInteger stakePoolRewardsPot,
final BigInteger totalActiveStakeOfOwners, final HashSet poolOwnerStakeAddresses,
final HashSet deregisteredAccounts, final boolean ignoreLeaderReward,
final HashSet lateDeregisteredAccounts,
final HashSet accountsRegisteredInThePast,
final NetworkConfig networkConfig) {
final int earnedEpoch = poolStateCurrentEpoch.getEpoch();
final PoolRewardCalculationResult poolRewardCalculationResult = PoolRewardCalculationResult.builder()
.epoch(earnedEpoch)
.poolId(poolId)
.poolReward(BigInteger.ZERO)
.distributedPoolReward(BigInteger.ZERO)
.unspendableEarnedRewards(BigInteger.ZERO)
.build();
/*
babbage-ledger.pdf | 6 Forgo Reward Calculation Prefilter | p. 14
The reward calculation no longer filters out the unregistered stake credentials when creating
a reward update. As in the Shelley era, though, they are still filtered on the epoch boundary
when the reward update is applied
*/
if (earnedEpoch >= networkConfig.getVasilHardforkEpoch()) {
lateDeregisteredAccounts.addAll(deregisteredAccounts);
deregisteredAccounts.clear();
}
final BigInteger poolStake = poolStateCurrentEpoch.getActiveStake();
final BigInteger poolPledge = poolStateCurrentEpoch.getPledge();
final double poolMargin = poolStateCurrentEpoch.getMargin();
final BigInteger poolFixedCost = poolStateCurrentEpoch.getFixedCost();
final int blocksPoolHasMinted = poolStateCurrentEpoch.getBlockCount();
poolRewardCalculationResult.setPoolMargin(poolMargin);
poolRewardCalculationResult.setPoolCost(poolFixedCost);
poolRewardCalculationResult.setRewardAddress(poolStateCurrentEpoch.getRewardAddress());
if (blocksPoolHasMinted == 0) {
return poolRewardCalculationResult;
}
BigDecimal decentralizationParameter = protocolParameters.getDecentralisation();
int optimalPoolCount = protocolParameters.getOptimalPoolCount();
BigDecimal influenceParam = protocolParameters.getPoolOwnerInfluence();
// Calculate apparent pool performance
final BigDecimal apparentPoolPerformance =
PoolRewardsCalculation.calculateApparentPoolPerformance(poolStake, activeStakeInEpoch,
blocksPoolHasMinted, totalBlocksInEpoch, decentralizationParameter);
poolRewardCalculationResult.setApparentPoolPerformance(apparentPoolPerformance);
// shelley-delegation.pdf 5.5.3
// "[...]the relative stake of the pool owner(s) (the amount of ada
// pledged during pool registration)"
poolRewardCalculationResult.setPoolOwnerStakeAddresses(poolOwnerStakeAddresses);
if (isLower(totalActiveStakeOfOwners, poolPledge)) {
return poolRewardCalculationResult;
}
final BigDecimal relativeStakeOfPoolOwner = divide(poolPledge, adaInCirculation);
final BigDecimal relativePoolStake = divide(poolStake, adaInCirculation);
// Step 8: Calculate optimal pool reward
final BigInteger optimalPoolReward =
PoolRewardsCalculation.calculateOptimalPoolReward(
stakePoolRewardsPot,
optimalPoolCount,
influenceParam,
relativePoolStake,
relativeStakeOfPoolOwner);
poolRewardCalculationResult.setOptimalPoolReward(optimalPoolReward);
// Step 9: Calculate pool reward as optimal pool reward * apparent pool performance
final BigInteger poolReward = PoolRewardsCalculation.calculatePoolReward(optimalPoolReward, apparentPoolPerformance);
poolRewardCalculationResult.setPoolReward(poolReward);
// Step 10: Calculate pool operator reward
BigInteger poolOperatorReward = PoolRewardsCalculation.calculateLeaderReward(poolReward, poolMargin, poolFixedCost,
divide(totalActiveStakeOfOwners, adaInCirculation), relativePoolStake);
BigInteger unspendableEarnedRewards = BigInteger.ZERO;
String rewardAddress = poolRewardCalculationResult.getRewardAddress();
if (!accountsRegisteredInThePast.contains(rewardAddress)) {
log.info(poolRewardCalculationResult.getRewardAddress() + " has never been registered. Operator would have received " + poolOperatorReward + " but will not receive any rewards.");
if (earnedEpoch >= networkConfig.getVasilHardforkEpoch()) {
unspendableEarnedRewards = poolOperatorReward;
}
poolOperatorReward = BigInteger.ZERO;
} else if (deregisteredAccounts.contains(rewardAddress)) {
log.info(poolRewardCalculationResult.getRewardAddress() + " has been deregistered. Operator would have received " + poolOperatorReward + " but will not receive any rewards.");
poolOperatorReward = BigInteger.ZERO;
} else if (lateDeregisteredAccounts.contains(rewardAddress)) {
log.info("[unregRU]: " + poolRewardCalculationResult.getRewardAddress() + " has been deregistered lately. Operator would have received " + poolOperatorReward + " but will not receive any rewards.");
unspendableEarnedRewards = poolOperatorReward;
poolOperatorReward = BigInteger.ZERO;
}
if (ignoreLeaderReward) {
poolOperatorReward = BigInteger.ZERO;
log.debug("[reward address of multiple pools] Pool " + poolId + " has been ignored. Operator would have received " + poolOperatorReward + " but will not receive any rewards.");
}
// Step 11: Calculate pool member reward
BigInteger poolMemberRewards = BigInteger.ZERO;
final HashSet memberRewards = new HashSet<>();
for (Delegator delegator : poolStateCurrentEpoch.getDelegators()) {
final String stakeAddress = delegator.getStakeAddress();
/*
"[...] the value of rewards in the reward function should be computed using an aggregating
union so that leader rewards from multiple sources are aggregated.
This was corrected at the Allegra hard fork"
shelley-ledger.pdf | 17.4 Reward aggregation | p. 114
*/
if (stakeAddress.equals(poolStateCurrentEpoch.getRewardAddress())
&& earnedEpoch < networkConfig.getAllegraHardforkEpoch()) {
continue;
}
if (poolOwnerStakeAddresses.contains(stakeAddress)) {
continue;
}
BigInteger memberReward = PoolRewardsCalculation.calculateMemberReward(poolReward, poolMargin,
poolFixedCost, divide(delegator.getActiveStake(), adaInCirculation), relativePoolStake);
if (deregisteredAccounts.contains(stakeAddress)) {
log.debug("Delegator " + stakeAddress + " has been deregistered. Delegator would have received " + memberReward + " but will not receive any rewards.");
memberReward = BigInteger.ZERO;
} else if (lateDeregisteredAccounts.contains(stakeAddress)) {
log.debug("[unregRU]: " + stakeAddress + " has been deregistered lately. Delegator would have received " + memberReward + " but will not receive any rewards.");
unspendableEarnedRewards = unspendableEarnedRewards.add(memberReward);
memberReward = BigInteger.ZERO;
}
memberRewards.add(Reward.builder()
.amount(memberReward)
.stakeAddress(stakeAddress)
.build());
poolMemberRewards = poolMemberRewards.add(memberReward);
}
poolRewardCalculationResult.setDistributedPoolReward(poolOperatorReward.add(poolMemberRewards));
poolRewardCalculationResult.setOperatorReward(poolOperatorReward);
poolRewardCalculationResult.setMemberRewards(memberRewards);
poolRewardCalculationResult.setUnspendableEarnedRewards(unspendableEarnedRewards);
return poolRewardCalculationResult;
}
}