Skip to content

refactor(cardano): move reward calc into its own module - #746

Merged
scarmuega merged 2 commits into
mainfrom
refactor/reward-mod
Oct 5, 2025
Merged

refactor(cardano): move reward calc into its own module#746
scarmuega merged 2 commits into
mainfrom
refactor/reward-mod

Conversation

@scarmuega

@scarmuega scarmuega commented Oct 5, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added pool and delegator reward calculations, including operator (owner) share.
    • Improved numeric precision via ratio-based arithmetic helpers.
  • Refactor

    • Unified reward logic into a shared rewards module used across the system.
    • Standardized conversion helpers for consistent arithmetic.
  • User Interface

    • dump_state table: added margin cost column (numerator/denominator), removed legacy pool fields, and updated epoch value formatting for stake/pool pairs.

@coderabbitai

coderabbitai Bot commented Oct 5, 2025

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Walkthrough

Adds public modules math_macros and rewards; introduces exported macros for BigRational helpers; centralizes pool reward calculations in crates/cardano/src/rewards.rs; refactors pots and sweep reward call sites to use new macros and rewards API; adjusts snapshot/stake field names and a CLI dump formatting macro.

Changes

Cohort / File(s) Summary
Module wiring
crates/cardano/src/lib.rs
Exposes new public modules: math_macros and rewards.
Rational math macros
crates/cardano/src/math_macros.rs
Adds exported macros: ratio!, floor_int!, pallas_ratio! using num_bigint/num_rational.
Pots math refactor
crates/cardano/src/pots.rs
Replaces legacy BigInt/ratio conversions with ratio!/pallas_ratio! in calculate_eta and compute_pot_delta (no public signatures changed).
Centralized rewards API
crates/cardano/src/rewards.rs
New rewards module: public type aliases (Ratio, TotalPoolReward, OperatorShare) and functions delegator_reward, pool_rewards, pool_operator_share plus internal helpers and tests.
Sweep rewards delegation
crates/cardano/src/sweep/rewards.rs
Removes local reward helpers/types and delegates to crate::rewards::{delegator_reward,pool_rewards,pool_operator_share}; uses pallas_ratio!; logs live_pledge.
Snapshot struct & loading
crates/cardano/src/sweep/mod.rs
crates/cardano/src/sweep/loading.rs
Renames Snapshot.total_stake -> total_stake_sum; adds active_stake_sum; renames track_account -> track_registered_account; loading now skips unregistered accounts and populates registered_accounts.
CLI dump formatting
src/bin/dolos/data/dump_state.rs
Changes format_epoch_value! macro signature to take (stake, pool) expressions; updates call sites; removes legacy pool columns; adds margin_cost column formatted as "numerator/denominator".

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as Node runtime
  participant Sweep as sweep::BoundaryVisitor
  participant Macros as math_macros (ratio!/pallas_ratio!)
  participant Rewards as crate::rewards

  User->>Sweep: visit_pool(...)
  Sweep->>Macros: pallas_ratio!(a0), pallas_ratio!(d), pallas_ratio!(pool.margin_cost)
  Sweep->>Rewards: pool_rewards(epoch_rewards, circulating_supply, _total_stake, active_stake, pool_stake, declared_pledge, live_pledge, k, a0, d, pool_blocks, epoch_blocks)
  Rewards->>Rewards: optimal_pool_rewards(...), pool_apparent_performance(...)
  Rewards-->>Sweep: TotalPoolReward (u64)

  Sweep->>Rewards: pool_operator_share(pool_rewards, fixed_cost, margin_cost, pool_stake, live_pledge, circulating_supply)
  Rewards-->>Sweep: OperatorShare (u64)

  Sweep->>Rewards: delegator_reward(available_rewards, total_delegated, delegator_stake)
  Rewards-->>Sweep: DelegatorReward (u64)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit scribbles on a slate so fine,
Ratios hop in tidy line.
Pledge and margin, stake and block,
Rewards now measured—tic and tock.
Hooray! The ledger hums in time. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely describes the primary change of extracting reward calculation logic into a dedicated module within the Cardano crate, matching the addition of the new rewards module and related refactoring without including extraneous details.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56e2149 and dce67cc.

📒 Files selected for processing (4)
  • crates/cardano/src/rewards.rs (1 hunks)
  • crates/cardano/src/sweep/loading.rs (6 hunks)
  • crates/cardano/src/sweep/mod.rs (1 hunks)
  • crates/cardano/src/sweep/rewards.rs (4 hunks)

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
crates/cardano/src/rewards.rs (2)

209-237: Address the ignored test or remove it.

This test is marked #[ignore], so it won't run in CI. Either:

  1. Fix the test to assert expected values deterministically, or
  2. Remove it if it was used only for development/discovery

The current design (iterating until a match is found, then panicking otherwise) is unusual for a unit test.


189-238: Add test coverage for the public API.

The public functions delegator_reward (line 67) and pool_rewards (line 104) lack tests. Given that these are financial calculations, comprehensive test coverage is recommended to catch edge cases and regressions.

Consider adding tests for:

  • delegator_reward: zero stake, zero total, normal cases
  • pool_rewards: underpledged pools (returns 0), normal cases, edge cases with k, d thresholds
  • pool_operator_share: current ignored test plus additional edge cases
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b3937d7 and 56e2149.

📒 Files selected for processing (6)
  • crates/cardano/src/lib.rs (2 hunks)
  • crates/cardano/src/math_macros.rs (1 hunks)
  • crates/cardano/src/pots.rs (4 hunks)
  • crates/cardano/src/rewards.rs (1 hunks)
  • crates/cardano/src/sweep/rewards.rs (4 hunks)
  • src/bin/dolos/data/dump_state.rs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
crates/cardano/src/rewards.rs (1)
crates/cardano/src/model.rs (3)
  • d (840-842)
  • k (824-826)
  • a0 (828-830)
src/bin/dolos/data/dump_state.rs (1)
crates/cardano/src/model.rs (1)
  • live_stake (275-281)
crates/cardano/src/lib.rs (1)
crates/minibf/src/routes/accounts.rs (1)
  • rewards (550-573)
crates/cardano/src/pots.rs (1)
crates/cardano/src/model.rs (3)
  • d (840-842)
  • rho (836-838)
  • tau (832-834)
crates/cardano/src/sweep/rewards.rs (2)
crates/cardano/src/rewards.rs (3)
  • delegator_reward (67-70)
  • pool_rewards (104-134)
  • pool_operator_share (150-187)
crates/cardano/src/model.rs (3)
  • d (840-842)
  • k (824-826)
  • a0 (828-830)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check Build
🔇 Additional comments (2)
crates/cardano/src/rewards.rs (2)

7-61: LGTM: Optimal pool rewards calculation is well-implemented.

The function correctly implements the Cardano optimal pool reward formula using exact rational arithmetic. The use of min() to cap relative stakes prevents oversaturation, and floor_int! ensures proper integer conversion.


72-101: LGTM: Pool apparent performance calculation is correct and defensive.

The function properly guards against division by zero (lines 80, 86, 91) and correctly implements the Cardano apparent performance formula with decentralization threshold logic.

Comment on lines +67 to +70
pub fn delegator_reward(available_rewards: u64, total_delegated: u64, delegator_stake: u64) -> u64 {
let share = (delegator_stake as f64 / total_delegated as f64) * available_rewards as f64;
share.round() as u64
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Replace floating-point with exact rational arithmetic.

Floating-point arithmetic introduces rounding errors and non-determinism in financial calculations. This is inconsistent with the rest of the module, which uses BigRational for exact arithmetic.

Apply this diff to use exact rational math:

 pub fn delegator_reward(available_rewards: u64, total_delegated: u64, delegator_stake: u64) -> u64 {
-    let share = (delegator_stake as f64 / total_delegated as f64) * available_rewards as f64;
-    share.round() as u64
+    if total_delegated == 0 {
+        return 0;
+    }
+    let share = ratio!(delegator_stake, total_delegated) * ratio!(available_rewards);
+    floor_int!(share, u64)
 }
📝 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
pub fn delegator_reward(available_rewards: u64, total_delegated: u64, delegator_stake: u64) -> u64 {
let share = (delegator_stake as f64 / total_delegated as f64) * available_rewards as f64;
share.round() as u64
}
pub fn delegator_reward(available_rewards: u64, total_delegated: u64, delegator_stake: u64) -> u64 {
if total_delegated == 0 {
return 0;
}
let share = ratio!(delegator_stake, total_delegated) * ratio!(available_rewards);
floor_int!(share, u64)
}
🤖 Prompt for AI Agents
In crates/cardano/src/rewards.rs around lines 67-70, replace the f64-based
computation with exact rational arithmetic: import num_bigint::BigInt (or
BigUint) and num_rational::BigRational, convert available_rewards,
total_delegated and delegator_stake into BigInt, compute share_r =
BigRational::new(BigInt::from(available_rewards) *
BigInt::from(delegator_stake), BigInt::from(total_delegated)) (handle
total_delegated == 0), perform correct rounding to the nearest integer by adding
BigRational::from_integer(BigInt::from(1)) /
BigRational::from_integer(BigInt::from(2)) then taking to_integer(), and finally
convert that BigInt to u64 (using try_from/to_u64 and handle overflow or
conversion failure); remove any f64 usage and ensure required crates are added
to Cargo.toml.

Comment on lines +117 to +121
if live_pledge < declared_pledge {
return 0;
}

assert!(k > 0, "k must be > 0");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Major: Replace panic-inducing assert! with graceful error handling.

The assert! on line 121 will panic in production if k is 0. For a public API, this is a runtime hazard. Either validate k at the calling boundary or return 0/early-exit.

Apply this diff to handle k=0 gracefully:

-    assert!(k > 0, "k must be > 0");
+    if k == 0 {
+        return 0;
+    }

Alternatively, document the precondition clearly in the function's doc comment if panicking is intentional.

📝 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
if live_pledge < declared_pledge {
return 0;
}
assert!(k > 0, "k must be > 0");
if live_pledge < declared_pledge {
return 0;
}
if k == 0 {
return 0;
}
🤖 Prompt for AI Agents
In crates/cardano/src/rewards.rs around lines 117 to 121, the current assert!(k
> 0, "k must be > 0") can panic in production; replace it with graceful handling
by checking if k == 0 and returning 0 (or an appropriate error result) instead
of panicking — validate the input at the function boundary and return early when
k is zero, or alternatively document the precondition in the function doc
comment if you purposely want callers to ensure k>0.

Comment on lines +150 to +187
pub fn pool_operator_share(
pool_rewards: u64,
fixed_cost: u64,
margin_cost: Ratio,
pool_stake: u64,
live_pledge: u64,
circulating_supply: u64,
) -> u64 {
let c = fixed_cost;

if pool_rewards <= c {
return pool_rewards; // operator takes it all if rewards ≤ fixed cost
}

let after_cost = pool_rewards - c;

let s = ratio!(live_pledge, circulating_supply);
let σ = ratio!(pool_stake, circulating_supply);

// s/σ — ratio of owner's pledge to pool stake (denominator cancels, so we can
// use amounts)
let s_over_sigma = s / σ;

let one = ratio!(1);

let m = margin_cost;

// c + (f̂ − c) · ( m + (1 − m) · s/σ )
let term = &m + (one - &m) * s_over_sigma;

let after_cost = ratio!(after_cost);

let variable = after_cost * term;

let variable = floor_int!(variable, u64);

c + variable
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Major: Guard against division by zero in ratio construction.

Lines 166-167 construct ratios with circulating_supply as denominator without checking for zero. Line 171 divides by σ, which is zero if pool_stake is zero. Both scenarios would cause division by zero.

Apply this diff to add defensive guards:

 pub fn pool_operator_share(
     pool_rewards: u64,
     fixed_cost: u64,
     margin_cost: Ratio,
     pool_stake: u64,
     live_pledge: u64,
     circulating_supply: u64,
 ) -> u64 {
+    if circulating_supply == 0 || pool_stake == 0 {
+        return pool_rewards;
+    }
+
     let c = fixed_cost;
📝 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
pub fn pool_operator_share(
pool_rewards: u64,
fixed_cost: u64,
margin_cost: Ratio,
pool_stake: u64,
live_pledge: u64,
circulating_supply: u64,
) -> u64 {
let c = fixed_cost;
if pool_rewards <= c {
return pool_rewards; // operator takes it all if rewards ≤ fixed cost
}
let after_cost = pool_rewards - c;
let s = ratio!(live_pledge, circulating_supply);
let σ = ratio!(pool_stake, circulating_supply);
// s/σ — ratio of owner's pledge to pool stake (denominator cancels, so we can
// use amounts)
let s_over_sigma = s / σ;
let one = ratio!(1);
let m = margin_cost;
// c + (f̂ − c) · ( m + (1 − m) · s/σ )
let term = &m + (one - &m) * s_over_sigma;
let after_cost = ratio!(after_cost);
let variable = after_cost * term;
let variable = floor_int!(variable, u64);
c + variable
}
pub fn pool_operator_share(
pool_rewards: u64,
fixed_cost: u64,
margin_cost: Ratio,
pool_stake: u64,
live_pledge: u64,
circulating_supply: u64,
) -> u64 {
// Prevent division-by-zero in ratio! and subsequent division
if circulating_supply == 0 || pool_stake == 0 {
return pool_rewards;
}
let c = fixed_cost;
if pool_rewards <= c {
return pool_rewards; // operator takes it all if rewards ≤ fixed cost
}
let after_cost = pool_rewards - c;
let s = ratio!(live_pledge, circulating_supply);
let σ = ratio!(pool_stake, circulating_supply);
// s/σ — ratio of owner's pledge to pool stake (denominator cancels, so we can
// use amounts)
let s_over_sigma = s / σ;
let one = ratio!(1);
let m = margin_cost;
// c + (f̂ − c) · ( m + (1 − m) · s/σ )
let term = &m + (one - &m) * s_over_sigma;
let after_cost = ratio!(after_cost);
let variable = after_cost * term;
let variable = floor_int!(variable, u64);
c + variable
}
🤖 Prompt for AI Agents
In crates/cardano/src/rewards.rs around lines 150–187, guard the ratio
construction and division to avoid divide-by-zero: if circulating_supply == 0
return pool_rewards immediately (can't compute ratios); otherwise build s and σ
using circulating_supply as before but if σ is zero (pool_stake == 0) set
s_over_sigma to zero (or Ratio::zero()) instead of performing s / σ; keep the
remaining math unchanged and ensure any conversions from integers to Ratio
happen after these checks.

Comment on lines 269 to 296
@@ -454,6 +290,7 @@ impl super::BoundaryVisitor for BoundaryVisitor {
%epoch_rewards,
%total_pool_reward,
%operator_share,
%live_pledge,
"computed pool rewards"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Use total stake snapshot instead of circulating supply
crate::rewards::pool_rewards (see crates/cardano/src/rewards.rs, lines 103‑133) expects its second argument to be the E‑2 total registered stake so it can derive σ and s with the correct denominator. Feeding circulating_supply shrinks both ratios dramatically, which causes optimal_pool_rewards to underpay every pool. Please pass the actual snapshot total you already track in ctx.active_snapshot (the same number that backs pool_stake), rather than the network-wide supply figure.

🤖 Prompt for AI Agents
In crates/cardano/src/sweep/rewards.rs around lines 260–281, the call sites pass
the network-wide `circulating_supply` into `crate::rewards::pool_rewards`
(second parameter) and `crate::rewards::pool_operator_share` (last parameter),
but those functions expect the E‑2 total registered stake snapshot; replace
`circulating_supply` with the snapshot total you already track in
`ctx.active_snapshot` (the same total that backs `pool_stake`) at both call
sites so σ and s are computed with the correct denominator.

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.

1 participant