Skip to content

Commit d7d7794

Browse files
committed
chore: fix issue about spendable and earned reward that result in a reserve difference around the allegra hard fork
1 parent 088779a commit d7d7794

20 files changed

+288
-230
lines changed

README.md

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Cardano Rewards Calculation 🚧️ Under Construction 🚧️
1+
# Cardano Rewards Calculation
22

33
<p align="left">
44
<img alt="Tests" src="https://github.com/cardano-foundation/cf-java-rewards-calculation/actions/workflows/tests.yaml/badge.svg?branch=main" />
@@ -8,7 +8,13 @@
88
<a href="https://opensource.org/licenses/MIT"><img alt="License" src="https://img.shields.io/badge/License-MIT-green.svg" /></a>
99
</p>
1010

11-
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.
11+
This project is aims to target *multiple goals*. First of all, it tries to *re-implement the cardano ledger rules* for calculating
12+
the [ADA pots](https://cexplorer.io/pot) (Treasury, Reserves, Rewards, Deposits, Utxo, Fees), as well as the rewards for stake pool operators and delegators.
13+
The second goal is to use this implementation to *[validate the rewards calculation](https://cardano-foundation.github.io/cf-java-rewards-calculation/report-latest/treasury_calculation.html)* of the Cardano blockchain by providing a *different implementation of the [ledger specification](https://github.com/IntersectMBO/cardano-ledger?tab=readme-ov-file#cardano-ledger)*.
14+
The third goal is to *provide a library* that can be used in other project (such as [yaci-store](https://github.com/bloxbean/yaci-store)) to serve the rewards data *independently of [DB Sync](https://github.com/IntersectMBO/cardano-db-sync)*.
15+
Last but not least, this project could also be used to *understand the influence of protocol parameters* and the flow of ADA through an interactive report.
16+
17+
🚧️ The calculation is currently correct until the beginning of the Babbage era. 🚧️
1218

1319
## 🧪 Test Reports
1420

@@ -20,24 +26,24 @@ We also generate for each version of this project calculation reports. These rep
2026

2127
```mermaid
2228
flowchart
23-
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/>]
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/>]
26-
subgraph ADA_POTS[" "]
27-
D --> | Unclaimed Rewards | E["ADA Reserves<br /> (monetary expansion) <br /> Started at ~14B ADA"]
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
29-
C --> F[Payouts e.g. for <br /><a href='https://projectcatalyst.io/'>Project Catalyst</a>]
30-
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]
31-
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]
32-
D --> I[...]
33-
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><a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/a3151888e3133937b6098efdec72b587d88ba4cd/src/main/java/org/cardanofoundation/rewards/calculation/PoolRewardCalculation.java#L85'>margin & minPoolCost</a></i></b> | K[Operators]
35-
J --> | <b><i><a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/a3151888e3133937b6098efdec72b587d88ba4cd/src/main/java/org/cardanofoundation/rewards/calculation/PoolRewardCalculation.java#L102'>rewards</a></b></i> | L[Delegators]
36-
D --> | Rewards going to<br /> de-registered<br /> stake addresses | C
37-
L <--> | Stake Key Registration & <br /> Deregistration | M[Deposits]
38-
K <--> | Stake Pool Registration & <br /> Deregistration | M
39-
M --> | <a href='https://github.com/cardano-foundation/cf-java-rewards-calculation/blob/1ed16503d7ed592410d55489cc2144762ce718d5/src/main/java/org/cardanofoundation/rewards/calculation/TreasuryCalculation.java#L122'>Unclaimed Refunds for Retired Pools</a> | C
40-
end
29+
A[Total Transaction Fees <br />at Epoch n] --> B[Total Reward Pot <br />at Epoch n]
30+
B --> | <b><i>treasuryGrowthRate</b></i> | C[Treasury]
31+
B --> | 1 - <b><i>treasuryGrowthRate</b></i> | D[Stake Pool Rewards Pot <br />at Epoch n]
32+
subgraph ADA_POTS[" "]
33+
D --> | Unclaimed Rewards | E["ADA Reserves<br /> (monetary expansion) <br /> Started at ~14B ADA"]
34+
E --> | <b><i>monetaryExpandRate</a></b></i> * Performance of all Stake Pools | B
35+
C --> F[Payouts e.g. for <br />Project Catalyst]
36+
D --> | Rewards Equation<br /> for Pool 1 | G[Stake Pool 1]
37+
D --> | ewards Equation<br /> for Pool 2 | H[Stake Pool 2]
38+
D --> I[...]
39+
D --> | Rewards Equation<br /> for Pool n | J[Stake Pool n]
40+
J --> | <b><i>margin & minPoolCost</i></b> | K[Operators]
41+
J --> | <b><i>rewards</i></b> | L[Delegators]
42+
D --> | Rewards going to<br /> de-registered<br /> stake addresses | C
43+
L <--> | Stake Key Registration & <br /> Deregistration | M[Deposits]
44+
K <--> | Stake Pool Registration & <br /> Deregistration | M
45+
M --> | Unclaimed Refunds for Retired Pools | C
46+
end
4147
4248
style A fill:#5C8DFF,stroke:#5C8DFF
4349
style B fill:#5C8DFF,stroke:#5C8DFF
@@ -74,7 +80,6 @@ This repository offers different data providers and also an interface if you wan
7480
- [Koios Data Provider](./src/main/java/org/cardanofoundation/rewards/validation/data/provider/KoiosDataProvider.java)
7581
- [JSON Data Provider](./src/main/java/org/cardanofoundation/rewards/validation/data/provider/JsonDataProvider.java)
7682
- [DbSync Data Provider](./src/main/java/org/cardanofoundation/rewards/validation/data/provider/DbSyncDataProvider.java)
77-
- Yaci Store Data Provider (Coming Next 👀)
7883

7984
#### Data Fetcher
8085

@@ -98,9 +103,9 @@ POSTGRES_DB=cexplorer
98103
99104
JSON_DATA_SOURCE_FOLDER=/path/to/your/rewards-calculation-test-data
100105
```
106+
`⚠️ The actual rewards data will also be fetched but only used from the validator and not within the calculation package.`
101107

102108
## 🫡 Roadmap
103-
- [ ] Add a data provider for [Yaci Store](https://github.com/bloxbean/yaci-store) (scoped indexer 👀)
104109
- [ ] Create REST endpoints to get the rewards as a service
105110
- [X] Include MIR certificates
106111
- [ ] Add a `/docs` folder containing parsable Markdown files to explain MIR certificates and edge cases
@@ -109,11 +114,12 @@ JSON_DATA_SOURCE_FOLDER=/path/to/your/rewards-calculation-test-data
109114
- [X] Add deposits and utxo pot
110115
- [X] Calculate unclaimed rewards that need to go back to the reserves
111116
- [X] Put rewards to unregistered stake addresses into the treasury
112-
- [ ] A nice web ui to visualize the rewards calculation
117+
- [ ] Create a web ui to visualize the rewards calculation
118+
- [ ] Find out the root cause of the difference between the actual rewards and the calculated rewards beginning with epoch 350
113119

114120
## 📖 Sources
115121
- [Shelley Cardano Delegation Specification](https://github.com/input-output-hk/cardano-ledger/releases/download/cardano-ledger-spec-2023-04-03/shelley-ledger.pdf)
116122
- [Shelley Cardano Ledger Specification](https://github.com/input-output-hk/cardano-ledger/releases/download/cardano-ledger-spec-2023-04-03/shelley-ledger.pdf)
117-
- [Protocol Parameters - CIP-0009](https://cips.cardano.org/cips/cip9/)
123+
- [Protocol Parameters - CIP-0009](https://cips.cardano.org/cips/cip9/#updatableprotocolparameters)
118124
- Beavr Cardano Stake Pool: [How is the Rewards Pot (R) Calculated](https://archive.ph/HQfoV/fb8166e31d2bf61d3d6ca769e7785f2a96530f8e.webp)
119125
- [History of Protocol Parameters](https://beta.explorer.cardano.org/en/protocol-parameters/)

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

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.cardanofoundation.rewards.calculation.domain.*;
44
import org.cardanofoundation.rewards.calculation.enums.MirPot;
55

6+
import java.math.BigDecimal;
67
import java.math.BigInteger;
78
import java.util.ArrayList;
89
import java.util.HashSet;
@@ -32,7 +33,16 @@ public static EpochCalculationResult calculateEpochRewardPots(final int epoch, f
3233
final HashSet<String> sharedPoolRewardAddressesWithoutReward) {
3334
final EpochCalculationResult epochCalculationResult = EpochCalculationResult.builder().epoch(epoch).build();
3435

35-
if (epoch < 209) {
36+
if (epoch < MAINNET_SHELLEY_START_EPOCH) {
37+
log.warn("Epoch " + epoch + " is before the start of the Shelley era. No rewards were calculated in this epoch.");
38+
epochCalculationResult.setReserves(BigInteger.ZERO);
39+
epochCalculationResult.setTreasury(BigInteger.ZERO);
40+
epochCalculationResult.setTotalDistributedRewards(BigInteger.ZERO);
41+
epochCalculationResult.setTotalRewardsPot(BigInteger.ZERO);
42+
epochCalculationResult.setTotalPoolRewardsPot(BigInteger.ZERO);
43+
epochCalculationResult.setTotalAdaInCirculation(BigInteger.ZERO);
44+
return epochCalculationResult;
45+
} else if (epoch == MAINNET_SHELLEY_START_EPOCH) {
3646
epochCalculationResult.setReserves(MAINNET_SHELLEY_INITIAL_RESERVES);
3747
epochCalculationResult.setTreasury(MAINNET_SHELLEY_INITIAL_TREASURY);
3848
epochCalculationResult.setTotalDistributedRewards(BigInteger.ZERO);
@@ -42,26 +52,19 @@ public static EpochCalculationResult calculateEpochRewardPots(final int epoch, f
4252
return epochCalculationResult;
4353
}
4454

45-
double treasuryGrowthRate = 0.2;
46-
double monetaryExpandRate = 0.003;
47-
double decentralizationParameter = 1;
48-
4955
BigInteger totalFeesForCurrentEpoch = BigInteger.ZERO;
5056
int totalBlocksInEpoch = 0;
5157

52-
/* We need to use the epoch info 2 epochs before as shelley starts in epoch 208 it will be possible to get
53-
those values from epoch 210 onwards. Before that we need to use the genesis values, but they are not
54-
needed anyway if the decentralization parameter is > 0.8.
55-
See: shelley-delegation.pdf 5.4.3 */
56-
if (epoch > 209) {
57-
totalFeesForCurrentEpoch = epochInfo.getFees();
58-
treasuryGrowthRate = protocolParameters.getTreasuryGrowRate();
59-
monetaryExpandRate = protocolParameters.getMonetaryExpandRate();
60-
decentralizationParameter = protocolParameters.getDecentralisation();
58+
BigDecimal treasuryGrowthRate = protocolParameters.getTreasuryGrowRate();
59+
BigDecimal monetaryExpandRate = protocolParameters.getMonetaryExpandRate();
60+
BigDecimal decentralizationParameter = protocolParameters.getDecentralisation();
61+
BigInteger activeStakeInEpoch = BigInteger.ZERO;
6162

63+
if (epochInfo != null) {
64+
activeStakeInEpoch = epochInfo.getActiveStake();
65+
totalFeesForCurrentEpoch = epochInfo.getFees();
6266
totalBlocksInEpoch = epochInfo.getBlockCount();
63-
64-
if (decentralizationParameter < 0.8 && decentralizationParameter > 0.0) {
67+
if (isLower(decentralizationParameter, BigDecimal.valueOf(0.8)) && isHigher(decentralizationParameter, BigDecimal.ZERO)) {
6568
totalBlocksInEpoch = epochInfo.getNonOBFTBlockCount();
6669
}
6770
}
@@ -96,7 +99,8 @@ public static EpochCalculationResult calculateEpochRewardPots(final int epoch, f
9699
if (deregisteredOwnerAccounts.contains(rewardAddress) ||
97100
lateDeregisteredOwnerAccounts.contains(rewardAddress) ||
98101
!ownerAccountsRegisteredInThePast.contains(rewardAddress)) {
99-
// If the reward address has been unregistered, the deposit is added to the treasury
102+
// If the reward address has been unregistered, the deposit can not be returned
103+
// and will be added to the treasury instead (Pool Reap see: shelley-ledger.pdf p.53)
100104
treasuryForCurrentEpoch = treasuryForCurrentEpoch.add(POOL_DEPOSIT_IN_LOVELACE);
101105
}
102106
}
@@ -114,7 +118,6 @@ public static EpochCalculationResult calculateEpochRewardPots(final int epoch, f
114118
}
115119

116120
treasuryForCurrentEpoch = treasuryForCurrentEpoch.subtract(treasuryWithdrawals);
117-
118121
BigInteger totalDistributedRewards = BigInteger.ZERO;
119122
final BigInteger adaInCirculation = TOTAL_LOVELACE.subtract(reserveInPreviousEpoch);
120123
final List<PoolRewardCalculationResult> poolRewardCalculationResults = new ArrayList<>();
@@ -124,24 +127,21 @@ public static EpochCalculationResult calculateEpochRewardPots(final int epoch, f
124127
for (String poolId : poolsThatProducedBlocksInEpoch) {
125128
log.debug("[" + i + " / " + poolsThatProducedBlocksInEpoch.size() + "] Processing pool: " + poolId);
126129
PoolHistory poolHistory = poolHistories.stream().filter(history -> history.getPoolId().equals(poolId)).findFirst().orElse(null);
127-
128130
PoolRewardCalculationResult poolRewardCalculationResult = PoolRewardCalculationResult
129131
.builder().poolId(poolId).epoch(epoch).poolReward(BigInteger.ZERO).build();
130132

131133
if(poolHistory != null) {
132-
BigInteger activeStakeInEpoch = BigInteger.ZERO;
133-
if (epochInfo.getActiveStake() != null) {
134-
activeStakeInEpoch = epochInfo.getActiveStake();
135-
}
136-
137-
// Step 10 a: Check if pool reward address or member stake addresses have been unregistered before
134+
// Get the reward addresses of the pool and the reward addresses of its delegators
138135
final HashSet<String> stakeAddresses = new HashSet<>();
139136
stakeAddresses.add(poolHistory.getRewardAddress());
140137
stakeAddresses.addAll(poolHistory.getDelegators().stream().map(Delegator::getStakeAddress).toList());
141-
138+
// We need the get the registration state of those accounts. If they were unregistered before
139+
// the randomness stabilization window, they will not receive any rewards. The remaining of the
140+
// reward pot will go back to the reserves
142141
final HashSet<String> delegatorAccountDeregistrations = deregisteredAccounts.stream()
143142
.filter(stakeAddresses::contains).collect(Collectors.toCollection(HashSet::new));
144-
143+
// If they were unregistered after the randomness stabilization window, rewards will be calculated
144+
// but they will not be spendable and will be added to the treasury instead
145145
final HashSet<String> lateDeregisteredDelegators = lateDeregisteredAccounts.stream()
146146
.filter(stakeAddresses::contains).collect(Collectors.toCollection(HashSet::new));
147147

@@ -151,7 +151,7 @@ public static EpochCalculationResult calculateEpochRewardPots(final int epoch, f
151151
// This is not the case anymore and the stake account receives the reward for all pools
152152
// Until the Allegra hard fork, this method will be used to emulate the old behavior
153153
boolean ignoreLeaderReward = false;
154-
if (epoch < MAINNET_ALLEGRA_HARDFORK_EPOCH) {
154+
if (epoch - 2 < MAINNET_ALLEGRA_HARDFORK_EPOCH) {
155155
ignoreLeaderReward = sharedPoolRewardAddressesWithoutReward.contains(poolId);
156156
}
157157

0 commit comments

Comments
 (0)