Skip to content

Commit d2c85a5

Browse files
Merge pull request #5 from cardano-foundation/release-please--branches--main
chore(main): release 0.0.2-SNAPSHOT
2 parents 8c04ad7 + 9b5ed9a commit d2c85a5

23 files changed

+705
-366
lines changed

README.md

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,33 @@
1010

1111
This java project is used to calculate the rewards of the Cardano network. It aims to be both an edge case documentation and formula implementation.
1212

13+
## 🧪 Test Reports
14+
15+
To ensure the stability and reliability of this project, unit tests have been implemented. By clicking on the link below, you can access the detailed test report.
16+
We also generate for each version of this project calculation reports. These reports are generated by the unit tests and contain the calculation results compared to the actual values.
17+
1318
📈 [Treasury Calculation Report](https://cardano-foundation.github.io/cf-java-rewards-calculation/report-latest/treasury_calculation.html)
19+
📊 [Coverage Report](https://cardano-foundation.github.io/cf-java-rewards-calculation/coverage-report/)
1420

1521
```mermaid
1622
flowchart
1723
A[Total Transaction Fees <br />at Epoch n] --> B[<a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/a794130dc0e320426725a58b8b15f1fbe726b2de/src/main/java/org/cardanofoundation/rewards/calculation/TreasuryCalculation.java#L42'>Total Reward Pot <br />at Epoch n</a/>]
18-
B --> | <a href='https://cips.cardano.org/cips/cip9/#updatableprotocolparameters'>treasuryGrowthRate</a> | C[<a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/a794130dc0e320426725a58b8b15f1fbe726b2de/src/main/java/org/cardanofoundation/rewards/calculation/TreasuryCalculation.java#L17'>Treasury</a/>]
19-
B --> | 1 - <a href='https://cips.cardano.org/cips/cip9/#updatableprotocolparameters'>treasuryGrowthRate</a> | D[<a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/a794130dc0e320426725a58b8b15f1fbe726b2de/src/test/java/org/cardanofoundation/rewards/calculation/PoolRewardCalculationTest.java#L63'>Stake Pool Rewards Pot <br />at Epoch n</a/>]
24+
B --> | <b><i><a href='https://cips.cardano.org/cips/cip9/#updatableprotocolparameters'>treasuryGrowthRate</a></b></i> | C[<a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/a794130dc0e320426725a58b8b15f1fbe726b2de/src/main/java/org/cardanofoundation/rewards/calculation/TreasuryCalculation.java#L17'>Treasury</a/>]
25+
B --> | 1 - <b><i><a href='https://cips.cardano.org/cips/cip9/#updatableprotocolparameters'>treasuryGrowthRate</a></b></i> | D[<a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/a794130dc0e320426725a58b8b15f1fbe726b2de/src/test/java/org/cardanofoundation/rewards/calculation/PoolRewardCalculationTest.java#L63'>Stake Pool Rewards Pot <br />at Epoch n</a/>]
2026
subgraph ADA_POTS[" "]
2127
D --> | Unclaimed Rewards | E["ADA Reserves<br /> (monetary expansion) <br /> Started at ~14B ADA"]
22-
E --> | <a href='https://cips.cardano.org/cips/cip9/#updatableprotocolparameters'>monetaryExpandRate</a> * <a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/main/src/main/java/org/cardanofoundation/rewards/calculation/PoolRewardCalculation.java#L19'>apparent performance of all stake pools</a> | B
28+
E --> | <b><i><a href='https://cips.cardano.org/cips/cip9/#updatableprotocolparameters'>monetaryExpandRate</a></b></i> * <a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/main/src/main/java/org/cardanofoundation/rewards/calculation/PoolRewardCalculation.java#L19'>Performance of all Stake Pools</a> | B
2329
C --> F[Payouts e.g. for <br /><a href='https://projectcatalyst.io/'>Project Catalyst</a>]
2430
D --> | <a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/a794130dc0e320426725a58b8b15f1fbe726b2de/src/main/java/org/cardanofoundation/rewards/calculation/PoolRewardCalculation.java#L87'>Rewards Equation<br /> for Pool 1</a> | G[Stake Pool 1]
2531
D --> | <a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/a794130dc0e320426725a58b8b15f1fbe726b2de/src/main/java/org/cardanofoundation/rewards/calculation/PoolRewardCalculation.java#L87'>Rewards Equation<br /> for Pool 2</a> | H[Stake Pool 2]
2632
D --> I[...]
2733
D --> | <a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/a794130dc0e320426725a58b8b15f1fbe726b2de/src/main/java/org/cardanofoundation/rewards/calculation/PoolRewardCalculation.java#L87'>Rewards Equation<br /> for Pool n</a> | J[Stake Pool n]
34+
J --> | <b><i>margin & minPoolCost</i></b> | K[Operators]
35+
J --> | <b><i>rewards</b></i> | L[Delegators]
36+
D --> | Capped Pool Rewards | C
37+
L <--> | Stake Key Registration & <br /> Deregistration | M[Deposits]
38+
K <--> | Stake Pool Registration & <br /> Deregistration | M
39+
M --> | Unclaimed Refunds for Retired Pools | C
2840
end
2941
3042
style A fill:#5C8DFF,stroke:#5C8DFF
@@ -56,13 +68,17 @@ cd cf-java-rewards-calculation
5668
./mvnw clean test
5769
```
5870

59-
## 🧪 Test Reports
60-
61-
To ensure the stability and reliability of this project, unit tests have been implemented. By clicking on the link below, you can access the detailed test report.
62-
63-
📊 [Coverage Report](https://cardano-foundation.github.io/cf-java-rewards-calculation/coverage-report/)
71+
## 🫡 Todo
72+
- [ ] Include MIR certificates
73+
- [ ] Add a `/docs` folder containing parsable Markdown files to explain MIR certificates and edge cases
74+
- [ ] Implement "The difference between the maximal amount and the actual amount received is added to the amount moved to the treasury" (shelley-ledger.pdf p.65)
75+
- [ ] Enhance reporting and add values for the other pots as well. Include information from the `/docs` folder
76+
- [ ] Add member and operator reward calculation as well as the deposits pot
77+
- [ ] Calculate unclaimed rewards that need to go back to the reserves
6478

6579
## 📖 Sources
66-
80+
- [Shelley Cardano Delegation Specification](https://github.com/input-output-hk/cardano-ledger/releases/download/cardano-ledger-spec-2023-04-03/shelley-ledger.pdf)
81+
- [Shelley Cardano Ledger Specification](https://github.com/input-output-hk/cardano-ledger/releases/download/cardano-ledger-spec-2023-04-03/shelley-ledger.pdf)
82+
- [Protocol Parameters - CIP-0009](https://cips.cardano.org/cips/cip9/)
6783
- Beavr Cardano Stake Pool: [How is the Rewards Pot (R) Calculated](https://archive.ph/HQfoV/fb8166e31d2bf61d3d6ca769e7785f2a96530f8e.webp)
68-
- Protocol Parameters: https://beta.explorer.cardano.org/en/protocol-parameters/
84+
- [History of Protocol Parameters](https://beta.explorer.cardano.org/en/protocol-parameters/)

src/main/java/org/cardanofoundation/rewards/calculation/PoolRewardCalculation.java

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ public class PoolRewardCalculation {
1111
*
1212
* hint: shelley-delegation.pdf 3.8.3
1313
* As long as we have d >= 0.8, we set the apparent performance of any pool to 1
14+
*
15+
* See Haskell implementation: https://github.com/input-output-hk/cardano-ledger/blob/64459cc87094331c79d11880e0a4c81b9a721ab0/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rewards.hs#L87C32-L87C44
1416
*/
15-
public static double calculateApparentPoolPerformance(final double activePoolStake, final double totalActiveEpochStake, final int blocksMintedByPool, final int totalBlocksInEpoch, final double decentralizationParam) {
17+
public static double calculateApparentPoolPerformance(final double activePoolStake, final double totalActiveEpochStake, final int blocksMintedByPool, final int blocksMintedByStakePools, final double decentralizationParam) {
1618
if (decentralizationParam >= 0.8) {
1719
return 1.0;
1820
} else if (activePoolStake == 0.0 || totalActiveEpochStake == 0.0) {
1921
return 0.0;
2022
} else {
21-
final double blocksMintedInNonOBFTSlots = (double) totalBlocksInEpoch * (1.0 - decentralizationParam);
22-
final double relativeBlocksCreatedInEpoch = blocksMintedByPool / blocksMintedInNonOBFTSlots;
23+
24+
final double relativeBlocksCreatedInEpoch = (double) blocksMintedByPool / (double) blocksMintedByStakePools;
2325
final double relativeActiveStake = activePoolStake / totalActiveEpochStake;
2426
return relativeBlocksCreatedInEpoch / relativeActiveStake;
2527
}
@@ -44,6 +46,8 @@ public static double calculateApparentPoolPerformance(final double activePoolSta
4446
* (( sizeOfASaturatedPool - cappedRelativeStake ) / sizeOfASaturatedPool))
4547
* / sizeOfASaturatedPool)
4648
* )
49+
*
50+
* 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
4751
*/
4852
public static double calculateOptimalPoolReward(double totalAvailableRewards, int optimalPoolCount, double influence, double relativeStakeOfPool, double relativeStakeOfPoolOwner) {
4953

@@ -53,20 +57,17 @@ public static double calculateOptimalPoolReward(double totalAvailableRewards, in
5357

5458
// R / (1 + a0)
5559
// "R are the total available rewards for the epoch (in ada)." (shelley-delegation.pdf 5.5.3)
56-
double totalAvailableRewardsInAda = Math.round(totalAvailableRewards);
57-
double rewardsDividedByOnePlusInfluence = totalAvailableRewardsInAda / (1 + influence);
58-
59-
// s' * a0
60-
double influenceOfOwner = cappedRelativeStakeOfPoolOwner * influence;
60+
double rewardsDividedByOnePlusInfluence = totalAvailableRewards / (1 + influence);
6161

62-
// s'((z0 - o')/ z0)
63-
double relativeStakeOfSaturatedPool = cappedRelativeStakeOfPoolOwner * ((sizeOfASaturatedPool - cappedRelativeStake) / sizeOfASaturatedPool);
62+
// (z0 - sigma') / z0
63+
double relativeStakeOfSaturatedPool = (sizeOfASaturatedPool - cappedRelativeStake) / sizeOfASaturatedPool;
6464

65-
// o' - ((s' * (z0 - o')/ z0) / z0)
66-
double saturatedPoolWeight = cappedRelativeStake - (relativeStakeOfSaturatedPool / sizeOfASaturatedPool);
65+
// (sigma' - s' * relativeStakeOfSaturatedPool) / z0
66+
double saturatedPoolWeight = (cappedRelativeStake - cappedRelativeStakeOfPoolOwner * relativeStakeOfSaturatedPool) / sizeOfASaturatedPool;
6767

68-
// R/(1+a0) (s'a0(o' - (s'(z0 - o') / z0)) / z0)
69-
return rewardsDividedByOnePlusInfluence * (cappedRelativeStake + influenceOfOwner * saturatedPoolWeight);
68+
// R / (1+a0) * (sigma' + s' * a0 * saturatedPoolWeight)
69+
return Math.floor(rewardsDividedByOnePlusInfluence *
70+
(cappedRelativeStake + cappedRelativeStakeOfPoolOwner * influence * saturatedPoolWeight));
7071
}
7172

7273
/*
@@ -76,27 +77,4 @@ public static double calculateOptimalPoolReward(double totalAvailableRewards, in
7677
public static double calculatePoolReward(double optimalPoolReward, double poolPerformance) {
7778
return optimalPoolReward * poolPerformance;
7879
}
79-
80-
/*
81-
* Calculate the total reward pot (R) with transposing the equation from above:
82-
*/
83-
public static double calculateRewardPotByOptimalPoolReward(double poolReward, int optimalPoolCount, double influence, double relativeStakeOfPool, double relativeStakeOfPoolOwner, double poolPerformance) {
84-
85-
double sizeOfASaturatedPool = 1.0 / optimalPoolCount;
86-
double cappedRelativeStake = Math.min(sizeOfASaturatedPool, relativeStakeOfPool);
87-
double cappedRelativeStakeOfPoolOwner = Math.min(sizeOfASaturatedPool, relativeStakeOfPoolOwner);
88-
89-
// s' * a0
90-
double influenceOfOwner = cappedRelativeStakeOfPoolOwner * influence;
91-
92-
// s'((z0 - o')/ z0)
93-
double relativeStakeOfSaturatedPool = cappedRelativeStakeOfPoolOwner * ((sizeOfASaturatedPool - cappedRelativeStake) / sizeOfASaturatedPool);
94-
95-
// o' - (s' * (z0 - o')/ z0) / z0)
96-
double saturatedPoolWeight = (cappedRelativeStake - relativeStakeOfSaturatedPool) / sizeOfASaturatedPool;
97-
98-
99-
double poolWeightsWithPerformance = poolPerformance * (cappedRelativeStake + influenceOfOwner * saturatedPoolWeight);
100-
return ((1.0 + influence) * poolReward) / poolWeightsWithPerformance;
101-
}
10280
}

src/main/java/org/cardanofoundation/rewards/calculation/TreasuryCalculation.java

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package org.cardanofoundation.rewards.calculation;
22

33
import org.cardanofoundation.rewards.data.provider.DataProvider;
4-
import org.cardanofoundation.rewards.entity.AdaPots;
5-
import org.cardanofoundation.rewards.entity.Epoch;
6-
import org.cardanofoundation.rewards.entity.ProtocolParameters;
7-
import org.cardanofoundation.rewards.entity.TreasuryCalculationResult;
4+
import org.cardanofoundation.rewards.data.provider.JsonDataProvider;
5+
import org.cardanofoundation.rewards.data.provider.KoiosDataProvider;
6+
import org.cardanofoundation.rewards.entity.*;
7+
import org.cardanofoundation.rewards.enums.AccountUpdateAction;
88

9-
import static org.cardanofoundation.rewards.constants.RewardConstants.EXPECTED_SLOT_PER_EPOCH;
9+
import java.util.ArrayList;
10+
import java.util.Comparator;
11+
import java.util.List;
12+
13+
import static org.cardanofoundation.rewards.constants.RewardConstants.*;
1014
import static org.cardanofoundation.rewards.util.CurrencyConverter.lovelaceToAda;
1115

1216
public class TreasuryCalculation {
@@ -30,7 +34,7 @@ public static double calculateTreasury(double treasuryGrowRate, double rewardPot
3034
public static double calculateTotalRewardPotWithEta(double monetaryExpandRate, int totalBlocksInEpochByPools,
3135
double decentralizationParameter, double reserve, double fee) {
3236
double eta = calculateEta(totalBlocksInEpochByPools, decentralizationParameter);
33-
return Math.floor(reserve * monetaryExpandRate * eta + fee);
37+
return Math.floor(reserve * monetaryExpandRate * eta) + fee;
3438
}
3539

3640
/*
@@ -98,17 +102,9 @@ public static TreasuryCalculationResult calculateTreasuryForEpoch(int epoch, Dat
98102
double treasuryForCurrentEpoch = TreasuryCalculation.calculateTreasury(
99103
treasuryGrowthRate, rewardPot, treasuryInPreviousEpoch);
100104

101-
/*
102-
TODO: "For each retiring pool, the refund for the pool registration deposit is added to the
103-
pool's registered reward account, provided the reward account is still registered." -
104-
https://github.com/input-output-hk/cardano-ledger/blob/9e2f8151e3b9a0dde9faeb29a7dd2456e854427c/eras/shelley/formal-spec/epoch.tex#L546C9-L547C87
105-
106-
107-
int retiredPoolsWithDeregisteredRewardAddress = koiosDataProvider.countRetiredPoolsWithDeregisteredRewardAddress(epoch - 1);
108-
109-
BigDecimal deposit = new BigDecimal(DEPOSIT_POOL_REGISTRATION_IN_ADA);
110-
treasuryForCurrentEpoch = treasuryForCurrentEpoch.add(deposit.multiply(new BigDecimal(retiredPoolsWithDeregisteredRewardAddress)));
111-
*/
105+
// The sum of all the refunds attached to unregistered reward accounts are added to the
106+
// treasury (see: Pool Reap Transition, p.53, figure 40, shely-ledger.pdf)
107+
treasuryForCurrentEpoch += TreasuryCalculation.calculateUnclaimedRefundsForRetiredPools(epoch, dataProvider);
112108

113109
return TreasuryCalculationResult.builder()
114110
.calculatedTreasury(treasuryForCurrentEpoch)
@@ -117,4 +113,44 @@ public static TreasuryCalculationResult calculateTreasuryForEpoch(int epoch, Dat
117113
.totalRewardPot(rewardPot)
118114
.build();
119115
}
116+
117+
/*
118+
"For each retiring pool, the refund for the pool registration deposit is added to the
119+
pool's registered reward account, provided the reward account is still registered." -
120+
https://github.com/input-output-hk/cardano-ledger/blob/9e2f8151e3b9a0dde9faeb29a7dd2456e854427c/eras/shelley/formal-spec/epoch.tex#L546C9-L547C87
121+
*/
122+
public static Double calculateUnclaimedRefundsForRetiredPools(int epoch, DataProvider dataProvider) {
123+
List<PoolDeregistration> retiredPools = dataProvider.getRetiredPoolsInEpoch(epoch);
124+
125+
double refunds = 0.0;
126+
127+
if (retiredPools.size() > 0) {
128+
// The deposit will pay back one epoch later
129+
List<AccountUpdate> accountUpdates = dataProvider.getAccountUpdatesUntilEpoch(
130+
retiredPools.stream().map(PoolDeregistration::getRewardAddress).toList(), epoch - 1);
131+
132+
// Order list by unix block time
133+
accountUpdates = accountUpdates.stream().filter(update ->
134+
update.getAction().equals(AccountUpdateAction.DEREGISTRATION)
135+
|| update.getAction().equals(AccountUpdateAction.REGISTRATION)).sorted(
136+
Comparator.comparing(AccountUpdate::getUnixBlockTime).reversed()).toList();
137+
138+
// only hold the latest account update for each reward address
139+
// preventing the case of an unregistered reward address becoming registered again
140+
List<AccountUpdate> latestAccountUpdates = new ArrayList<>();
141+
for (AccountUpdate accountUpdate : accountUpdates) {
142+
if (latestAccountUpdates.stream().map(AccountUpdate::getStakeAddress).noneMatch(stakeAddress -> stakeAddress.equals(accountUpdate.getStakeAddress()))) {
143+
latestAccountUpdates.add(accountUpdate);
144+
}
145+
}
146+
147+
for (AccountUpdate lastAccountUpdate : latestAccountUpdates) {
148+
if (lastAccountUpdate.getAction() == AccountUpdateAction.DEREGISTRATION) {
149+
refunds += DEPOSIT_POOL_REGISTRATION_IN_LOVELACE;
150+
}
151+
}
152+
}
153+
154+
return refunds;
155+
}
120156
}

0 commit comments

Comments
 (0)