Skip to content

fix(cardano): improve reward logic accuracy - #745

Merged
scarmuega merged 2 commits into
mainfrom
fix/reward-adjustments
Oct 4, 2025
Merged

fix(cardano): improve reward logic accuracy#745
scarmuega merged 2 commits into
mainfrom
fix/reward-adjustments

Conversation

@scarmuega

@scarmuega scarmuega commented Oct 4, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Distinguishes spendable vs unspendable rewards and tracks registered accounts to ensure correct reward assignment.
    • Exposes treasury tax, rewards, unspendable rewards, and blocks minted in outputs.
  • Improvements

    • Higher-precision reward and pot calculations for more accurate epoch accounting.
    • Pool registration now updates or initializes pool data more robustly; reward allocation respects pledge and performance.
  • CLI

    • Updated table headers/rows to show treasury tax, rewards, unspendable rewards, blocks minted, and streamlined stake columns.

@coderabbitai

coderabbitai Bot commented Oct 4, 2025

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Walkthrough

Bootstraps pots using PParams-derived pot delta and BigRational math; restructures EpochState reward fields; removes PoolAccountDetected and adjusts pool registration; reworks sweep reward accounting to track registered accounts and unspendable rewards; updates CLI dumps to new fields.

Changes

Cohort / File(s) Summary
Genesis bootstrapping and pots
crates/cardano/src/genesis/mod.rs, crates/cardano/src/pots.rs
bootstrap_pots now accepts &PParamsSet, computes genesis utxos and pot delta (tau/rho/eta → compute_pot_delta), derives reserves/treasury from pot delta; Ratio switched to BigRational with helper macros and updated floor/cast usage.
Model and pool/delta surface
crates/cardano/src/model.rs, crates/cardano/src/roll/pools.rs
Removed PoolAccountDetected from CardanoDelta and related delta plumbing; AccountState::is_registered replaces is_active; EpochState fields renamed/added (rewards_to_distributeeffective_rewards, rewards_to_treasuryunspendable_rewards, treasury_tax added); pool registration now mutates or constructs PoolState directly without PoolAccountDetected.
Sweep: compute/loading/state
crates/cardano/src/sweep/compute.rs, crates/cardano/src/sweep/loading.rs, crates/cardano/src/sweep/mod.rs
BoundaryWork gains registered_accounts; starting/ending state now carry effective_rewards, unspendable_rewards, and treasury_tax; added update_ending_state and epoch-2 special-case; loading tracks registered accounts.
Sweep: rewards and visitor
crates/cardano/src/sweep/rewards.rs
Rewrote reward math to use BigRational and optimal_pool_rewards2; validates pledge, splits spendable vs unspendable rewards; BoundaryVisitor gains unspendable_rewards and visit_pool_leader now takes &StakeCredential.
Sweep: transitions
crates/cardano/src/sweep/transition.rs
Added debug log when setting pool stake in PoolTransition::apply; otherwise behavior unchanged.
CLI state/log dump updates
src/bin/dolos/data/dump_logs.rs, src/bin/dolos/data/dump_state.rs
Table headers and row mappings updated to use treasury_tax, effective_rewards, unspendable_rewards, updated stake/epoch formatting and new format_epoch_value helper.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Node
  participant Genesis
  participant PParams
  participant Pots
  participant PotDelta

  Node->>Genesis: read genesis utxos and max_supply
  Node->>PParams: load tau, rho, eta
  Node->>Pots: bootstrap_pots(protocol, genesis, &pparams)
  Pots->>PotDelta: compute_pot_delta(utxos, tau, rho, eta)
  PotDelta-->>Pots: incentives/available_rewards/treasury_tax
  Pots-->>Node: Pots{utxos, reserves, treasury}
Loading
sequenceDiagram
  autonumber
  participant Sweep as SweepEngine
  participant Visitor as BoundaryVisitor
  participant Pools
  participant Accounts

  Sweep->>Pools: iterate pool leaders and delegators
  Pools-->>Visitor: visit_pool_leader(pool, &StakeCredential, op_share)
  Visitor->>Accounts: check if reward account is registered
  alt registered
    Visitor->>Visitor: add to effective_rewards (spendable)
  else unregistered
    Visitor->>Visitor: add to unspendable_rewards
  end
  Sweep->>Sweep: update_ending_state(effective, unspendable)
  Sweep->>Sweep: define_starting_state(genesis, effective, unspendable)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

I hop through epochs, counting tau and rho,
BigRationals hum where the carrots grow.
Pools shed their coats—no more detected name,
Rewards split tidy, some spendable, some tame.
I tidy treasury, then munch a stack of code-carrots 🥕

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 succinctly indicates that this PR fixes Cardano logic by improving the accuracy of reward calculations, which aligns with the extensive updates to reward, pot, and distribution code shown in the changeset. It is concise, clear, and directly related to the primary purpose of the pull request.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 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 be1806c and 07e4706.

📒 Files selected for processing (2)
  • crates/cardano/src/sweep/compute.rs (3 hunks)
  • src/bin/dolos/data/dump_state.rs (6 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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/cardano/src/genesis/mod.rs (1)

27-59: Do not apply pot delta during genesis bootstrap

Lines 46‑58 call compute_pot_delta with eta = 1, then credit pot_delta.treasury_tax to the treasury while shaving the same amount off reserves. At bootstrap no epoch has finished, so the ledger hasn’t minted incentives or treasury tax yet—treasury should stay at the genesis value (typically 0) and reserves should remain max_supply - utxos. This change artificially mints funds into treasury and misaligns the starting state with real chain history.

Please restore the prior behaviour: derive initial_reserves from the genesis file, keep treasury at its configured genesis balance, and leave reward-pot accounting to the first real epoch transition.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c92b7d and be1806c.

📒 Files selected for processing (11)
  • crates/cardano/src/genesis/mod.rs (4 hunks)
  • crates/cardano/src/model.rs (4 hunks)
  • crates/cardano/src/pots.rs (4 hunks)
  • crates/cardano/src/roll/pools.rs (2 hunks)
  • crates/cardano/src/sweep/compute.rs (3 hunks)
  • crates/cardano/src/sweep/loading.rs (3 hunks)
  • crates/cardano/src/sweep/mod.rs (2 hunks)
  • crates/cardano/src/sweep/rewards.rs (11 hunks)
  • crates/cardano/src/sweep/transition.rs (1 hunks)
  • src/bin/dolos/data/dump_logs.rs (2 hunks)
  • src/bin/dolos/data/dump_state.rs (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
crates/cardano/src/sweep/loading.rs (2)
crates/cardano/src/model.rs (5)
  • new (69-76)
  • new (245-259)
  • new (403-419)
  • new (762-767)
  • new (1068-1078)
crates/cardano/src/roll/accounts.rs (5)
  • new (26-32)
  • new (102-112)
  • new (155-161)
  • new (199-205)
  • new (247-257)
crates/cardano/src/genesis/mod.rs (2)
crates/cardano/src/model.rs (6)
  • tau (832-834)
  • rho (836-838)
  • from (1099-1101)
  • from (1105-1107)
  • from (1111-1113)
  • from (1117-1120)
crates/cardano/src/pots.rs (1)
  • compute_pot_delta (119-148)
crates/cardano/src/roll/pools.rs (1)
crates/cardano/src/model.rs (6)
  • roll (932-950)
  • new (69-76)
  • new (245-259)
  • new (403-419)
  • new (762-767)
  • new (1068-1078)
crates/cardano/src/pots.rs (1)
crates/cardano/src/model.rs (2)
  • d (840-842)
  • tau (832-834)
crates/cardano/src/sweep/compute.rs (3)
crates/core/src/lib.rs (1)
  • genesis (670-670)
src/adapters.rs (1)
  • genesis (61-63)
crates/cardano/src/model.rs (1)
  • incentives (1015-1021)
src/bin/dolos/data/dump_state.rs (2)
src/bin/dolos/data/dump_logs.rs (4)
  • header (25-25)
  • header (30-32)
  • header (48-63)
  • header (86-99)
crates/cardano/src/model.rs (1)
  • live_stake (275-281)
crates/cardano/src/sweep/rewards.rs (2)
crates/cardano/src/model.rs (6)
  • k (824-826)
  • a0 (828-830)
  • from (1099-1101)
  • from (1105-1107)
  • from (1111-1113)
  • from (1117-1120)
crates/cardano/src/pallas_extras.rs (1)
  • pool_reward_account (304-307)
⏰ 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

Comment on lines +62 to +91
if let Some(entity) = entity {
entity.vrf_keyhash = self.cert.vrf_keyhash;
entity.reward_account = self.cert.reward_account.to_vec();
entity.pool_owners = self.cert.pool_owners.clone();
entity.relays = self.cert.relays.clone();
entity.declared_pledge = self.cert.pledge;
entity.margin_cost = self.cert.margin.clone();
entity.fixed_cost = self.cert.cost;
entity.metadata = self.cert.pool_metadata.clone();
} else {
let state = PoolState {
register_slot: self.slot,
vrf_keyhash: self.cert.vrf_keyhash,
reward_account: self.cert.reward_account.to_vec(),
pool_owners: self.cert.pool_owners.clone(),
relays: self.cert.relays.clone(),
declared_pledge: self.cert.pledge,
margin_cost: self.cert.margin.clone(),
fixed_cost: self.cert.cost,
metadata: self.cert.pool_metadata.clone(),
total_stake: EpochValue::new(0, self.epoch),
blocks_minted_total: 0,
blocks_minted_epoch: 0,
retiring_epoch: None,
is_retired: false,
deposit: self.pool_deposit,
};

*entity = Some(state);
}

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

Re-registering pools never restores deposit or clears retirement flags

When re-applying a registration for an existing pool, we clone over the certificate fields but leave the retirement state and deposit untouched. After a prior retirement (see PoolDeRegistration::apply on Line 165), entity.deposit is set to 0 and entity.retiring_epoch remains Some(...). Because the re-registration branch on Line 62 never resets those fields, the pool stays marked as retiring with no deposit locked, which breaks reward and deposit accounting. Please reset the lifecycle fields (slot, retiring flags, deposit) when updating an existing pool.

Suggested fix:

         if let Some(entity) = entity {
+            entity.register_slot = self.slot;
             entity.vrf_keyhash = self.cert.vrf_keyhash;
             entity.reward_account = self.cert.reward_account.to_vec();
             entity.pool_owners = self.cert.pool_owners.clone();
             entity.relays = self.cert.relays.clone();
             entity.declared_pledge = self.cert.pledge;
             entity.margin_cost = self.cert.margin.clone();
             entity.fixed_cost = self.cert.cost;
             entity.metadata = self.cert.pool_metadata.clone();
+            entity.retiring_epoch = None;
+            entity.is_retired = false;
+            entity.deposit = self.pool_deposit;
         } else {
📝 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 let Some(entity) = entity {
entity.vrf_keyhash = self.cert.vrf_keyhash;
entity.reward_account = self.cert.reward_account.to_vec();
entity.pool_owners = self.cert.pool_owners.clone();
entity.relays = self.cert.relays.clone();
entity.declared_pledge = self.cert.pledge;
entity.margin_cost = self.cert.margin.clone();
entity.fixed_cost = self.cert.cost;
entity.metadata = self.cert.pool_metadata.clone();
} else {
let state = PoolState {
register_slot: self.slot,
vrf_keyhash: self.cert.vrf_keyhash,
reward_account: self.cert.reward_account.to_vec(),
pool_owners: self.cert.pool_owners.clone(),
relays: self.cert.relays.clone(),
declared_pledge: self.cert.pledge,
margin_cost: self.cert.margin.clone(),
fixed_cost: self.cert.cost,
metadata: self.cert.pool_metadata.clone(),
total_stake: EpochValue::new(0, self.epoch),
blocks_minted_total: 0,
blocks_minted_epoch: 0,
retiring_epoch: None,
is_retired: false,
deposit: self.pool_deposit,
};
*entity = Some(state);
}
if let Some(entity) = entity {
entity.register_slot = self.slot;
entity.vrf_keyhash = self.cert.vrf_keyhash;
entity.reward_account = self.cert.reward_account.to_vec();
entity.pool_owners = self.cert.pool_owners.clone();
entity.relays = self.cert.relays.clone();
entity.declared_pledge = self.cert.pledge;
entity.margin_cost = self.cert.margin.clone();
entity.fixed_cost = self.cert.cost;
entity.metadata = self.cert.pool_metadata.clone();
entity.retiring_epoch = None;
entity.is_retired = false;
entity.deposit = self.pool_deposit;
} else {
let state = PoolState {
register_slot: self.slot,
vrf_keyhash: self.cert.vrf_keyhash,
reward_account: self.cert.reward_account.to_vec(),
pool_owners: self.cert.pool_owners.clone(),
relays: self.cert.relays.clone(),
declared_pledge: self.cert.pledge,
margin_cost: self.cert.margin.clone(),
fixed_cost: self.cert.cost,
metadata: self.cert.pool_metadata.clone(),
total_stake: EpochValue::new(0, self.epoch),
blocks_minted_total: 0,
blocks_minted_epoch: 0,
retiring_epoch: None,
is_retired: false,
deposit: self.pool_deposit,
};
*entity = Some(state);
}
🤖 Prompt for AI Agents
In crates/cardano/src/roll/pools.rs around lines 62 to 91, the re-registration
branch updates certificate fields but fails to reset lifecycle fields so a
previously retired pool keeps retiring flags and a zero deposit; update the
existing entity to also reset lifecycle state by setting entity.register_slot =
self.slot, entity.retiring_epoch = None, entity.is_retired = false, and
entity.deposit = self.pool_deposit (optionally reset entity.blocks_minted_epoch
= 0 if you want epoch counters cleared) so re-registration fully restores the
pool lifecycle and deposit.

Comment thread crates/cardano/src/sweep/compute.rs Outdated
Comment thread src/bin/dolos/data/dump_state.rs
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