All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cardanofoundation.rewards.calculation.EpochCalculation Maven / Gradle / Ivy

Go to download

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 org.cardanofoundation.rewards.calculation.enums.MirPot;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static org.cardanofoundation.rewards.calculation.PoolRewardsCalculation.calculatePoolRewardInEpoch;
import static org.cardanofoundation.rewards.calculation.util.BigNumberUtils.*;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class EpochCalculation {

    public static EpochCalculationResult calculateEpochRewardPots(final int epoch,
                                                                  final BigInteger reserveInPreviousEpoch,
                                                                  final BigInteger treasuryInPreviousEpoch,
                                                                  final ProtocolParameters protocolParameters, final Epoch epochInfo,
                                                                  final Set retiredPools,
                                                                  final HashSet deregisteredAccounts,
                                                                  final List mirCertificates,
                                                                  final List poolsThatProducedBlocksInEpoch,
                                                                  final List poolHistories,
                                                                  final HashSet lateDeregisteredAccounts,
                                                                  final HashSet registeredAccountsSinceLastEpoch,
                                                                  final HashSet registeredAccountsUntilNow,
                                                                  final HashSet sharedPoolRewardAddressesWithoutReward,
                                                                  final HashSet deregisteredAccountsOnEpochBoundary,
                                                                  final NetworkConfig networkConfig) {
        final EpochCalculationResult epochCalculationResult = EpochCalculationResult.builder().epoch(epoch).build();

        if (epoch < networkConfig.getShelleyStartEpoch()) {
            log.warn("Epoch " + epoch + " is before the start of the Shelley era. No rewards were calculated in this epoch.");
            epochCalculationResult.setReserves(BigInteger.ZERO);
            epochCalculationResult.setTreasury(BigInteger.ZERO);
            epochCalculationResult.setTotalDistributedRewards(BigInteger.ZERO);
            epochCalculationResult.setTotalRewardsPot(BigInteger.ZERO);
            epochCalculationResult.setTotalPoolRewardsPot(BigInteger.ZERO);
            epochCalculationResult.setTotalAdaInCirculation(BigInteger.ZERO);
            return epochCalculationResult;
        } else if (epoch == networkConfig.getShelleyStartEpoch()) {
            epochCalculationResult.setReserves(networkConfig.getShelleyInitialReserves());
            epochCalculationResult.setTreasury(networkConfig.getShelleyInitialTreasury());
            epochCalculationResult.setTotalDistributedRewards(BigInteger.ZERO);
            epochCalculationResult.setTotalRewardsPot(BigInteger.ZERO);
            epochCalculationResult.setTotalPoolRewardsPot(BigInteger.ZERO);
            epochCalculationResult.setTotalAdaInCirculation(networkConfig.getShelleyInitialUtxo());
            return epochCalculationResult;
        }

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

        BigDecimal treasuryGrowthRate = protocolParameters.getTreasuryGrowRate();
        BigDecimal monetaryExpandRate = protocolParameters.getMonetaryExpandRate();
        BigDecimal decentralizationParameter = protocolParameters.getDecentralisation();
        BigInteger activeStakeInEpoch = BigInteger.ZERO;

        if (epochInfo != null) {
            activeStakeInEpoch = epochInfo.getActiveStake();
            totalFeesForCurrentEpoch = epochInfo.getFees();
            totalBlocksInEpoch = epochInfo.getBlockCount();
            if (isLower(decentralizationParameter, BigDecimal.valueOf(0.8)) && isHigher(decentralizationParameter, BigDecimal.ZERO)) {
                totalBlocksInEpoch = epochInfo.getNonOBFTBlockCount();
            }
        }

        final int blocksInEpoch = totalBlocksInEpoch;
        final BigInteger rewardPot = TreasuryCalculation.calculateTotalRewardPotWithEta(
                monetaryExpandRate, totalBlocksInEpoch, decentralizationParameter, reserveInPreviousEpoch, totalFeesForCurrentEpoch, networkConfig);

        final BigInteger treasuryCut = multiplyAndFloor(rewardPot, treasuryGrowthRate);
        BigInteger treasuryForCurrentEpoch = treasuryInPreviousEpoch.add(treasuryCut);
        final BigInteger stakePoolRewardsPot = rewardPot.subtract(treasuryCut);

        // The sum of all the refunds attached to unregistered reward accounts are added to the
        // treasury (see: Pool Reap Transition, p.53, figure 40, shelley-ledger.pdf)
        BigInteger unclaimedRefunds = BigInteger.ZERO;
        var rewardAddressesOfRetiredPools = retiredPools.stream()
                .map(retiredPool -> retiredPool.getRewardAddress()).collect(Collectors.toSet());

        if (rewardAddressesOfRetiredPools.size() > 0) {
            List deregisteredOwnerAccounts = deregisteredAccountsOnEpochBoundary.stream()
                    .filter(rewardAddressesOfRetiredPools::contains).toList();
            List ownerAccountsRegisteredInThePast = registeredAccountsUntilNow.stream()
                    .filter(rewardAddressesOfRetiredPools::contains).toList();

            /* Check if the reward address of the retired pool has been unregistered before
               or if the reward address has been unregistered after the randomness stabilization window
               or if the reward address has not been registered at all */
            for (var retiredPool : retiredPools) {
                if (deregisteredOwnerAccounts.contains(retiredPool.getRewardAddress()) ||
                        !ownerAccountsRegisteredInThePast.contains(retiredPool.getRewardAddress())) {
                    // If the reward address has been unregistered, the deposit can not be returned
                    // and will be added to the treasury instead (Pool Reap see: shelley-ledger.pdf p.53)
                    treasuryForCurrentEpoch = treasuryForCurrentEpoch.add(retiredPool.getDepositAmount());
                    unclaimedRefunds = unclaimedRefunds.add(retiredPool.getDepositAmount());
                }
            }
        }
        // Check if there was a MIR Certificate in the previous epoch
        BigInteger treasuryWithdrawals = BigInteger.ZERO;
        BigInteger calculatedReserve = subtract(reserveInPreviousEpoch, subtract(rewardPot, totalFeesForCurrentEpoch));

        for (MirCertificate mirCertificate : mirCertificates) {
            if (mirCertificate.getPot() == MirPot.TREASURY) {
                treasuryWithdrawals = treasuryWithdrawals.add(mirCertificate.getTotalRewards());
            } else if (mirCertificate.getPot() == MirPot.RESERVES) {
                calculatedReserve = calculatedReserve.subtract(mirCertificate.getTotalRewards());
            }
        }

        treasuryForCurrentEpoch = treasuryForCurrentEpoch.subtract(treasuryWithdrawals);
        BigInteger totalDistributedRewards = BigInteger.ZERO;
        final BigInteger adaInCirculation = networkConfig.getTotalLovelace().subtract(reserveInPreviousEpoch);
        final List poolRewardCalculationResults = new ArrayList<>();
        BigInteger unspendableEarnedRewards = BigInteger.ZERO;

        int i = 1;
        for (String poolId : poolsThatProducedBlocksInEpoch) {
            log.debug("[" + i + " / " + poolsThatProducedBlocksInEpoch.size() + "] Processing pool: " + poolId);
            PoolState poolState = poolHistories.stream().filter(history -> history.getPoolId().equals(poolId)).findFirst().orElse(null);
            PoolRewardCalculationResult poolRewardCalculationResult = PoolRewardCalculationResult
                    .builder().poolId(poolId).epoch(epoch).poolReward(BigInteger.ZERO).build();

            if(poolState != null) {
                // Get the reward addresses of the pool and the reward addresses of its delegators
                final HashSet stakeAddresses = new HashSet<>();
                stakeAddresses.add(poolState.getRewardAddress());
                stakeAddresses.addAll(poolState.getDelegators().stream().map(Delegator::getStakeAddress).toList());
                // We need the get the registration state of those accounts. If they were unregistered before
                // the randomness stabilization window, they will not receive any rewards. The remaining of the
                // reward pot will go back to the reserves
                final HashSet delegatorAccountDeregistrations = deregisteredAccounts.stream()
                        .filter(stakeAddresses::contains).collect(Collectors.toCollection(HashSet::new));
                // If they were unregistered after the randomness stabilization window, rewards will be calculated
                // but they will not be spendable and will be added to the treasury instead
                final HashSet lateDeregisteredDelegators = lateDeregisteredAccounts.stream()
                        .filter(stakeAddresses::contains).collect(Collectors.toCollection(HashSet::new));

                // 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 = false;
                if (epoch - 2 < networkConfig.getAllegraHardforkEpoch()) {
                    ignoreLeaderReward = sharedPoolRewardAddressesWithoutReward.contains(poolId);
                }

                poolRewardCalculationResult = calculatePoolRewardInEpoch(poolId, poolState,
                        blocksInEpoch, protocolParameters,
                        adaInCirculation, activeStakeInEpoch, stakePoolRewardsPot,
                        poolState.getOwnerActiveStake(), poolState.getOwners(),
                        delegatorAccountDeregistrations, ignoreLeaderReward, lateDeregisteredDelegators, registeredAccountsSinceLastEpoch, networkConfig);
            }

            totalDistributedRewards = add(totalDistributedRewards, poolRewardCalculationResult.getDistributedPoolReward());
            unspendableEarnedRewards = unspendableEarnedRewards.add(poolRewardCalculationResult.getUnspendableEarnedRewards());
            poolRewardCalculationResults.add(poolRewardCalculationResult);
            i++;
        }

        BigInteger undistributedRewards = subtract(stakePoolRewardsPot, totalDistributedRewards);
        calculatedReserve = add(calculatedReserve, undistributedRewards);
        calculatedReserve = subtract(calculatedReserve, unspendableEarnedRewards);

        if (epoch == networkConfig.getAllegraHardforkEpoch()) {
            /*
                "The bootstrap addresses from Figure 6 were not intended to include the Byron era redeem
                addresses (those with addrtype 2, see the Byron CDDL spec). These addresses were, however,
                not spendable in the Shelley era. At the Allegra hard fork they were removed from the UTxO
                and the Ada contained in them was returned to the reserves."
                    - shelley-spec-ledger.pdf 17.5 p.115
             */
            calculatedReserve = calculatedReserve.add(networkConfig.getBootstrapAddressAmount());
        }

        log.debug("Unspendable earned rewards: " + unspendableEarnedRewards.longValue() + " Lovelace");
        treasuryForCurrentEpoch = add(treasuryForCurrentEpoch, unspendableEarnedRewards);

        TreasuryCalculationResult treasuryCalculationResult = TreasuryCalculationResult.builder()
                .epoch(epoch)
                .treasury(treasuryForCurrentEpoch)
                .totalRewardPot(rewardPot)
                .treasuryWithdrawals(treasuryWithdrawals)
                .unspendableEarnedRewards(unspendableEarnedRewards)
                .unclaimedRefunds(unclaimedRefunds)
                .build();

        epochCalculationResult.setTotalDistributedRewards(totalDistributedRewards);
        epochCalculationResult.setTotalRewardsPot(rewardPot);
        epochCalculationResult.setReserves(calculatedReserve);
        epochCalculationResult.setTreasury(treasuryForCurrentEpoch);
        epochCalculationResult.setPoolRewardCalculationResults(poolRewardCalculationResults);
        epochCalculationResult.setTotalPoolRewardsPot(stakePoolRewardsPot);
        epochCalculationResult.setTotalAdaInCirculation(adaInCirculation);
        epochCalculationResult.setTotalUndistributedRewards(undistributedRewards);
        epochCalculationResult.setTreasuryCalculationResult(treasuryCalculationResult);

        return epochCalculationResult;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy