fix(cardano): improve reward logic accuracy - #745
Conversation
|
Caution Review failedThe pull request is closed. WalkthroughBootstraps 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
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}
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)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
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. Comment |
There was a problem hiding this comment.
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 bootstrapLines 46‑58 call
compute_pot_deltawitheta = 1, then creditpot_delta.treasury_taxto 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 remainmax_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_reservesfrom 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
📒 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
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
Summary by CodeRabbit
New Features
Improvements
CLI