Skip to content

Conversation

@wolf31o2
Copy link
Member

@wolf31o2 wolf31o2 commented Nov 18, 2025

Summary by CodeRabbit

  • New Features

    • Added comprehensive reward calculation and distribution for epochs (pools and delegators).
    • Ledger state now exposes reward-related APIs for fetching/updating ADA pots and snapshots.
    • Added a RewardService to compute and apply epoch rewards and validate distributions.
  • Tests

    • Extensive unit and integration tests covering reward math, edge cases, validation, and end-to-end scenarios.

@wolf31o2 wolf31o2 requested a review from a team as a code owner November 18, 2025 22:21
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 18, 2025

📝 Walkthrough

Walkthrough

This PR adds end-to-end reward calculation features: new core types (AdaPots, RewardParameters, RewardSnapshot, PoolRewards, RewardCalculationResult, PoolRetirementInfo), complete reward calculation and helper functions (CalculateAdaPots, CalculateRewards, eta/pool performance/share/saturation helpers, distribution logic), a RewardService that orchestrates epoch reward calculation and pot updates, a RewardState interface embedded into LedgerState, updates to the mock ledger state to satisfy the new interface, and extensive tests validating formulas, reference scenarios, edge cases, and an integration-style test using the mock ledger.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Dense, numerically sensitive logic in ledger/common/rewards.go: verify formulas, rational arithmetic, rounding, overflow handling, and error paths.
  • Extensive tests in ledger/common/rewards_test.go and ledger/common/rewards_validation_test.go: review coverage, determinism, and that test scaffolding matches production assumptions.
  • Interface change in ledger/common/state.go and mock updates in internal/test/ledger/ledger.go: confirm implementations satisfy the new RewardState methods and error semantics.
  • RewardService in ledger/common/rewards_service.go: check error wrapping, pot update sequence, and parameter construction for CalculateRewards.

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: initial rewards calculation formulas' directly describes the main change—introduction of comprehensive reward calculation logic with data models, calculation functions, and tests.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/initial-rewards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@wolf31o2 wolf31o2 force-pushed the feat/initial-rewards branch 2 times, most recently from 3816143 to f884773 Compare November 19, 2025 00:06
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (12)
ledger/common/state.go (1)

41-58: RewardState shape looks coherent; consider future extensibility around parameters

The RewardState interface and its embedding into LedgerState align cleanly with the new RewardService and mocks: CalculateRewards, GetAdaPots, UpdateAdaPots, and GetRewardSnapshot are exactly what the service needs. The only thing to keep in mind is that CalculateEpochRewards currently hardcodes RewardParameters inside the service; if you later move protocol parameters into ledger state, you may want either an additional interface (e.g., ProtocolState) or a method here to fetch them rather than constructing defaults in the service.

Also applies to: 66-66

internal/test/ledger/ledger.go (1)

29-31: MockLedgerState correctly implements RewardState; clarify UpdateAdaPots semantics

The added fields and methods give MockLedgerState full RewardState coverage and delegate reward calculation to the production CalculateRewards, which is useful for integration-style tests. Note that UpdateAdaPots is intentionally a no-op; if any tests start relying on pots mutation through this mock (similar to the local mockLedgerState in ledger/common/rewards_test.go), they will need a stateful implementation here as well or explicit comments indicating that only read‑only behavior is expected from this mock.

Also applies to: 95-116

ledger/common/rewards_test.go (2)

28-71: Reward pot and distribution tests are well‑structured; potential future enhancements

The TestCalculateAdaPots and TestCalculateRewards scenarios line up with the formulas in CalculateAdaPots and CalculateRewards and assert the key invariants (pot movements, full pot depletion, and distribution sum). Once the core formulas settle, you might consider tightening these tests by also asserting specific per‑pool values (or at least relative proportions) to catch regressions in share calculations, not just conservation of the total.

Also applies to: 73-147


212-220: Handle possible JSON unmarshal failure in createGenesisRat

createGenesisRat ignores the error from UnmarshalJSON. If the JSON shape ever changes, tests may silently use a zero value instead of failing loudly, making debugging harder. Since this is test‑only code, panicking on error is acceptable and keeps failures obvious.

 func createGenesisRat(num, denom int64) GenesisRat {
 	var r GenesisRat
 	jsonData := []byte(
 		fmt.Sprintf(`{"numerator": %d, "denominator": %d}`, num, denom),
 	)
-	r.UnmarshalJSON(jsonData)
+	if err := r.UnmarshalJSON(jsonData); err != nil {
+		panic(fmt.Sprintf("failed to unmarshal GenesisRat: %v", err))
+	}
 	return r
 }
ledger/common/rewards_validation_test.go (2)

25-90: Spec helper formulas are encoded correctly; consider using them to assert on production results

TestShelleyRewardFormulas plus the calculateLeaderReward/calculateMemberReward helpers implement the Shelley/Amaru reward formulas in a clear, numeric way and validate them against hand‑computed expectations. Right now these helpers are only used to test themselves; as a follow‑up, it may be worth using them to derive expected leader/member results from a small RewardSnapshot and compare those against what CalculateRewards/distributePoolRewards produce for a single pool, to directly pin production behavior to the spec.

Also applies to: 339-371


222-337: Validation and compatibility tests provide strong coverage; some duplication is acceptable

The edge‑case tests (TestRewardEdgeCases) and the Java/Amaru compatibility suites do a good job of pinning behavior around high margins, zero‑reward pools, owner‑only delegation, eta calculation, pool performance, and monetary expansion. There is some intentional duplication between the “MonetaryExpansion_With_Eta” and “MonetaryExpansion_With_Eta_Amaru” subtests, but they serve as regression tests from different reference perspectives; if this starts to feel noisy later, you can always factor a shared helper for the common numeric scenario without changing coverage.

Also applies to: 373-681

ledger/common/rewards_service.go (2)

22-83: Epoch reward workflow is coherent; protocol parameters are currently hardcoded

RewardService.CalculateEpochRewards wires together GetAdaPots, GetRewardSnapshot, CalculateRewards, and UpdateAdaPots in a straightforward way, with good error wrapping. The inlined RewardParameters (rho, tau, a₀) are clearly marked as temporary defaults; when you later fetch real protocol parameters from the ledger, this method should adapt cleanly because all dependencies are already abstracted behind LedgerState.


85-112: ValidateRewardDistribution only checks total sum; document or extend as needed

ValidateRewardDistribution currently verifies that the sum of all actual rewards matches the sum of per‑pool TotalRewards, which is a useful coarse check but doesn’t validate per‑address correctness. That’s fine as a first step, especially given the comment about MIR certificates, but it might be worth either documenting this explicitly or extending it later to aggregate expected rewards by AddrKeyHash and compare address‑by‑address once the distribution mechanism is finalized.

ledger/common/rewards.go (4)

347-383: Reuse saturation helper and guard against pathological parameter values in calculatePoolShare

The core logic is fine, but a couple of robustness points:

  • Saturation is hard-coded as stakeRatio/0.05 (5%) while you also expose CalculatePoolSaturation with a configurable saturationPoint. Consider reusing that helper or a shared constant so you don’t end up with conflicting saturation definitions.
  • a0Float, _ := a0.Float64() and marginFloat(margin) assume reasonable parameter ranges. Extremely large/negative a0 or margin > 1 could make denominator <= 0 or numerator < 0, which then produce negative or infinite shares. Even if inputs are supposed to be constrained, a defensive clamp helps keep the math sane.

Example defensive tweaks:

-    saturation := math.Min(stakeRatio/0.05, 1.0) // Assuming 5% saturation point
+    saturation := math.Min(stakeRatio/0.05, 1.0) // TODO: consider wiring a param or helper

@@
-    a0Float, _ := a0.Float64()
-    denominator := 1.0 + a0Float*saturation
+    a0Float, _ := a0.Float64()
+    if a0Float < 0 {
+        a0Float = 0
+    }
+    denominator := 1.0 + a0Float*saturation
+    if denominator <= 0 {
+        return 0
+    }

385-455: Clarify leftover handling and deregistration semantics in distributePoolRewards

The high-level operator/delegator split looks good, but a few behavioral points are worth tightening:

  • Rounding: Delegator rewards are computed via float math and truncated to uint64, so sum(delegatorRewards) may be less than stakeholderRewardsTotal. The leftovers are silently dropped but TotalRewards is still reported as totalPoolRewards. If you care about conservation, you might want to track the actually assigned sum or redirect the remainder (e.g., to treasury or operator).
  • Deregistrations: Only StakeRegistrations is consulted. The snapshot also carries EarlyDeregistrations and LateDeregistrations, but they aren’t used here. If protocol rules require different handling for those sets (e.g., redirecting their rewards to reserves/treasury), that logic is currently missing from the distribution layer.

If you want to preserve all rewards per pool, one simple option is to assign any rounding remainder to the operator:

-    if totalPoolStake > 0 && stakeholderRewardsTotal > 0 {
+    assigned := uint64(0)
+    if totalPoolStake > 0 && stakeholderRewardsTotal > 0 {
         for stakeKey, stake := range delegatorStake {
@@
                 )
-                delegatorRewards[stakeKey] = reward
+                delegatorRewards[stakeKey] = reward
+                assigned += reward
         }
     }
+
+    if stakeholderRewardsTotal > assigned {
+        operatorRewards += stakeholderRewardsTotal - assigned
+    }

475-487: CalculateOptimalPoolCount stub is fine for now but should be clearly marked as simplified

The implementation currently ignores totalActiveStake (using _ is good) and just returns k, which matches the comment about simplification. If this is intentional as a placeholder, you might want to add a TODO so callers don’t over-interpret the result as mathematically “optimal”.

Example:

-// totalActiveStake is unused as current implementation is simplified and only uses k parameter
+// totalActiveStake is unused as current implementation is simplified and only uses k parameter.
+// TODO: implement real optimal pool count formula when requirements are finalized.

512-529: CalculatePoolSaturation behavior for small stakes and zero saturation point

The implementation is straightforward and numerically safe. A couple of behavioral notes:

  • When saturationPointStake == 0 (e.g., tiny totalActiveStake or misconfigured saturationPoint), you return 1.0 (“fully saturated”), which is a reasonable fail-safe but might be surprising—documenting this edge case would help.
  • You’re using a percent-style saturationPoint here, but calculatePoolShare hard-codes 5% via 0.05. Over time, it’d be good to align both on the same configuration source to avoid divergent definitions.

Otherwise, this helper looks fine.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df73e42 and f884773.

📒 Files selected for processing (6)
  • internal/test/ledger/ledger.go (2 hunks)
  • ledger/common/rewards.go (1 hunks)
  • ledger/common/rewards_service.go (1 hunks)
  • ledger/common/rewards_test.go (1 hunks)
  • ledger/common/rewards_validation_test.go (1 hunks)
  • ledger/common/state.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
ledger/common/state.go (1)
ledger/common/rewards.go (5)
  • CalculateRewards (240-345)
  • AdaPots (26-30)
  • RewardSnapshot (64-99)
  • RewardParameters (33-61)
  • RewardCalculationResult (108-117)
internal/test/ledger/ledger.go (1)
ledger/common/rewards.go (5)
  • AdaPots (26-30)
  • RewardSnapshot (64-99)
  • CalculateRewards (240-345)
  • RewardParameters (33-61)
  • RewardCalculationResult (108-117)
ledger/common/rewards_service.go (2)
ledger/common/state.go (1)
  • LedgerState (61-68)
ledger/common/rewards.go (4)
  • RewardCalculationResult (108-117)
  • RewardParameters (33-61)
  • CalculateRewards (240-345)
  • PoolRewards (120-129)
ledger/common/rewards_test.go (5)
ledger/common/rewards.go (9)
  • AdaPots (26-30)
  • RewardParameters (33-61)
  • CalculateAdaPots (132-181)
  • RewardSnapshot (64-99)
  • CalculateRewards (240-345)
  • PoolRewards (120-129)
  • ValidatePoolPledge (491-510)
  • CalculatePoolSaturation (513-529)
  • RewardCalculationResult (108-117)
ledger/common/certs.go (1)
  • PoolKeyHash (331-331)
cbor/tags.go (1)
  • Rat (94-96)
ledger/common/common.go (1)
  • GenesisRat (523-523)
ledger/common/rewards_service.go (1)
  • NewRewardService (28-32)
ledger/common/rewards_validation_test.go (2)
ledger/common/rewards.go (7)
  • AdaPots (26-30)
  • RewardSnapshot (64-99)
  • RewardParameters (33-61)
  • CalculateRewards (240-345)
  • PoolRewards (120-129)
  • CalculateAdaPots (132-181)
  • PoolRetirementInfo (102-105)
ledger/common/certs.go (1)
  • PoolKeyHash (331-331)
ledger/common/rewards.go (4)
ledger/common/pparams.go (1)
  • ProtocolParametersProtocolVersion (29-33)
cbor/tags.go (1)
  • Rat (94-96)
ledger/common/certs.go (1)
  • PoolKeyHash (331-331)
ledger/common/common.go (1)
  • GenesisRat (523-523)
🔇 Additional comments (5)
ledger/common/rewards_test.go (2)

149-210: Margin, pledge, and saturation tests give good coverage

The margin tests (TestMarginFloat) and the pledge/saturation checks (TestValidatePoolPledge, TestCalculatePoolSaturation) closely mirror the intended semantics of the underlying helpers (zero/no‑rat margin, owner stake vs pledge, saturation capped at 1). These should be useful guardrails against subtle changes in those utilities.

Also applies to: 163-183


222-304: RewardServiceIntegration + local mockLedgerState are consistent with RewardService design

The TestRewardServiceIntegration flow, together with the local mockLedgerState, exercises the full RewardService.CalculateEpochRewards path: reading pots and snapshot, delegating to CalculateRewards, updating pots, and validating total distribution. The mock’s mutable adaPots and GetAdaPots/UpdateAdaPots implementations accurately simulate the state update the service expects, and the test assertions (reward count, total rewards, updated pots.Rewards == 0) match the production behavior.

Also applies to: 306-363

ledger/common/rewards.go (3)

25-129: Type definitions look coherent and match the intended domain

The AdaPots, RewardParameters, RewardSnapshot, PoolRetirementInfo, RewardCalculationResult, and PoolRewards structs are well-organized and give you a clear, extendable shape for reward calculation. Field types and comments line up with the domain, and maps are keyed consistently on PoolKeyHash/AddrKeyHash.

No issues here from a correctness or Go-idiom perspective.


458-473: marginFloat is robust but can be simplified slightly

The zero-margin guard and nil checks make this safe even with the cbor.Rat wrapper, and using big.Rat as an intermediate is fine. Given GenesisRat is an alias, you could optionally simplify the zero check to something closer to the underlying representation if desired, but that’s stylistic.

Logic-wise this looks correct.


489-510: ValidatePoolPledge implementation looks correct and idiomatic

This function cleanly:

  • Handles nil poolParams by returning false.
  • Sums owner stake from the provided ownerStake map (safe even if the map is nil).
  • Compares against poolParams.Pledge as expected.

No changes needed.

Comment on lines 131 to 190
// CalculateAdaPots calculates the ADA pots for the next epoch
func CalculateAdaPots(
currentPots AdaPots,
params RewardParameters,
totalActiveStake uint64,
epochFees uint64,
totalBlocksInEpoch uint32,
) AdaPots {
// Calculate eta (pool performance factor)
eta := calculateEta(totalBlocksInEpoch, params)

// Calculate monetary expansion (rho * eta * reserves)
// rho is in millionths (3000 = 0.3%)
monetaryExpansion := new(big.Int).SetUint64(params.MonetaryExpansion)
reserves := new(big.Int).SetUint64(currentPots.Reserves)
expansion := new(big.Int).Mul(monetaryExpansion, reserves)
expansion.Div(
expansion,
new(big.Int).SetUint64(1000000),
) // Convert from millionths

// Apply eta factor
etaBig := eta
expansion = new(big.Int).Mul(expansion, etaBig.Num())
expansion.Div(expansion, etaBig.Denom())

// Add epoch fees to reward pot
rewardPot := new(big.Int).SetUint64(epochFees)
rewardPot.Add(rewardPot, expansion)

// Calculate treasury contribution (tau * (expansion + fees))
// tau is in ten-thousandths (2000 = 20%)
treasuryGrowth := new(big.Int).SetUint64(params.TreasuryGrowth)
treasuryContribution := new(big.Int).Mul(treasuryGrowth, rewardPot)
treasuryContribution.Div(
treasuryContribution,
new(big.Int).SetUint64(10000),
) // Convert from ten-thousandths

// Subtract treasury contribution from reward pot
rewardPot.Sub(rewardPot, treasuryContribution)

// Update pots
newPots := AdaPots{
Reserves: currentPots.Reserves - expansion.Uint64(),
Treasury: currentPots.Treasury + treasuryContribution.Uint64(),
Rewards: rewardPot.Uint64(),
}

return newPots
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fix unused parameter and guard against reserve underflow in CalculateAdaPots

  • totalActiveStake is not used anywhere in the function body. In Go this is a compile error, so either use it or change the parameter name to _ until it’s needed.
  • Reserves: currentPots.Reserves - expansion.Uint64() can underflow if parameters are misconfigured such that expansion > currentPots.Reserves. That will wrap the uint64 and silently produce a huge reserves value.

Consider:

-func CalculateAdaPots(
-    currentPots AdaPots,
-    params RewardParameters,
-    totalActiveStake uint64,
-    epochFees uint64,
-    totalBlocksInEpoch uint32,
-) AdaPots {
+func CalculateAdaPots(
+    currentPots AdaPots,
+    params RewardParameters,
+    _ uint64, // totalActiveStake (currently unused)
+    epochFees uint64,
+    totalBlocksInEpoch uint32,
+) AdaPots {
@@
-    newPots := AdaPots{
-        Reserves: currentPots.Reserves - expansion.Uint64(),
-        Treasury: currentPots.Treasury + treasuryContribution.Uint64(),
-        Rewards:  rewardPot.Uint64(),
-    }
+    expansionU64 := expansion.Uint64()
+    reserves := currentPots.Reserves
+    if expansionU64 > reserves {
+        expansionU64 = reserves
+    }
+    newPots := AdaPots{
+        Reserves: reserves - expansionU64,
+        Treasury: currentPots.Treasury + treasuryContribution.Uint64(),
+        Rewards:  rewardPot.Uint64(),
+    }

I'll verify the review comment by examining the actual codebase. Let me search for the relevant code and confirm the issues.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find and examine the CalculateAdaPots function in rewards.go
find . -name "rewards.go" -path "*/common/*" -type f 2>/dev/null | head -5

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Search for the CalculateAdaPots function to verify parameter usage
rg -n "func CalculateAdaPots" --type go -A 50

Length of output: 3033


🏁 Script executed:

#!/bin/bash
# Check if totalActiveStake is referenced anywhere in the function
rg -n "totalActiveStake" --type go -B 5 -A 5

Length of output: 3583


🏁 Script executed:

#!/bin/bash
# Look for AdaPots struct definition to understand the type
rg -n "type AdaPots" --type go -A 10

Length of output: 740


🏁 Script executed:

#!/bin/bash
# Check for any guards or validation logic around reserve calculations
rg -n "Reserves.*-" --type go -B 3 -A 3 ledger/common/

Length of output: 861


Fix unused parameter and guard against reserve underflow in CalculateAdaPots

The totalActiveStake parameter at line 135 is unused and will cause a Go compilation error. Additionally, line 175 lacks underflow protection: if expansion.Uint64() exceeds currentPots.Reserves, the uint64 subtraction silently wraps around, corrupting the reserves value.

-func CalculateAdaPots(
-    currentPots AdaPots,
-    params RewardParameters,
-    totalActiveStake uint64,
-    epochFees uint64,
-    totalBlocksInEpoch uint32,
-) AdaPots {
+func CalculateAdaPots(
+    currentPots AdaPots,
+    params RewardParameters,
+    _ uint64, // totalActiveStake (currently unused)
+    epochFees uint64,
+    totalBlocksInEpoch uint32,
+) AdaPots {
@@
-    newPots := AdaPots{
-        Reserves: currentPots.Reserves - expansion.Uint64(),
-        Treasury: currentPots.Treasury + treasuryContribution.Uint64(),
-        Rewards:  rewardPot.Uint64(),
-    }
+    expansionU64 := expansion.Uint64()
+    reserves := currentPots.Reserves
+    if expansionU64 > reserves {
+        expansionU64 = reserves
+    }
+    newPots := AdaPots{
+        Reserves: reserves - expansionU64,
+        Treasury: currentPots.Treasury + treasuryContribution.Uint64(),
+        Rewards:  rewardPot.Uint64(),
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// CalculateAdaPots calculates the ADA pots for the next epoch
func CalculateAdaPots(
currentPots AdaPots,
params RewardParameters,
totalActiveStake uint64,
epochFees uint64,
totalBlocksInEpoch uint32,
) AdaPots {
// Calculate eta (pool performance factor)
eta := calculateEta(totalBlocksInEpoch, params)
// Calculate monetary expansion (rho * eta * reserves)
// rho is in millionths (3000 = 0.3%)
monetaryExpansion := new(big.Int).SetUint64(params.MonetaryExpansion)
reserves := new(big.Int).SetUint64(currentPots.Reserves)
expansion := new(big.Int).Mul(monetaryExpansion, reserves)
expansion.Div(
expansion,
new(big.Int).SetUint64(1000000),
) // Convert from millionths
// Apply eta factor
etaBig := eta
expansion = new(big.Int).Mul(expansion, etaBig.Num())
expansion.Div(expansion, etaBig.Denom())
// Add epoch fees to reward pot
rewardPot := new(big.Int).SetUint64(epochFees)
rewardPot.Add(rewardPot, expansion)
// Calculate treasury contribution (tau * (expansion + fees))
// tau is in ten-thousandths (2000 = 20%)
treasuryGrowth := new(big.Int).SetUint64(params.TreasuryGrowth)
treasuryContribution := new(big.Int).Mul(treasuryGrowth, rewardPot)
treasuryContribution.Div(
treasuryContribution,
new(big.Int).SetUint64(10000),
) // Convert from ten-thousandths
// Subtract treasury contribution from reward pot
rewardPot.Sub(rewardPot, treasuryContribution)
// Update pots
newPots := AdaPots{
Reserves: currentPots.Reserves - expansion.Uint64(),
Treasury: currentPots.Treasury + treasuryContribution.Uint64(),
Rewards: rewardPot.Uint64(),
}
return newPots
}
// CalculateAdaPots calculates the ADA pots for the next epoch
func CalculateAdaPots(
currentPots AdaPots,
params RewardParameters,
_ uint64, // totalActiveStake (currently unused)
epochFees uint64,
totalBlocksInEpoch uint32,
) AdaPots {
// Calculate eta (pool performance factor)
eta := calculateEta(totalBlocksInEpoch, params)
// Calculate monetary expansion (rho * eta * reserves)
// rho is in millionths (3000 = 0.3%)
monetaryExpansion := new(big.Int).SetUint64(params.MonetaryExpansion)
reserves := new(big.Int).SetUint64(currentPots.Reserves)
expansion := new(big.Int).Mul(monetaryExpansion, reserves)
expansion.Div(
expansion,
new(big.Int).SetUint64(1000000),
) // Convert from millionths
// Apply eta factor
etaBig := eta
expansion = new(big.Int).Mul(expansion, etaBig.Num())
expansion.Div(expansion, etaBig.Denom())
// Add epoch fees to reward pot
rewardPot := new(big.Int).SetUint64(epochFees)
rewardPot.Add(rewardPot, expansion)
// Calculate treasury contribution (tau * (expansion + fees))
// tau is in ten-thousandths (2000 = 20%)
treasuryGrowth := new(big.Int).SetUint64(params.TreasuryGrowth)
treasuryContribution := new(big.Int).Mul(treasuryGrowth, rewardPot)
treasuryContribution.Div(
treasuryContribution,
new(big.Int).SetUint64(10000),
) // Convert from ten-thousandths
// Subtract treasury contribution from reward pot
rewardPot.Sub(rewardPot, treasuryContribution)
// Update pots
expansionU64 := expansion.Uint64()
reserves := currentPots.Reserves
if expansionU64 > reserves {
expansionU64 = reserves
}
newPots := AdaPots{
Reserves: reserves - expansionU64,
Treasury: currentPots.Treasury + treasuryContribution.Uint64(),
Rewards: rewardPot.Uint64(),
}
return newPots
}
🤖 Prompt for AI Agents
In ledger/common/rewards.go around lines 131–181: the parameter totalActiveStake
is unused (causes a compile error) and reserves subtraction can underflow if
expansion > currentPots.Reserves. Remove totalActiveStake from the function
signature (and update all call sites) or else rename it to _ if you
intentionally want to keep it, and replace the uint64 subtraction with a safe
big.Int-based subtraction: convert currentPots.Reserves to a big.Int, compute
reservesMinusExpansion = reservesBig.Sub(reservesBig, expansion) and if
reservesMinusExpansion is negative set new reserves to 0, otherwise set
newPots.Reserves to reservesMinusExpansion.Uint64(); keep other fields assigned
from the computed big.Int results.

Comment on lines 183 to 221
// calculateEta calculates the pool performance factor (η)
// Following Amaru's approach: eta = min(1, blocks_produced / (epoch_length * active_slot_coeff))
func calculateEta(totalBlocksInEpoch uint32, params RewardParameters) *big.Rat {
// Calculate expected blocks: epoch_length * active_slot_coeff
expectedBlocks := new(big.Rat).Mul(
new(big.Rat).SetUint64(uint64(params.ExpectedSlotsPerEpoch)),
params.ActiveSlotsCoeff,
)

// eta = min(1, totalBlocksInEpoch / expectedBlocks)
actualBlocks := new(big.Rat).SetUint64(uint64(totalBlocksInEpoch))
eta := new(big.Rat).Quo(actualBlocks, expectedBlocks)

// Cap at 1.0
oneRat := new(big.Rat).SetInt64(1)
if eta.Cmp(oneRat) > 0 {
eta = oneRat
}

return eta
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Handle zero/invalid ExpectedSlotsPerEpoch or ActiveSlotsCoeff in calculateEta

calculateEta assumes:

  • params.ExpectedSlotsPerEpoch > 0
  • params.ActiveSlotsCoeff != nil and not zero

If either is zero/nil, expectedBlocks becomes zero or the Mul uses a nil *big.Rat, and eta := actualBlocks / expectedBlocks risks invalid math (division by zero) or a panic depending on big.Rat behavior.

I’d defensively guard:

func calculateEta(totalBlocksInEpoch uint32, params RewardParameters) *big.Rat {
-    // Calculate expected blocks: epoch_length * active_slot_coeff
-    expectedBlocks := new(big.Rat).Mul(
-        new(big.Rat).SetUint64(uint64(params.ExpectedSlotsPerEpoch)),
-        params.ActiveSlotsCoeff,
-    )
+    if params.ActiveSlotsCoeff == nil || params.ExpectedSlotsPerEpoch == 0 {
+        // Fall back to eta = 1.0 if config is missing/invalid
+        return new(big.Rat).SetInt64(1)
+    }
+    // Calculate expected blocks: epoch_length * active_slot_coeff
+    expectedBlocks := new(big.Rat).Mul(
+        new(big.Rat).SetUint64(uint64(params.ExpectedSlotsPerEpoch)),
+        params.ActiveSlotsCoeff,
+    )
@@
-    eta := new(big.Rat).Quo(actualBlocks, expectedBlocks)
+    if expectedBlocks.Sign() == 0 {
+        return new(big.Rat).SetInt64(1)
+    }
+    eta := new(big.Rat).Quo(actualBlocks, expectedBlocks)

I'll verify the concerns raised in this review comment by examining the codebase structure and understanding the actual constraints on the parameters.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find RewardParameters struct definition
rg -nP 'type\s+RewardParameters\s+(struct|interface)' --type=go -A 10

Length of output: 731


🏁 Script executed:

#!/bin/bash
# Find where ExpectedSlotsPerEpoch is set/initialized
rg -nP 'ExpectedSlotsPerEpoch\s*[:=]' --type=go -B 2 -A 2

Length of output: 1822


🏁 Script executed:

#!/bin/bash
# Find where ActiveSlotsCoeff is set/initialized
rg -nP 'ActiveSlotsCoeff\s*[:=]' --type=go -B 2 -A 2

Length of output: 2744


🏁 Script executed:

#!/bin/bash
# Find all calls to calculateEta to understand usage context
rg -nP 'calculateEta\s*\(' --type=go -B 3 -A 3

Length of output: 3862


🏁 Script executed:

#!/bin/bash
# Search for validation logic in the rewards package
rg -nP 'func.*[Vv]alidate.*Param' --type=go -A 10

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Get the complete RewardParameters struct definition
rg -nP 'type\s+RewardParameters\s+struct' --type=go -A 20

Length of output: 1197


🏁 Script executed:

#!/bin/bash
# Find where CalculateAdaPots is called - this is the production caller
rg -nP 'CalculateAdaPots\s*\(' --type=go -B 5 -A 5

Length of output: 3675


🏁 Script executed:

#!/bin/bash
# Look for constructor functions or factory patterns for RewardParameters
rg -nP 'func.*RewardParameters|NewRewardParameters' --type=go -A 8

Length of output: 659


🏁 Script executed:

#!/bin/bash
# Search for any initialization or setup of RewardParameters outside tests
rg -nP 'RewardParameters\s*{' --type=go | grep -v _test.go

Length of output: 64


🏁 Script executed:

#!/bin/bash
# Check if there are any tests that specifically test zero or nil cases
rg -nP 'ExpectedSlotsPerEpoch.*0|ActiveSlotsCoeff.*nil' --type=go

Length of output: 418



---

🏁 Script executed:

```shell
#!/bin/bash
# Get full RewardParameters struct - need to see ActiveSlotsCoeff and ExpectedSlotsPerEpoch fields
rg -nP 'type\s+RewardParameters\s+struct' --type=go -A 40

Length of output: 2259


🏁 Script executed:

#!/bin/bash
# Examine the production initialization in rewards_service.go
cat ledger/common/rewards_service.go | sed -n '45,75p'

Length of output: 713


🏁 Script executed:

#!/bin/bash
# Check if ActiveSlotsCoeff and ExpectedSlotsPerEpoch are explicitly defined in struct
rg -nP '(ActiveSlotsCoeff|ExpectedSlotsPerEpoch)\s*\*?big\.(Rat|Int)' --type=go -B 1 -A 1

Length of output: 157


🏁 Script executed:

#!/bin/bash
# Look for any validation of these parameters
rg -nP 'ActiveSlotsCoeff|ExpectedSlotsPerEpoch' ledger/common/rewards.go | head -20

Length of output: 161






🏁 Script executed:

#!/bin/bash
# Check the full context of rewards_service.go to understand if it's incomplete
cat ledger/common/rewards_service.go

Length of output: 2959


🏁 Script executed:

#!/bin/bash
# Search for where CalculateRewards is defined or if RewardParameters is properly initialized elsewhere
rg -nP 'CalculateRewards|func.*RewardParameters' --type=go -B 2 -A 8

Length of output: 6793


🏁 Script executed:

#!/bin/bash
# Search for any code that sets these fields after initialization
rg -nP 'params\.(ActiveSlotsCoeff|ExpectedSlotsPerEpoch)\s*=' --type=go

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for default values or initialization patterns
rg -nP 'ActiveSlotsCoeff\s*=' ledger/common/rewards.go -B 5 -A 5

Length of output: 0


🌐 Web query:

Go big.Rat Mul behavior with nil pointer

💡 Result:

Short answer: don’t pass nil pointers. (*big.Rat).Mul expects pointer operands and a non-nil receiver; if z, x or y is a nil *big.Rat you will get a runtime nil-pointer dereference (panic). The docs show Mul has signature func (z *Rat) Mul(x, y *Rat) *Rat and the package treats the zero value of a Rat (not a nil *Rat) as 0 — you must allocate a Rat (e.g. new(big.Rat) or big.NewRat) before calling Mul. [1][2]. For the general rule on nil pointer deref panics see the Go nil-pointer behavior. [3]

Sources:

  • math/big package docs (Mul and zero-value note). [1]
  • math/big docs (Denom/zero-value vs uninitialized note). [2]
  • Go nil-pointer dereference explanation. [3]


---

🏁 Script executed:

```shell
#!/bin/bash
# Verify CalculateRewards calls CalculateAdaPots which calls calculateEta
rg -nP 'CalculateAdaPots\s*\(' ledger/common/rewards.go -B 3 -A 3

Length of output: 190


🏁 Script executed:

#!/bin/bash
# Check if calculateEta is called from CalculateAdaPots
sed -n '239,280p' ledger/common/rewards.go

Length of output: 1063


Add defensive guards in calculateEta to handle nil/zero ActiveSlotsCoeff and ExpectedSlotsPerEpoch

Production code in rewards_service.go initializes RewardParameters without setting ActiveSlotsCoeff and ExpectedSlotsPerEpoch, leaving them at their zero values (nil and 0 respectively). The calculateEta function at lines 187–189 calls new(big.Rat).Mul() directly on params.ActiveSlotsCoeff without a nil check. big.Rat.Mul with a nil operand causes a runtime nil-pointer dereference panic. Additionally, if ExpectedSlotsPerEpoch is 0, the resulting expectedBlocks will be zero, leading to division by zero in the Quo call at line 196.

Apply the suggested defensive checks:

func calculateEta(totalBlocksInEpoch uint32, params RewardParameters) *big.Rat {
+    if params.ActiveSlotsCoeff == nil || params.ExpectedSlotsPerEpoch == 0 {
+        // Fall back to eta = 1.0 if config is missing/invalid
+        return new(big.Rat).SetInt64(1)
+    }
     // Calculate expected blocks: epoch_length * active_slot_coeff
     expectedBlocks := new(big.Rat).Mul(
         new(big.Rat).SetUint64(uint64(params.ExpectedSlotsPerEpoch)),
         params.ActiveSlotsCoeff,
     )

     // eta = min(1, totalBlocksInEpoch / expectedBlocks)
     actualBlocks := new(big.Rat).SetUint64(uint64(totalBlocksInEpoch))
+    if expectedBlocks.Sign() == 0 {
+        return new(big.Rat).SetInt64(1)
+    }
     eta := new(big.Rat).Quo(actualBlocks, expectedBlocks)
🤖 Prompt for AI Agents
In ledger/common/rewards.go around lines 183 to 203, add defensive guards before
using params.ActiveSlotsCoeff and params.ExpectedSlotsPerEpoch: if
params.ActiveSlotsCoeff is nil treat it as 0 (use a zero *big.Rat) and if
params.ExpectedSlotsPerEpoch == 0 then short-circuit and return a zero *big.Rat
to avoid division by zero and nil-pointer dereference; otherwise compute
expectedBlocks = ExpectedSlotsPerEpoch * ActiveSlotsCoeff, compute eta =
actualBlocks / expectedBlocks, cap eta at 1 as before, and return the result.

Comment on lines 205 to 258
// calculatePoolPerformance calculates the apparent performance of a pool
// Following Amaru's approach: performance = (pool_blocks / total_blocks) * (total_stake / pool_stake)
// params is unused as performance calculation only requires block production data from snapshot
func calculatePoolPerformance(
poolID PoolKeyHash,
snapshot RewardSnapshot,
_ RewardParameters,
) float64 {
// Get blocks produced by this pool
poolBlocks := snapshot.PoolBlocks[poolID]

// If no total blocks, assume optimal performance
if snapshot.TotalBlocksInEpoch == 0 {
return 1.0
}

// If no blocks produced, performance = 0
if poolBlocks == 0 {
return 0.0
}

// Calculate blocks ratio: pool_blocks / total_blocks
blocksRatio := float64(poolBlocks) / float64(snapshot.TotalBlocksInEpoch)

// Calculate stake ratio: total_stake / pool_stake
poolStake := snapshot.PoolStake[poolID]
stakeRatio := float64(snapshot.TotalActiveStake) / float64(poolStake)

// Performance = blocks_ratio * stake_ratio
performance := blocksRatio * stakeRatio

return performance
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Avoid division by zero for zero-stake pools in calculatePoolPerformance

Here:

poolStake := snapshot.PoolStake[poolID]
stakeRatio := float64(snapshot.TotalActiveStake) / float64(poolStake)

If poolStake == 0 (e.g., a pool entry created but with zero active stake), this becomes TotalActiveStake / 0, yielding +Inf in Go. That propagates into performance and later into reward shares, leading to nonsensical pool weights.

Suggest:

-    poolStake := snapshot.PoolStake[poolID]
-    stakeRatio := float64(snapshot.TotalActiveStake) / float64(poolStake)
+    poolStake := snapshot.PoolStake[poolID]
+    if poolStake == 0 {
+        // No stake in the pool → no meaningful performance
+        return 0.0
+    }
+    stakeRatio := float64(snapshot.TotalActiveStake) / float64(poolStake)

I'll verify this review comment by examining the codebase to confirm the division-by-zero concern and validate whether the suggested fix is necessary.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find the rewards.go file in the ledger common directory
fd -t f rewards.go ledger/common/

Length of output: 24


🏁 Script executed:

#!/bin/bash
# Show the exact code at lines 205-237
sed -n '205,237p' ledger/common/rewards.go

Length of output: 1059


🏁 Script executed:

#!/bin/bash
# Search for calls to calculatePoolPerformance
rg -n 'calculatePoolPerformance' --type go

Length of output: 641


🏁 Script executed:

#!/bin/bash
# Search for how PoolStake map is populated and whether zero entries are possible
rg -n 'PoolStake\[' --type go -C 3

Length of output: 488


🏁 Script executed:

#!/bin/bash
# Check if there's any existing nil or zero-check pattern for pool stakes
rg -n 'poolStake.*== 0' --type go

Length of output: 0


I need to dive deeper into the caller context and how the snapshot is populated to determine if zero-stake entries are possible.


---

🏁 Script executed:

```shell
#!/bin/bash
# Show context around line 356 where calculatePoolPerformance is called
sed -n '340,370p' ledger/common/rewards.go

Length of output: 935


🏁 Script executed:

#!/bin/bash
# Search for PoolStake map initialization and population
rg -n 'PoolStake.*=' --type go -B 2 -A 2 | head -50

Length of output: 2826


🏁 Script executed:

#!/bin/bash
# Look for RewardSnapshot struct definition to understand PoolStake type
rg -n 'type RewardSnapshot' --type go -A 20

Length of output: 1285


🏁 Script executed:

#!/bin/bash
# Check test cases to see if they test zero-stake scenarios
sed -n '410,440p' ledger/common/rewards_validation_test.go

Length of output: 789


🏁 Script executed:

#!/bin/bash
# Check for any pool filtering or guards before calling calculatePoolPerformance
rg -n 'for.*poolID\|for.*pool' ledger/common/rewards.go -A 5 | head -60

Length of output: 0


I need to check where the RewardSnapshot is constructed and whether zero-stake pools can legitimately exist.


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for where RewardSnapshot is created/populated
rg -n 'RewardSnapshot\{' --type go -B 5 -A 10 | head -80

Length of output: 5693


🏁 Script executed:

#!/bin/bash
# Look for NewRewardSnapshot or similar constructor functions
rg -n 'func.*Snapshot|snapshot :=' ledger/common/rewards.go -B 2 -A 5 | head -100

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's filtering of pools or validation before reward calculation
rg -n 'PoolBlocks\[' --type go -B 3 -A 3

Length of output: 1521


🏁 Script executed:

#!/bin/bash
# Search the entire context around calculatePoolShare to see if poolStake is pre-validated
sed -n '345,380p' ledger/common/rewards.go

Length of output: 1191


I need to check where calculatePoolShare is called to understand if there are guards filtering out zero-stake pools upstream.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find all calls to calculatePoolShare
rg -n 'calculatePoolShare' --type go -B 3 -A 3

Length of output: 763


🏁 Script executed:

#!/bin/bash
# Look for the main rewards calculation loop that calls calculatePoolShare
rg -n 'for.*range.*PoolStake\|for.*range.*PoolParams' --type go -B 5 -A 10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Show broader context of where rewards are actually calculated
sed -n '320,360p' ledger/common/rewards.go

Length of output: 970


I need to see the complete loop structure to understand the iteration and guard conditions.


---

🏁 Script executed:

```shell
#!/bin/bash
# Show the full loop around line 274 where calculatePoolShare is called
sed -n '250,290p' ledger/common/rewards.go

Length of output: 959


🏁 Script executed:

#!/bin/bash
# Specifically look for the for loop that iterates over pools
sed -n '240,310p' ledger/common/rewards.go

Length of output: 1980


Fix division-by-zero vulnerability for zero-stake pools in calculatePoolPerformance

The vulnerability is confirmed. The loop at line 268 iterates over snapshot.PoolStake, which can contain zero-valued entries (pools with no active stake). The only guard is a check for PoolParams existence, leaving no protection against poolStake == 0.

When calculatePoolPerformance is called at line 231, it divides by this potentially-zero stake value:

stakeRatio := float64(snapshot.TotalActiveStake) / float64(poolStake)

In Go, division by zero for floats silently returns +Inf, which propagates into the performance metric and distorts reward calculations.

The suggested fix is correct:

poolStake := snapshot.PoolStake[poolID]
if poolStake == 0 {
    // No stake in the pool → no meaningful performance
    return 0.0
}
stakeRatio := float64(snapshot.TotalActiveStake) / float64(poolStake)

This aligns with the existing check at line 224 for zero blocks and correctly treats zero-stake pools as having zero performance.

🤖 Prompt for AI Agents
In ledger/common/rewards.go around lines 205 to 237, calculatePoolPerformance
divides by snapshot.PoolStake[poolID] which can be zero; add a guard after
reading poolStake: if poolStake == 0 { return 0.0 } so zero-stake pools are
treated as zero performance and avoid producing +Inf values; keep the existing
checks for TotalBlocksInEpoch and poolBlocks and then compute stakeRatio and
performance only when poolStake > 0.

Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
@wolf31o2 wolf31o2 force-pushed the feat/initial-rewards branch from f884773 to b3b245f Compare November 19, 2025 03:33
@wolf31o2
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
ledger/common/rewards_service.go (1)

34-83: Good orchestration, but consider documenting hardcoded parameters.

The method correctly orchestrates the reward calculation flow with proper error handling. However, the hardcoded RewardParameters at lines 53-57 should be highlighted with a TODO comment for visibility:

// TODO: Get protocol parameters from ledger state instead of using hardcoded defaults
params := RewardParameters{
    MonetaryExpansion: 3000,             // 0.3%
    TreasuryGrowth:    2000,             // 20%
    PoolInfluence:     big.NewRat(1, 1), // a0 = 1
}

This makes it clearer that parameter sourcing needs to be implemented.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f884773 and b3b245f.

📒 Files selected for processing (6)
  • internal/test/ledger/ledger.go (2 hunks)
  • ledger/common/rewards.go (1 hunks)
  • ledger/common/rewards_service.go (1 hunks)
  • ledger/common/rewards_test.go (1 hunks)
  • ledger/common/rewards_validation_test.go (1 hunks)
  • ledger/common/state.go (1 hunks)
🔇 Additional comments (30)
ledger/common/state.go (1)

41-66: LGTM! Clean interface design.

The RewardState interface is well-structured with clear separation of concerns. The method signatures are intuitive, and embedding it into LedgerState follows idiomatic Go patterns for interface composition.

internal/test/ledger/ledger.go (2)

29-31: LGTM! Mock fields follow conventions.

The new MockAdaPots and MockRewardSnapshot fields are consistent with the existing mock pattern and provide the necessary test data storage.


95-116: LGTM! Mock implementation is appropriate.

The mock methods correctly delegate to production logic where needed (CalculateRewards) and provide sensible test behavior for state queries. The no-op UpdateAdaPots with its explanatory comment is acceptable for a mock.

ledger/common/rewards_test.go (8)

28-69: LGTM! Thorough monetary expansion test.

The test validates the complete ADA pots calculation flow with realistic parameters. The inline comments explaining expected calculations (lines 54-61) make the test easy to verify and maintain.


71-145: LGTM! Comprehensive reward calculation validation.

The test exercises the full reward distribution pipeline with realistic multi-pool scenarios. The assertion at line 141 ensures conservation of rewards, and line 144 validates pot depletion—both critical invariants.


147-159: LGTM! Appropriate float conversion tests.

The test covers key margin values with reasonable float comparison tolerance (InDelta with 0.001).


161-181: LGTM! Good pledge validation coverage.

The test validates both passing and failing pledge scenarios with multi-owner pools, covering the essential validation logic.


183-208: LGTM! Saturation calculation tests look correct.

The test validates both fully-saturated (1.0) and under-saturated (0.2) scenarios with clear expected values.


210-220: LGTM! Useful test helper.

The createGenesisRat helper simplifies test data construction for margin and ratio values, improving test readability.


222-304: LGTM! Solid integration test.

The integration test validates the complete RewardService workflow, including pot updates and reward conservation (lines 292-303). This provides valuable end-to-end coverage.


306-363: LGTM! Complete mock implementation.

The local mockLedgerState provides all necessary interface implementations for integration testing, with appropriate delegation to production CalculateRewards at line 349.

ledger/common/rewards_validation_test.go (7)

25-90: LGTM! Excellent Shelley formula validation.

The test validates reward formulas against the Shelley specification with detailed inline calculations (lines 44-59). This provides a clear reference for the expected behavior and formula correctness.


92-219: LGTM! Thorough reference implementation validation.

The test exercises a realistic 3-pool scenario and validates key invariants: positive rewards, reward pot depletion (line 178), and conservation of total rewards (lines 213-218). The previous assertion issue was addressed in commit b3b245f.


221-336: LGTM! Comprehensive edge case coverage.

The test suite covers critical edge cases: zero distributable rewards (when reward equals cost), high margin scenarios, and single-owner pools. These validate that the formulas handle boundary conditions correctly.


338-370: LGTM! Clean reference formula implementations.

The helper functions provide clear reference implementations of Shelley reward formulas, with proper handling of the edge case when poolReward <= poolCost.


372-507: LGTM! Thorough Java reference validation.

The test suite validates key scenarios from the Java reference implementation (cf-java-rewards-calculation), including eta calculation with various block counts, Amaru-style pool performance, and monetary expansion with eta. This ensures compatibility with established reference implementations.


509-641: LGTM! Excellent Amaru compatibility validation.

The test suite explicitly validates compatibility with Amaru's implementation, documenting key differences (e.g., eta calculation without decentralization dependency, performance not capped at 1.0). The inline formula documentation (e.g., lines 513-514, 546-547) provides valuable context.


643-678: LGTM! Complete monetary expansion validation.

The test validates Amaru's monetary expansion approach end-to-end, ensuring correct calculation of incentives, treasury contributions, and final pot values (lines 674-676).

ledger/common/rewards_service.go (2)

22-32: LGTM! Clean service construction.

The RewardService struct and constructor follow standard Go service patterns with clear dependency injection via LedgerState.


85-112: LGTM! Basic validation logic is appropriate.

The method provides a simple but effective total rewards validation. The comment at lines 90-91 correctly notes that more detailed validation would depend on the specific distribution mechanism (MIR certificates, etc.).

ledger/common/rewards.go (10)

26-62: LGTM! Well-structured core types.

The AdaPots and RewardParameters types are clearly documented with appropriate field types. The unit documentation for rates (lines 36-37) is particularly helpful for understanding the fixed-point representations.


64-106: LGTM! Comprehensive reward snapshot design.

The RewardSnapshot type captures all necessary state for reward calculation. The detailed comments on deregistration timing (lines 85-92) are particularly valuable for understanding the complex Cardano reward rules.


108-130: LGTM! Clean result type hierarchy.

The result types provide a clear hierarchical structure: overall calculation result → per-pool rewards → individual rewards. This makes it easy to navigate and validate reward distributions.


132-190: LGTM! Monetary expansion calculation with proper underflow protection.

The function correctly implements the Cardano monetary expansion formula. The underflow protection at lines 173-181 (added in commit b3b245f) properly handles edge cases where expansion might exceed reserves.


192-221: LGTM! Eta calculation with defensive guards.

The function properly implements Amaru's efficiency-based eta calculation. The defensive guards at lines 195-202 (added in commit b3b245f) protect against nil pointers and division by zero, ensuring robustness.


223-258: LGTM! Pool performance calculation with complete edge case handling.

The function correctly implements Amaru's pool performance formula with proper guards for edge cases: no total blocks (line 235), no pool blocks (line 240), and zero pool stake (lines 249-251, added in commit b3b245f).


260-380: LGTM! Comprehensive reward calculation with robust guards.

The main reward calculation function properly handles all edge cases identified in previous reviews. The guards for empty pools (lines 303-305) and zero total share (lines 307-315) prevent division by zero and ensure fair fallback behavior. The rounding adjustment (lines 332-342) ensures exact distribution.


382-424: LGTM! Pool share calculation with defensive guards.

The function implements the leader stake influence formula correctly with appropriate defensive guards for nil parameters (lines 401-404) and potential division issues (lines 413-419).


426-504: LGTM! Correct Shelley reward distribution.

The function properly implements the Shelley formula for distributing pool rewards between operators and delegators. The check for stake registration (line 478) ensures only eligible participants receive rewards, and the rounding remainder is correctly assigned to the operator (lines 495-497).


506-580: LGTM! Solid utility function implementations.

The utility functions provide correct implementations with appropriate edge case handling:

  • marginFloat: proper nil/zero checks (lines 509-516)
  • CalculateOptimalPoolCount: simplified implementation with clear TODO (line 525)
  • ValidatePoolPledge: correct pledge validation logic
  • CalculatePoolSaturation: appropriate handling of zero denominators (lines 569-576)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@wolf31o2 wolf31o2 merged commit d95b170 into main Nov 19, 2025
10 checks passed
@wolf31o2 wolf31o2 deleted the feat/initial-rewards branch November 19, 2025 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants