Skip to content

Commit

Permalink
feat: add epoch calculation and refactor validation class
Browse files Browse the repository at this point in the history
  • Loading branch information
fabianbormann committed Feb 23, 2024
1 parent fdf116f commit 9fcadab
Show file tree
Hide file tree
Showing 18 changed files with 235 additions and 84 deletions.
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
package org.cardanofoundation.rewards.validation;
package org.cardanofoundation.rewards.calculation;

import org.cardanofoundation.rewards.calculation.PoolRewardsCalculation;
import org.cardanofoundation.rewards.calculation.domain.*;
import org.cardanofoundation.rewards.validation.data.provider.DataProvider;
import org.cardanofoundation.rewards.validation.entity.jpa.projection.LatestStakeAccountUpdate;
import org.cardanofoundation.rewards.validation.entity.jpa.projection.TotalPoolRewards;
import org.cardanofoundation.rewards.calculation.enums.MirPot;

import java.math.BigInteger;
import java.util.*;
import java.util.ArrayList;
import java.util.List;

import static org.cardanofoundation.rewards.calculation.PoolRewardsCalculation.calculatePoolRewardInEpoch;
import static org.cardanofoundation.rewards.calculation.constants.RewardConstants.TOTAL_LOVELACE;
import static org.cardanofoundation.rewards.calculation.util.BigNumberUtils.*;
import static org.cardanofoundation.rewards.calculation.util.CurrencyConverter.lovelaceToAda;

public class EpochComputation {

public static EpochCalculationResult calculateEpochPots(int epoch, DataProvider dataProvider) {
long epochCalculationStart = System.currentTimeMillis();
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class EpochCalculation {

public static EpochCalculationResult calculateEpochRewardPots(int epoch, AdaPots adaPotsForPreviousEpoch,
ProtocolParameters protocolParameters, Epoch epochInfo,
List<PoolDeregistration> retiredPools,
List<AccountUpdate> accountUpdates,
List<MirCertificate> mirCertificates,
List<String> poolsThatProducedBlocksInEpoch,
List<PoolHistory> poolHistories,
List<String> sharedPoolRewardAddressesWithoutReward) {
EpochCalculationResult epochCalculationResult = EpochCalculationResult.builder().epoch(epoch).build();

double treasuryGrowthRate = 0.2;
double monetaryExpandRate = 0.003;
double decentralizationParameter = 1;

AdaPots adaPotsForPreviousEpoch = dataProvider.getAdaPotsForEpoch(epoch - 1);

BigInteger totalFeesForCurrentEpoch = BigInteger.ZERO;
int totalBlocksInEpoch = 0;

ProtocolParameters protocolParameters = dataProvider.getProtocolParametersForEpoch(epoch - 2);
Epoch epochInfo = dataProvider.getEpochInfo(epoch - 2);

/* We need to use the epoch info 2 epochs before as shelley starts in epoch 208 it will be possible to get
those values from epoch 210 onwards. Before that we need to use the genesis values, but they are not
needed anyway if the decentralization parameter is > 0.8.
Expand All @@ -52,7 +53,7 @@ public static EpochCalculationResult calculateEpochPots(int epoch, DataProvider
BigInteger reserveInPreviousEpoch = adaPotsForPreviousEpoch.getReserves();
BigInteger treasuryInPreviousEpoch = adaPotsForPreviousEpoch.getTreasury();

BigInteger rewardPot = TreasuryComputation.calculateTotalRewardPotWithEta(
BigInteger rewardPot = TreasuryCalculation.calculateTotalRewardPotWithEta(
monetaryExpandRate, totalBlocksInEpoch, decentralizationParameter, reserveInPreviousEpoch, totalFeesForCurrentEpoch);

BigInteger treasuryCut = multiplyAndFloor(rewardPot, treasuryGrowthRate);
Expand All @@ -61,54 +62,67 @@ public static EpochCalculationResult calculateEpochPots(int epoch, DataProvider

// The sum of all the refunds attached to unregistered reward accounts are added to the
// treasury (see: Pool Reap Transition, p.53, figure 40, shely-ledger.pdf)
List<PoolDeregistration> retiredPools = dataProvider.getRetiredPoolsInEpoch(epoch);

if (retiredPools.size() > 0) {
// The deposit will pay back one epoch later
List<AccountUpdate> accountUpdates = dataProvider.getAccountUpdatesUntilEpoch(
retiredPools.stream().map(PoolDeregistration::getRewardAddress).toList(), epoch - 1);
List<String> rewardAddressesOfRetiredPools = retiredPools.stream().map(PoolDeregistration::getRewardAddress).toList();
List<AccountUpdate> latestAccountUpdates = accountUpdates.stream()
.filter(update -> rewardAddressesOfRetiredPools.contains(update.getStakeAddress())).toList();

treasuryForCurrentEpoch = treasuryForCurrentEpoch.add(
TreasuryComputation.calculateUnclaimedRefundsForRetiredPools(retiredPools, accountUpdates));
TreasuryCalculation.calculateUnclaimedRefundsForRetiredPools(retiredPools, latestAccountUpdates));
}
// Check if there was a MIR Certificate in the previous epoch
BigInteger treasuryWithdrawals = BigInteger.ZERO;
List<MirCertificate> mirCertificates = dataProvider.getMirCertificatesInEpoch(epoch - 1);
for (MirCertificate mirCertificate : mirCertificates) {
if (mirCertificate.getPot() == MirPot.TREASURY) {
treasuryWithdrawals = treasuryWithdrawals.add(mirCertificate.getTotalRewards());
}
}
treasuryForCurrentEpoch = treasuryForCurrentEpoch.subtract(treasuryWithdrawals);

List<String> poolIds = dataProvider.getPoolsThatProducedBlocksInEpoch(epoch - 2);
BigInteger totalDistributedRewards = BigInteger.ZERO;

BigInteger adaInCirculation = TOTAL_LOVELACE.subtract(reserveInPreviousEpoch);
List<PoolRewardCalculationResult> PoolRewardCalculationResults = new ArrayList<>();

int processedPools = 0;
long start = System.currentTimeMillis();
List<PoolHistory> poolHistories = dataProvider.getHistoryOfAllPoolsInEpoch(epoch - 2);
List<AccountUpdate> accountUpdates = dataProvider.getLatestStakeAccountUpdates(epoch - 1);

// Member and total rewards are used in the validation part only
List<Reward> memberRewardsInEpoch = dataProvider.getMemberRewardsInEpoch(epoch - 2);
List<TotalPoolRewards> totalPoolRewards = dataProvider.getSumOfMemberAndLeaderRewardsInEpoch(epoch - 2);

long end = System.currentTimeMillis();
System.out.println("Pool and account data fetched in " + (end - start) + "ms");
BigInteger unspendableEarnedRewards = BigInteger.ZERO;

List<String> sharedPoolRewardAddressesWithoutReward = dataProvider.findSharedPoolRewardAddressWithoutReward(epoch - 2);

for (String poolId : poolIds) {
System.out.println("[" + processedPools + "/" + poolIds.size() + "] Processing pool: " + poolId);
for (String poolId : poolsThatProducedBlocksInEpoch) {
log.info("[" + processedPools + "/" + poolsThatProducedBlocksInEpoch.size() + "] Processing pool: " + poolId);
PoolHistory poolHistory = poolHistories.stream().filter(history -> history.getPoolId().equals(poolId)).findFirst().orElse(null);
PoolRewardCalculationResult poolRewardCalculationResult = PoolRewardComputation.computePoolRewardInEpoch(poolId, epoch - 2, protocolParameters, epochInfo, stakePoolRewardsPot, adaInCirculation, poolHistory, accountUpdates, sharedPoolRewardAddressesWithoutReward);

if (!PoolRewardComputation.poolRewardIsValid(poolRewardCalculationResult, memberRewardsInEpoch, totalPoolRewards)) {
System.out.println("Pool reward is invalid. Please check the details for pool " + poolId);
PoolRewardCalculationResult poolRewardCalculationResult = PoolRewardCalculationResult
.builder().poolId(poolId).epoch(epoch).poolReward(BigInteger.ZERO).build();

if(poolHistory != null) {
BigInteger activeStakeInEpoch = BigInteger.ZERO;
if (epochInfo.getActiveStake() != null) {
activeStakeInEpoch = epochInfo.getActiveStake();
}

if (epoch > 212 && epoch < 255) {
totalBlocksInEpoch = epochInfo.getNonOBFTBlockCount();
}

// Step 10 a: Check if pool reward address or member stake addresses have been unregistered before
List<String> stakeAddresses = new ArrayList<>();
stakeAddresses.add(poolHistory.getRewardAddress());
stakeAddresses.addAll(poolHistory.getDelegators().stream().map(Delegator::getStakeAddress).toList());

List<AccountUpdate> latestAccountUpdates = accountUpdates.stream()
.filter(update -> stakeAddresses.contains(update.getStakeAddress())).toList();

// There was a different behavior in the previous version of the node
// If a pool reward address had been used for multiple pools,
// the stake account only received the reward for one of those pools
// This is not the case anymore and the stake account receives the reward for all pools
// Until the Allegra hard fork, this method will be used to emulate the old behavior
boolean ignoreLeaderReward = sharedPoolRewardAddressesWithoutReward.contains(poolId);

poolRewardCalculationResult = calculatePoolRewardInEpoch(poolId, poolHistory,
totalBlocksInEpoch, protocolParameters,
adaInCirculation, activeStakeInEpoch, stakePoolRewardsPot,
poolHistory.getOwnerActiveStake(), poolHistory.getOwners(),
latestAccountUpdates, ignoreLeaderReward);
}

PoolRewardCalculationResults.add(poolRewardCalculationResult);
Expand All @@ -122,7 +136,7 @@ public static EpochCalculationResult calculateEpochPots(int epoch, DataProvider
calculatedReserve = add(calculatedReserve, undistributedRewards);
calculatedReserve = subtract(calculatedReserve, unspendableEarnedRewards);

System.out.println("Unspendable earned rewards: " + lovelaceToAda(unspendableEarnedRewards.intValue()) + " ADA");
log.info("Unspendable earned rewards: " + lovelaceToAda(unspendableEarnedRewards.intValue()) + " ADA");
treasuryForCurrentEpoch = add(treasuryForCurrentEpoch, unspendableEarnedRewards);

epochCalculationResult.setTotalDistributedRewards(totalDistributedRewards);
Expand All @@ -134,10 +148,6 @@ public static EpochCalculationResult calculateEpochPots(int epoch, DataProvider
epochCalculationResult.setTotalAdaInCirculation(adaInCirculation);
epochCalculationResult.setTotalUndistributedRewards(undistributedRewards);

long epochCalculationEnd = System.currentTimeMillis();
System.out.println("Epoch calculation took " +
Math.round((epochCalculationEnd - epochCalculationStart) / 1000.0) + " seconds in total.");

return epochCalculationResult;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
import java.math.BigInteger;
import java.util.*;

import static org.cardanofoundation.rewards.calculation.constants.RewardConstants.RANDOMNESS_STABILISATION_WINDOW;
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 {

/*
Expand Down Expand Up @@ -198,21 +202,20 @@ public static PoolRewardCalculationResult calculatePoolRewardInEpoch(String pool
BigInteger unspendableEarnedRewards = BigInteger.ZERO;

if (!memberWithUpdates.contains(poolRewardCalculationResult.getRewardAddress()) || memberWithDeregisteredStakeAddresses.contains(poolRewardCalculationResult.getRewardAddress())) {
System.out.println("Pool " + poolId + " has been deregistered. Operator would have received " + poolOperatorReward + " but will not receive any rewards.");
AccountUpdate latestStakeAccountUpdate = accountUpdates.stream().filter(accountUpdate -> accountUpdate.getStakeAddress().equals(poolRewardCalculationResult.getRewardAddress())).findFirst().orElse(null);

if (latestStakeAccountUpdate != null &&
latestStakeAccountUpdate.getEpoch() == poolHistoryCurrentEpoch.getEpoch() + 1 &&
latestStakeAccountUpdate.getEpochSlot() > 212500) {
System.out.println("[unregRU]: " + poolRewardCalculationResult.getRewardAddress() + " has been deregistered. Operator would have received " + poolOperatorReward + " but will not receive any rewards.");
latestStakeAccountUpdate.getEpochSlot() > RANDOMNESS_STABILISATION_WINDOW) {
log.info("[unregRU]: " + poolRewardCalculationResult.getRewardAddress() + " has been deregistered. Operator would have received " + poolOperatorReward + " but will not receive any rewards.");
unspendableEarnedRewards = poolOperatorReward;
}
poolOperatorReward = BigInteger.ZERO;
}

if (ignoreLeaderReward) {
poolOperatorReward = BigInteger.ZERO;
System.out.println("[reward address of multiple pools] Pool " + poolId + " has been ignored. Operator would have received " + poolOperatorReward + " but will not receive any rewards.");
log.info("[reward address of multiple pools] Pool " + poolId + " has been ignored. Operator would have received " + poolOperatorReward + " but will not receive any rewards.");
}

poolRewardCalculationResult.setOperatorReward(poolOperatorReward);
Expand All @@ -229,14 +232,14 @@ public static PoolRewardCalculationResult calculatePoolRewardInEpoch(String pool
poolFixedCost, divide(delegator.getActiveStake(), adaInCirculation), relativePoolStake);

if (!memberWithUpdates.contains(delegator.getStakeAddress()) || memberWithDeregisteredStakeAddresses.contains(delegator.getStakeAddress())) {
System.out.println("Delegator " + delegator.getStakeAddress() + " has been deregistered. Delegator would have received " + memberReward + " but will not receive any rewards.");
log.info("Delegator " + delegator.getStakeAddress() + " has been deregistered. Delegator would have received " + memberReward + " but will not receive any rewards.");

AccountUpdate latestStakeAccountUpdate = accountUpdates.stream().filter(accountUpdate -> accountUpdate.getStakeAddress().equals(delegator.getStakeAddress())).findFirst().orElse(null);

if (latestStakeAccountUpdate != null &&
latestStakeAccountUpdate.getEpoch() == poolHistoryCurrentEpoch.getEpoch() + 1 &&
latestStakeAccountUpdate.getEpochSlot() > 212500) {
System.out.println("[unregRU]: " + delegator.getStakeAddress() + " has been deregistered. Operator would have received " + memberReward + " but will not receive any rewards.");
log.info("[unregRU]: " + delegator.getStakeAddress() + " has been deregistered. Operator would have received " + memberReward + " but will not receive any rewards.");
unspendableEarnedRewards = unspendableEarnedRewards.add(memberReward);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.cardanofoundation.rewards.calculation;

import org.cardanofoundation.rewards.calculation.domain.*;
import org.cardanofoundation.rewards.calculation.enums.AccountUpdateAction;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

import static org.cardanofoundation.rewards.calculation.constants.RewardConstants.EXPECTED_SLOT_PER_EPOCH;
import static org.cardanofoundation.rewards.calculation.constants.RewardConstants.POOL_DEPOSIT_IN_LOVELACE;
import static org.cardanofoundation.rewards.calculation.util.BigNumberUtils.multiplyAndFloor;

public class TreasuryCalculation {

/*
* Calculate the reward pot for epoch e with the formula:
*
* rewards(e) = floor(monetary_expand_rate * eta * reserve(e - 1) + fee(e - 1))
* rewards(e) = 0, if e < 209
*/
public static BigInteger calculateTotalRewardPotWithEta(double monetaryExpandRate, int totalBlocksInEpochByPools,
double decentralizationParameter, BigInteger reserve, BigInteger fee) {
double eta = calculateEta(totalBlocksInEpochByPools, decentralizationParameter);
return multiplyAndFloor(reserve, monetaryExpandRate, eta).add(fee);
}

/*
* Calculate eta using the decentralisation parameter and the formula:
*
* eta(totalBlocksInEpochMadeByPools, decentralisation) = 1, if decentralisation >= 0.8, otherwise
* eta(totalBlocksInEpochMadeByPools, decentralisation) =
* min(1, totalBlocksInEpochMadeByPools / ((1 - decentralisation) * expectedSlotPerEpoch * activeSlotsCoeff))
*
* See: https://github.com/input-output-hk/cardano-ledger/commit/c4f10d286faadcec9e4437411bce9c6c3b6e51c2
*/
private static double calculateEta(int totalBlocksInEpochByPools, double decentralizationParameter) {
// shelley-delegation.pdf 5.4.3

if (decentralizationParameter >= 0.8) {
return 1.0;
}

// The number of expected blocks will be the number of slots per epoch times the active slots coefficient
double activeSlotsCoeff = 0.05; // See: Non-Updatable Parameters: https://cips.cardano.org/cips/cip9/

// decentralizationParameter is the proportion of blocks that are expected to be produced by stake pools
// instead of the OBFT (Ouroboros Byzantine Fault Tolerance) nodes. It was introduced close before the Shelley era:
// https://github.com/input-output-hk/cardano-ledger/commit/c4f10d286faadcec9e4437411bce9c6c3b6e51c2
double expectedBlocksInNonOBFTSlots = EXPECTED_SLOT_PER_EPOCH * activeSlotsCoeff * (1 - decentralizationParameter);

// eta is the ratio between the number of blocks that have been produced during the epoch, and
// the expectation value of blocks that should have been produced during the epoch under
// ideal conditions.
return Math.min(1, totalBlocksInEpochByPools / expectedBlocksInNonOBFTSlots);
}

/*
"For each retiring pool, the refund for the pool registration deposit is added to the
pool's registered reward account, provided the reward account is still registered." -
https://github.com/input-output-hk/cardano-ledger/blob/9e2f8151e3b9a0dde9faeb29a7dd2456e854427c/eras/shelley/formal-spec/epoch.tex#L546C9-L547C87
*/
public static BigInteger calculateUnclaimedRefundsForRetiredPools(List<PoolDeregistration> retiredPools,
List<AccountUpdate> latestAccountUpdates) {
BigInteger refunds = BigInteger.ZERO;

if (retiredPools.size() > 0) {
for (AccountUpdate lastAccountUpdate : latestAccountUpdates) {
if (lastAccountUpdate.getAction() == AccountUpdateAction.DEREGISTRATION) {
refunds = refunds.add(POOL_DEPOSIT_IN_LOVELACE);
}
}
}

return refunds;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ public class RewardConstants {
public static final BigInteger POOL_DEPOSIT_IN_LOVELACE = BigInteger.valueOf(500000000);
// https://developers.cardano.org/docs/operate-a-stake-pool/introduction-to-cardano/#slots-and-epochs
public static final int EXPECTED_SLOT_PER_EPOCH = 432000;
public static final int GENESIS_CONFIG_SECURITY_PARAMETER = 2160;
public static final double ACTIVE_SLOT_COEFFICIENT = 0.05;
public static final long RANDOMNESS_STABILISATION_WINDOW = Math.round(
(4 * GENESIS_CONFIG_SECURITY_PARAMETER) / ACTIVE_SLOT_COEFFICIENT);
}

0 comments on commit 9fcadab

Please sign in to comment.