Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@scarmuega
Copy link
Member

@scarmuega scarmuega commented Oct 20, 2025

Summary by CodeRabbit

  • New Features

    • Introduced Lovelace type for monetary amounts, improving type safety.
    • Added new delta types (AccountTransition, PoolTransition, EpochTransition, PoolWrapUp) for granular state transitions.
  • Bug Fixes & Improvements

    • Restructured core data model for better epoch state consistency.
    • Refactored account and pool state management with enhanced transition tracking.
    • Improved rewards calculation and parameter access patterns.

@coderabbitai
Copy link

coderabbitai bot commented Oct 20, 2025

Walkthrough

This PR restructures Cardano's epoch transition and entity delta system. It refactors EpochValue to track live and scheduled next values, introduces a TransitionDefault trait for default behavior, reorganizes protocol parameter access patterns throughout the codebase, restructures AccountState with a new Stake struct, updates Pots to use a Lovelace type alias, consolidates delta types (removing snapshot.rs), and changes pool iteration APIs to separate pool enumeration from parameter retrieval.

Changes

Cohort / File(s) Summary
Core Data Model Refactoring
crates/cardano/src/model.rs
Introduces TransitionDefault trait; refactors EpochValue<T> with live/next/mark/set/go fields and new constructors (new, with_live, with_scheduled, with_genesis); adds Stake struct with utxo_sum, rewards_sum, withdrawals_sum; restructures AccountState to use EpochValue<Stake>, EpochValue<PoolHash>, EpochValue<DRep>; removes is_pending from PoolSnapshot; introduces Lovelace = u64 type alias; renames EpochReset to EpochTransition in CardanoDelta; adds PoolWrapUp variant to CardanoDelta.
Monetary Types & Pots
crates/cardano/src/pots.rs
Changes Pots and PotDelta fields from u64 to Lovelace for all monetary amounts; adds pool_count, account_count, deposit_per_pool, deposit_per_account, nominal_deposits fields; replaces check_consistency() with is_consistent() and assert_consistency().
Protocol Parameter Access
crates/cardano/src/estart/nonces.rs, crates/cardano/src/ewrap/commit.rs, crates/cardano/src/genesis/mod.rs, crates/cardano/src/lib.rs
Replaces era_transition() with pparams.era_transition(); changes active() calls to unwrap_live(); updates get_utxo_amount return type to Lovelace; adjusts reserve calculation from saturating_sub to direct subtraction.
Epoch Transition Delta System
crates/cardano/src/estart/reset.rs
Introduces AccountTransition, PoolTransition, EpochTransition as distinct EntityDelta types; renames define_next_pots to define_new_pots; removes define_next_pparams; refactors BoundaryVisitor to accumulate deltas via visit_account and visit_pool methods.
Epoch Boundary & Wrapup
crates/cardano/src/ewrap/wrapup.rs
Introduces PoolWrapUp delta; adds migration: Option<PParamsSet> field to EpochWrapUp; reworks BoundaryVisitor with deltas: Vec<CardanoDelta> and change() helper; adds define_pparams_migration() helper for era transitions.
Retirement & Delegate Drops
crates/cardano/src/ewrap/retires.rs
Simplifies PoolDelegatorDrop.prev_pool from Option<EpochValue<...>> to Option<PoolHash>; introduces DRepExpiration::new() constructor; introduces PoolDepositRefund public struct and EntityDelta impl; updates DRepDelegatorDrop.prev_drep to store Option<DRep> directly; adds visit_pool() logic to emit PoolDepositRefund delta.
Rewards & Pool Iteration API
crates/cardano/src/rewards/mod.rs, crates/cardano/src/rewards/mocking.rs
Changes iter_all_pools() signature from impl Iterator<Item = (PoolHash, &PoolParams)> to impl Iterator<Item = PoolHash>; adds pool_params(pool: PoolHash) -> &PoolParams method to RewardsContext trait; adds Display impl for RewardMap<C>; updates define_rewards() to fetch params separately.
Entity Snapshot Module Removal
crates/cardano/src/ewrap/snapshot.rs
Removed entire module including AccountTransition, PoolTransition, BoundaryVisitor types that were previously defined here.
Module Exports
crates/cardano/src/ewrap/mod.rs
Removes pub mod snapshot export.
Boundary Loading Logic
crates/cardano/src/ewrap/loading.rs
Updates load_pool_data to use pool.snapshot.mark(); reworks should_expire_drep to use pparams.unwrap_live(); removes visitor_snapshot and per-entity visit_* calls; defaults visitor_wrapup instead.
Reward Delta Types
crates/cardano/src/estart/rewards.rs
Removes public PoolDepositRefund type and its EntityDelta impl; changes reward updates to use entity.stake.unwrap_live_mut(); adds input validation with early returns.
Stake/Account Transaction Rolling
crates/cardano/src/roll/accounts.rs
Simplifies StakeDelegation.prev_pool, VoteDelegation.prev_drep, StakeDeregistration fields from nested EpochValue to direct types; updates undo logic to use reset() instead of unwrap patterns; adjusts UTXO accounting via entity.stake.unwrap_live_mut().
Epoch State & Stats Tracking
crates/cardano/src/roll/epochs.rs
Replaces separate utxo_produced/utxo_consumed with single utxo_delta: i64 in EpochStatsUpdate; adds registered_pools: HashSet<PoolHash>; updates PParamsUpdate.apply to use scheduled_or_default(); reworks EpochStateVisitor.visit_tx to use collateral-aware fee calculation and accumulate via utxo_delta.
Pool State & Snapshot Mutations
crates/cardano/src/roll/pools.rs
Changes pool parameter scheduling to use entity.snapshot.schedule() instead of direct live mutation; replaces EpochValue::new() with EpochValue::with_live(); updates minted blocks via unwrap_live_mut().
Rupd/Rewards Module
crates/cardano/src/rupd/loading.rs, crates/cardano/src/rupd/mod.rs
Consolidates pparams access via unwrap_live(); adds Display impl for StakeSnapshot in loading.rs, removes it in mod.rs; updates pool iteration and snapshot handling per new RewardsContext API; changes RupdWork.pparams initialization to use mark().
MiniBF Epoch/Account Routes
crates/minibf/src/lib.rs, crates/minibf/src/routes/epochs/mod.rs, crates/minibf/src/routes/accounts.rs, crates/minibf/src/routes/pools.rs
Replaces active().clone() with live().cloned().unwrap_or_default(); refactors AccountContent to derive stake values from account_state.stake.live(); updates pool snapshot and params access with safer optional chains; adds defaults for missing data.
Debug Output & Dump Tools
src/bin/dolos/data/dump_logs.rs, src/bin/dolos/data/dump_state.rs
Refactors EpochState::row to cache pparams and rolling locals; updates field access chains to use as_ref().map() patterns; replaces "pending" pool columns with "pledge" columns; adds next-epoch pledge value display.

Sequence Diagram

sequenceDiagram
    participant OldFlow as Old: Direct Mutation
    participant NewFlow as New: Delta-Based
    participant Epoch as Epoch State
    participant Stake as Stake/Account
    participant Pool as Pool/Snapshot
    
    Note over OldFlow,Epoch: Previous Approach
    OldFlow->>Epoch: mutate epoch.pparams directly
    OldFlow->>Stake: set rewards_sum field directly
    OldFlow->>Pool: mutate snapshot.live_mut() in-place
    
    Note over NewFlow,Epoch: New Approach
    NewFlow->>NewFlow: create AccountTransition delta
    NewFlow->>NewFlow: create PoolTransition delta
    NewFlow->>NewFlow: create EpochTransition delta
    NewFlow->>Epoch: apply all deltas atomically
    NewFlow->>Stake: schedule via stake.schedule()
    NewFlow->>Pool: schedule via snapshot.schedule()
    
    Note over Epoch: Transition happens via transition_unchecked()
    Epoch->>Epoch: move next→live, clear mark/set/go
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Rationale: This is a foundational refactoring affecting the core data model (EpochValue with new multi-state tracking), entity delta system (new types: AccountTransition, PoolTransition, EpochTransition, PoolWrapUp), and pervasive protocol parameter access patterns across 20+ files. Changes are heterogeneous—spanning data structure redesigns, API signature changes, state transition semantics, and interconnected boundary/reward logic updates. High logic density in model.rs and delta implementations, coupled with wide-ranging cascading updates throughout the codebase, demands comprehensive cross-file reasoning.

Possibly related PRs

Poem

🐰 A rabbit's ode to refactoring:
Old states would mutate without a care,
Now EpochValues schedule their affair—
Live and next in tandem gleam,
Deltas flow through every seam,
Transitions atomic, smooth and fair! ✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The PR title "fix(cardano): use correct timing for pool params activation" references real changes present in the changeset—the PR does include systematic updates to parameter access patterns (changing from .active() to .unwrap_live() and related timing adjustments). However, the title severely understates the scope and primary nature of the changes. The changeset includes a comprehensive architectural refactoring across multiple high-effort areas: introduction of the TransitionDefault trait, complete rework of EpochValue with new scheduling semantics, restructuring of the entire delta and boundary system, removal of the snapshot module, redefinition of core data structures (Stake, AccountState, PoolSnapshot), and extensive changes to state transition logic. A teammate scanning the repository history would not understand from this title that a major model refactoring has occurred; instead, they would expect a narrow, localized fix to pool parameter timing. This misrepresents the PR's scope and significance. Consider revising the title to better reflect the primary scope of this PR. A more accurate title might focus on the core refactoring effort, such as "refactor(cardano): restructure epoch transitions and parameter access patterns" or "refactor(cardano): redesign EpochValue and delta system for better state management." This would set appropriate expectations for reviewers about the breadth and significance of the changes involved.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.80% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 fix/pool-params-timing

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.

Copy link

@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: 16

Caution

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

⚠️ Outside diff range comments (2)
crates/cardano/src/ewrap/commit.rs (1)

40-47: Avoid panic on missing live pparams during era switch

unwrap_live() will abort the process if live pparams are absent; return a typed error instead to preserve chain progress.

Apply:

-        let pparams = self.ending_state().pparams.unwrap_live();
+        let pparams = self
+            .ending_state()
+            .pparams
+            .live()
+            .ok_or_else(|| ChainError::from(BrokenInvariant::BadBootstrap))?;

No other logic changes needed; ensure ensure_epoch_length()/ensure_slot_length() remain fallible as-is.

crates/cardano/src/roll/pools.rs (1)

120-122: Undo path is unimplemented (panic). Implement reversible logic.

todo!() in EntityDelta::undo will panic during reorgs. Capture and restore prior state.

Suggested approach:

  • Store previous scheduled “next” snapshot before apply.
  • On undo, restore that value; if this delta created the entity, delete it.

Example diff (fields + logic):

@@
 pub struct PoolRegistration {
     cert: MultiEraPoolRegistration,
     slot: BlockSlot,
     epoch: Epoch,
     pool_deposit: u64,
     // undo
     is_new: Option<bool>,
+    prev_next: Option<PoolSnapshot>,
 }
@@
     fn apply(&mut self, entity: &mut Option<PoolState>) {
         if let Some(entity) = entity {
@@
-            entity.snapshot.schedule(
+            // save undo
+            self.prev_next = entity.snapshot.next().cloned();
+            entity.snapshot.schedule(
                 self.epoch,
                 Some(PoolSnapshot {
                     is_retired: false,
                     blocks_minted: 0,
                     params: self.cert.clone().into(),
                 }),
             );
         } else {
@@
             *entity = Some(state);
         }
     }
@@
-    fn undo(&self, _entity: &mut Option<PoolState>) {
-        todo!()
+    fn undo(&self, entity: &mut Option<PoolState>) {
+        if self.is_new == Some(true) {
+            // registration created a new pool; undo by removing it
+            *entity = None;
+            return;
+        }
+        if let Some(entity) = entity {
+            // restore previously scheduled snapshot
+            entity
+                .snapshot
+                .schedule(self.epoch, self.prev_next.clone());
+        }
     }
🧹 Nitpick comments (28)
crates/minibf/src/routes/epochs/mod.rs (2)

35-38: Avoid silently defaulting protocol params; prefer explicit error or a deliberate fallback.

Using unwrap_or_default() can emit a zeroed/default parameter set, which may misrepresent the network. Consider returning 404/500 when no live params exist, or use a documented fallback (e.g., current effective params) with an explicit comment.
Also, params use live() while nonce still reads active; please confirm this mismatch is intentional for epoch timing.


60-66: Same defaulting concern for historical parameters.

by_number_parameters now also defaults to an empty param set when live() is None. Prefer explicit error or a documented fallback to avoid serving incorrect values. Verify if nonce should also be based on live() here for consistency.

crates/minibf/src/lib.rs (1)

123-126: Semantics check: is prior_epoch.live() the correct source for “effective in epoch E”?

If logs capture both “live” and “scheduled next,” retrieving effective params for epoch E via prior_epoch.live() may be off by one depending on model semantics. Please confirm whether prior.next (or equivalent) is the intended source. Also, avoid unwrap_or_default() here to prevent silently returning bogus params; prefer an explicit error or well-documented fallback.

crates/minibf/src/routes/pools.rs (2)

69-94: Default numeric strings to "0" instead of empty string.

declared_pledge and fixed_cost currently default to "". Prefer "0" to match typical Blockfrost numeric-as-string conventions and avoid surprising clients.

Apply this diff:

-            declared_pledge: params
-                .as_ref()
-                .map(|x| x.pledge.to_string())
-                .unwrap_or_default(),
+            declared_pledge: params
+                .as_ref()
+                .map(|x| x.pledge.to_string())
+                .unwrap_or_else(|| "0".to_string()),

-            fixed_cost: params
-                .as_ref()
-                .map(|x| x.cost.to_string())
-                .unwrap_or_default(),
+            fixed_cost: params
+                .as_ref()
+                .map(|x| x.cost.to_string())
+                .unwrap_or_else(|| "0".to_string()),

120-136: Retired filter may include pools with missing snapshots.

state.snapshot.live().map(|x| x.is_retired).unwrap_or(false) treats “no snapshot” as “not retired,” potentially surfacing pools lacking a live snapshot. If that state is possible, consider excluding entries without a snapshot or making the default explicit in a comment.

crates/cardano/src/roll/accounts.rs (1)

100-100: Remove unused field.

The prev_deposit field is declared but never assigned in the apply method and is not used in undo. This appears to be dead code left over from the refactoring.

Apply this diff to remove the unused field:

-    prev_deposit: Option<u64>,

Also remove it from the constructor at line 112:

         prev_registered_at: None,
         prev_deregistered_at: None,
-        prev_deposit: None,
crates/cardano/src/model.rs (1)

456-469: Defensive arithmetic to avoid underflow in Stake.

total() and withdrawable() subtract on u64; if invariants are violated (e.g., withdrawals_sum > rewards_sum), values wrap. Add saturating math or debug assertions.

 impl Stake {
-    pub fn total(&self) -> u64 {
-        let mut out = self.utxo_sum;
-        out += self.rewards_sum;
-        out -= self.withdrawals_sum;
-        out
-    }
+    pub fn total(&self) -> u64 {
+        self.utxo_sum
+            .saturating_add(self.rewards_sum)
+            .saturating_sub(self.withdrawals_sum)
+    }
 
-    pub fn withdrawable(&self) -> u64 {
-        let mut out = self.rewards_sum;
-        out -= self.withdrawals_sum;
-        out
-    }
+    pub fn withdrawable(&self) -> u64 {
+        self.rewards_sum.saturating_sub(self.withdrawals_sum)
+    }
 }
crates/cardano/src/genesis/mod.rs (1)

47-48: Guard against underflow when computing reserves.

Plain subtraction can wrap if utxos > max_supply in malformed genesis. Prefer checked or saturating subtraction.

-    let reserves = max_supply - utxos;
+    let reserves = max_supply.saturating_sub(utxos);
+    // Alternatively, use checked_sub to error out:
+    // let reserves = max_supply
+    //     .checked_sub(utxos)
+    //     .ok_or_else(|| ChainError::GenesisFieldMissing("inconsistent supply: utxos > max".into()))?;

Please confirm desired behavior (clamp vs. error) for invalid genesis.

crates/cardano/src/ewrap/commit.rs (1)

33-47: Small invariant check for new EraSummary

Optionally assert previous.end is set after define_end to catch regressions early.

-        let new = EraSummary {
+        debug_assert!(previous.end.is_some(), "EraSummary end not defined");
+        let new = EraSummary {
             start: previous.end.clone().unwrap(),
crates/cardano/src/rupd/mod.rs (1)

177-178: Commented debug print—fine to keep

No functional impact. Consider a trace! with feature-gate if you plan to re-enable.

crates/cardano/src/ewrap/loading.rs (1)

32-35: Improve retire log context

Log which pool/operator is being retired to aid debugging.

-                info!("retiring pool");
+                info!(operator = %pool.operator, "retiring pool");
crates/cardano/src/rewards/mocking.rs (2)

320-324: Make missing params error explicit (avoid bare unwrap)

unwrap() will panic without context if a pool hash is absent. Prefer expect with key context, or return Option and let callers decide.

-    fn pool_params(&self, pool: PoolHash) -> &PoolParams {
-        self.pool_params_converted
-            .get(&hex::encode(pool.as_ref()))
-            .unwrap()
-    }
+    fn pool_params(&self, pool: PoolHash) -> &PoolParams {
+        self.pool_params_converted
+            .get(&hex::encode(pool.as_ref()))
+            .expect("missing mock pool_params for given PoolHash")
+    }

Alternatively, change the trait to return Option<&PoolParams> if broader callers can handle absence.


65-113: Mock reward account uses mainnet stake tag (0xe1)

Just noting: hardcoded to mainnet stake address semantics; fine for tests. If test vectors intend preview/preprod, consider parameterizing.

crates/cardano/src/estart/rewards.rs (1)

44-46: Undo path should be safe from underflow

Use saturating_sub (or checked_sub with debug_assert) to avoid accidental underflow if deltas are replayed out of order.

-        let stake = entity.stake.unwrap_live_mut();
-        stake.rewards_sum -= self.reward;
+        if let Some(stake) = entity.stake.live_mut() {
+            stake.rewards_sum = stake.rewards_sum.saturating_sub(self.reward);
+        } else {
+            warn!("missing live stake when undoing rewards");
+        }
crates/cardano/src/rewards/mod.rs (3)

155-164: Display for RewardMap is minimal—consider adding account context

Currently prints only amounts; adding the stake credential (or pool split) would improve diagnostics, but fine if intentionally terse.


327-415: Avoid O(n) owner lookups per delegator

owners.contains(&delegator) is linear; convert owners to a HashSet once to reduce complexity in large pools.

-        let owners = pool_params
-            .pool_owners
-            .iter()
-            .map(|owner| pallas_extras::keyhash_to_stake_cred(*owner))
-            .collect::<Vec<_>>();
+        let owners: std::collections::HashSet<_> = pool_params
+            .pool_owners
+            .iter()
+            .map(|owner| pallas_extras::keyhash_to_stake_cred(*owner))
+            .collect();
...
-        for delegator in ctx.pool_delegators(pool) {
-            if owners.contains(&delegator) {
+        for delegator in ctx.pool_delegators(pool) {
+            if owners.contains(&delegator) {
                 continue;
             }

350-366: Parameter fetches: consolidate to avoid repeated pparams() calls

Minor: cache pparams once per loop to avoid repeated virtual calls.

-        let k = ctx.pparams().ensure_k()?;
-        let a0 = ctx.pparams().ensure_a0()?;
-        let d = ctx.pparams().ensure_d()?;
+        let pparams = ctx.pparams();
+        let k = pparams.ensure_k()?;
+        let a0 = pparams.ensure_a0()?;
+        let d = pparams.ensure_d()?;
crates/cardano/src/roll/pools.rs (1)

107-111: Fix comment typo and grammar.

“udpate its live snapshot” → “update its live snapshot”.

-// please note that new pools will udpate its live snapshot directly. This differs
+// please note that new pools will update their live snapshot directly. This differs
crates/cardano/src/pots.rs (1)

46-52: Guard against u64 overflow in obligations().

Multiplying deposit_per_* by *_count and summing as u64 can overflow on large inputs.

Use u128 intermediates and clamp on conversion:

-    pub fn obligations(&self) -> Lovelace {
-        let pool_deposits = self.deposit_per_pool * self.pool_count;
-        let account_deposits = self.deposit_per_account * self.account_count;
-
-        Lovelace::from(self.nominal_deposits + pool_deposits + account_deposits)
-    }
+    pub fn obligations(&self) -> Lovelace {
+        let pool_deposits = (self.deposit_per_pool as u128) * (self.pool_count as u128);
+        let account_deposits = (self.deposit_per_account as u128) * (self.account_count as u128);
+        let nominal = self.nominal_deposits as u128;
+        let sum = nominal
+            .saturating_add(pool_deposits)
+            .saturating_add(account_deposits);
+        Lovelace::from(sum as u64)
+    }
crates/cardano/src/ewrap/wrapup.rs (2)

1-5: Remove unused imports to satisfy warnings.

Strip unused AccountId, AccountState, PoolSnapshot, and Epoch.

-use crate::{
-    ewrap::{AccountId, PoolId},
-    AccountState, CardanoDelta, EndStats, EpochState, FixedNamespace as _, PParamsSet,
-    PoolSnapshot, PoolState, CURRENT_EPOCH_KEY,
-};
+use crate::{
+    ewrap::PoolId, CardanoDelta, EndStats, EpochState, FixedNamespace as _, PParamsSet, PoolState,
+    CURRENT_EPOCH_KEY,
+};
-use pallas::ledger::primitives::Epoch;

28-35: Be defensive on missing pool state (prefer warn-and-return).

Using expect("existing pool") will panic; consider logging and returning to keep boundary processing resilient.

-        let entity = entity.as_mut().expect("existing pool");
+        let Some(entity) = entity.as_mut() else {
+            tracing::warn!("missing pool during wrap-up; skipping");
+            return;
+        };
crates/cardano/src/ewrap/retires.rs (2)

40-47: Fix log message in ProposalExpiration::undo.

Warn says “missing pool” but this delta targets proposals.

-            warn!("missing pool");
+            warn!("missing proposal");

115-122: Message nit: replace “existing account” with “existing drep”.

Clarity improvement for DRepExpiration.

-        let entity = entity.as_mut().expect("existing account");
+        let entity = entity.as_mut().expect("existing drep");
crates/cardano/src/rupd/loading.rs (2)

59-76: Avoid potential panic: prefer non-panicking pparams access or guard Byron/None before unwrap.

Using epoch.pparams.unwrap_live() can panic if live params are absent. Guard with mark() and bail out early (neutral pot) or Byron check before using ensure_*.

-    let pparams = epoch.pparams.unwrap_live();
+    let Some(pparams) = epoch.pparams.mark() else {
+        info!("no pparams available for epoch; neutral incentives");
+        return Ok(neutral_incentives());
+    };
+    if pparams.is_byron() {
+        info!("no pot changes during Byron epoch");
+        return Ok(neutral_incentives());
+    }
-    if pparams.is_byron() {
-        info!("no pot changes during Byron epoch");
-        return Ok(neutral_incentives());
-    }

Please confirm unwrap_live cannot occur in Byron/edge epochs, otherwise this is required.


195-208: Display should not unwrap pool_params; handle missing gracefully.

Even with the insertion fix, defensive formatting avoids panics during debug output.

-        for (pool, blocks) in self.pool_blocks.iter() {
-            let pparams = self.pool_params.get(pool).unwrap();
-            writeln!(f, "| {pool} | {blocks} | {} |", pparams.pledge)?;
-        }
+        for (pool, blocks) in self.pool_blocks.iter() {
+            if let Some(pparams) = self.pool_params.get(pool) {
+                writeln!(f, "| {pool} | {blocks} | {} |", pparams.pledge)?;
+            } else {
+                writeln!(f, "| {pool} | {blocks} | <missing params> |")?;
+            }
+        }
crates/cardano/src/estart/reset.rs (3)

30-42: Undo paths are todo; confirm not required for current flows.

AccountTransition::undo left as todo!(). If rollbacks/rewinds can reach these deltas, this will crash.

Add a tracked issue or implement minimal reversible state if rewinds are possible in estart.


62-73: PoolTransition undo missing; verify no rewind usage.

Same concern as accounts; please confirm or stub with a safe no-op if acceptable.


102-129: Prefer non-panicking accessors for rolling live values.

Using unwrap_live() for rolling assumes presence at boundary. If absence is possible on early epochs, use mark() or expect(...) with context.

-    let rolling = epoch.rolling.unwrap_live();
+    let rolling = epoch
+        .rolling
+        .mark()
+        .expect("rolling stats should be live at epoch boundary");

Also confirm epoch.end is always Some at this stage; otherwise expect(...) with a clearer message.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between becdeda and 645bb2e.

📒 Files selected for processing (26)
  • crates/cardano/src/estart/nonces.rs (1 hunks)
  • crates/cardano/src/estart/reset.rs (2 hunks)
  • crates/cardano/src/estart/rewards.rs (2 hunks)
  • crates/cardano/src/ewrap/commit.rs (2 hunks)
  • crates/cardano/src/ewrap/loading.rs (3 hunks)
  • crates/cardano/src/ewrap/mod.rs (0 hunks)
  • crates/cardano/src/ewrap/retires.rs (10 hunks)
  • crates/cardano/src/ewrap/snapshot.rs (0 hunks)
  • crates/cardano/src/ewrap/wrapup.rs (4 hunks)
  • crates/cardano/src/genesis/mod.rs (5 hunks)
  • crates/cardano/src/lib.rs (1 hunks)
  • crates/cardano/src/model.rs (19 hunks)
  • crates/cardano/src/pots.rs (9 hunks)
  • crates/cardano/src/rewards/mocking.rs (1 hunks)
  • crates/cardano/src/rewards/mod.rs (4 hunks)
  • crates/cardano/src/roll/accounts.rs (9 hunks)
  • crates/cardano/src/roll/epochs.rs (7 hunks)
  • crates/cardano/src/roll/pools.rs (3 hunks)
  • crates/cardano/src/rupd/loading.rs (6 hunks)
  • crates/cardano/src/rupd/mod.rs (1 hunks)
  • crates/minibf/src/lib.rs (1 hunks)
  • crates/minibf/src/routes/accounts.rs (1 hunks)
  • crates/minibf/src/routes/epochs/mod.rs (2 hunks)
  • crates/minibf/src/routes/pools.rs (4 hunks)
  • src/bin/dolos/data/dump_logs.rs (1 hunks)
  • src/bin/dolos/data/dump_state.rs (5 hunks)
💤 Files with no reviewable changes (2)
  • crates/cardano/src/ewrap/mod.rs
  • crates/cardano/src/ewrap/snapshot.rs
🧰 Additional context used
🧬 Code graph analysis (18)
crates/cardano/src/estart/nonces.rs (1)
crates/cardano/src/model.rs (1)
  • era_transition (1007-1020)
crates/cardano/src/genesis/mod.rs (2)
crates/cardano/src/pots.rs (1)
  • max_supply (54-56)
crates/cardano/src/model.rs (4)
  • with_genesis (183-192)
  • with_live (161-170)
  • epoch (195-200)
  • protocol_major (1113-1115)
crates/cardano/src/rewards/mod.rs (2)
crates/cardano/src/rupd/loading.rs (3)
  • fmt (196-207)
  • iter_all_pools (302-304)
  • pool_params (306-308)
crates/cardano/src/rewards/mocking.rs (2)
  • iter_all_pools (314-318)
  • pool_params (320-324)
crates/cardano/src/ewrap/commit.rs (3)
crates/cardano/src/rewards/mocking.rs (1)
  • pparams (340-342)
crates/cardano/src/rewards/mod.rs (1)
  • pparams (309-309)
crates/cardano/src/rupd/loading.rs (1)
  • pparams (317-321)
crates/cardano/src/ewrap/loading.rs (3)
crates/cardano/src/rewards/mocking.rs (1)
  • pparams (340-342)
crates/cardano/src/rewards/mod.rs (1)
  • pparams (309-309)
crates/cardano/src/rupd/loading.rs (1)
  • pparams (317-321)
src/bin/dolos/data/dump_state.rs (2)
crates/cardano/src/model.rs (13)
  • total (456-462)
  • go (77-82)
  • go (215-217)
  • set (71-76)
  • set (219-221)
  • set (1081-1089)
  • mark (65-70)
  • mark (223-225)
  • live (203-205)
  • epoch (195-200)
  • protocol_major (1113-1115)
  • len (1065-1067)
  • next (227-229)
crates/cardano/src/pots.rs (1)
  • obligations (47-52)
crates/minibf/src/routes/pools.rs (1)
crates/cardano/src/model.rs (1)
  • live_stake (525-527)
src/bin/dolos/data/dump_logs.rs (2)
crates/cardano/src/model.rs (2)
  • protocol_major (1113-1115)
  • len (1065-1067)
crates/cardano/src/pots.rs (1)
  • obligations (47-52)
crates/cardano/src/rupd/loading.rs (2)
crates/cardano/src/rewards/mocking.rs (3)
  • pparams (340-342)
  • iter_all_pools (314-318)
  • pool_params (320-324)
crates/cardano/src/rewards/mod.rs (4)
  • pparams (309-309)
  • iter_all_pools (306-306)
  • pool_params (307-307)
  • pool_params (333-337)
crates/cardano/src/pots.rs (3)
crates/cardano/src/rewards/mocking.rs (1)
  • pots (246-248)
crates/cardano/src/rewards/mod.rs (1)
  • pots (296-296)
crates/cardano/src/rupd/loading.rs (1)
  • pots (274-276)
crates/cardano/src/rewards/mocking.rs (2)
crates/cardano/src/rewards/mod.rs (3)
  • iter_all_pools (306-306)
  • pool_params (307-307)
  • pool_params (333-337)
crates/cardano/src/rupd/loading.rs (2)
  • iter_all_pools (302-304)
  • pool_params (306-308)
crates/cardano/src/roll/accounts.rs (3)
crates/cardano/src/model.rs (1)
  • undo (1774-1808)
crates/cardano/src/roll/pools.rs (3)
  • undo (120-122)
  • undo (145-150)
  • undo (199-204)
crates/cardano/src/roll/dreps.rs (3)
  • undo (70-76)
  • undo (123-129)
  • undo (166-170)
crates/cardano/src/roll/epochs.rs (3)
crates/cardano/src/model.rs (2)
  • next (227-229)
  • undo (1774-1808)
crates/core/src/state.rs (2)
  • next (224-234)
  • undo (163-163)
crates/cardano/src/roll/accounts.rs (7)
  • undo (56-60)
  • undo (83-87)
  • undo (142-147)
  • undo (189-193)
  • undo (239-244)
  • undo (303-305)
  • undo (328-332)
crates/cardano/src/ewrap/retires.rs (3)
crates/cardano/src/estart/rewards.rs (3)
  • undo (36-46)
  • key (20-22)
  • apply (24-34)
crates/cardano/src/roll/accounts.rs (18)
  • undo (56-60)
  • undo (83-87)
  • undo (142-147)
  • undo (189-193)
  • undo (239-244)
  • new (26-32)
  • new (104-114)
  • new (160-166)
  • new (208-216)
  • new (261-271)
  • key (45-48)
  • key (72-75)
  • key (120-123)
  • key (172-175)
  • apply (50-54)
  • apply (77-81)
  • apply (125-140)
  • apply (177-187)
crates/cardano/src/pallas_extras.rs (1)
  • pool_reward_account (334-337)
crates/cardano/src/ewrap/wrapup.rs (3)
crates/cardano/src/model.rs (9)
  • key (700-702)
  • key (1702-1736)
  • from (1429-1431)
  • from (1435-1437)
  • from (1441-1443)
  • from (1447-1450)
  • apply (1738-1772)
  • undo (1774-1808)
  • transition (299-302)
crates/cardano/src/ewrap/mod.rs (2)
  • BoundaryWork (118-118)
  • visit_pool (24-31)
crates/cardano/src/forks.rs (1)
  • migrate_pparams_version (212-246)
crates/cardano/src/roll/pools.rs (3)
crates/cardano/src/model.rs (2)
  • with_live (161-170)
  • undo (1774-1808)
crates/cardano/src/roll/accounts.rs (7)
  • undo (56-60)
  • undo (83-87)
  • undo (142-147)
  • undo (189-193)
  • undo (239-244)
  • undo (303-305)
  • undo (328-332)
crates/cardano/src/roll/epochs.rs (3)
  • undo (67-94)
  • undo (125-128)
  • undo (168-170)
crates/cardano/src/estart/reset.rs (4)
crates/cardano/src/estart/mod.rs (4)
  • WorkContext (109-109)
  • visit_account (31-38)
  • visit_pool (21-28)
  • flush (61-63)
crates/cardano/src/rewards/mod.rs (5)
  • pots (296-296)
  • new (87-114)
  • new (191-199)
  • incentives (274-276)
  • incentives (295-295)
crates/cardano/src/pots.rs (1)
  • apply_delta (223-262)
crates/cardano/src/model.rs (7)
  • new (150-159)
  • new (511-523)
  • new (682-698)
  • new (1392-1402)
  • key (700-702)
  • key (1702-1736)
  • apply (1738-1772)
crates/cardano/src/model.rs (3)
crates/cardano/src/roll/accounts.rs (5)
  • new (26-32)
  • new (104-114)
  • new (160-166)
  • new (208-216)
  • new (261-271)
crates/cardano/src/roll/epochs.rs (1)
  • new (140-145)
crates/cardano/src/roll/pools.rs (3)
  • new (45-58)
  • new (164-171)
  • from (30-41)
🪛 GitHub Actions: CI
crates/cardano/src/estart/reset.rs

[warning] 8-9: unused imports: PParamsSet and RollingStats

🪛 GitHub Check: Check Build
crates/cardano/src/ewrap/wrapup.rs

[warning] 7-7:
unused import: pallas::ledger::primitives::Epoch


[warning] 2-2:
unused imports: AccountId, AccountState, and PoolSnapshot

🔇 Additional comments (23)
crates/minibf/src/routes/accounts.rs (1)

112-124: Good alignment with Stake-centric live values.

Using stake.live() for totals, rewards, withdrawals, and withdrawable improves consistency and avoids NPEs. Looks correct.

Please confirm these stringified zeros match Blockfrost responses for accounts without live stake.

crates/minibf/src/routes/pools.rs (2)

60-60: OK to surface params as Option; keep clone localized.

Mapping snapshot.live() to an Option of params is fine and keeps cloning scoped.


155-160: Delegator live stake path looks good.

Using AccountState::live_stake() centralizes the live() handling for stake totals and cleanly defaults to 0.

crates/cardano/src/roll/epochs.rs (1)

227-234: Fee selection for invalid txs looks good.

Using total_collateral when is_valid() is false matches the semantics; falls back to fee otherwise. LGTM.

Please confirm that this aligns with how downstream consumers expect “gathered_fees” to report invalid tx collateral vs. fees in epoch stats.

crates/cardano/src/model.rs (3)

1006-1021: Era transition detection via PParamsSet is clear.

Comparing live protocol_major to scheduled-next captures upgrades cleanly. LGTM.


1295-1299: RollingStats reset policy is appropriate.

TransitionDefault::next_value returns default, ensuring epoch-scoped stats reset at transitions. LGTM.


525-527: Minor: live_stake() can reuse withdrawable().

Not required, but consider using stake.live().map(Stake::total).unwrap_or_default() as you have; it’s fine. No change needed.

crates/cardano/src/genesis/mod.rs (1)

76-86: Genesis bootstrap changes look consistent.

  • EpochValue::with_genesis for pparams and with_live for rolling are aligned with new EpochValue flow.
  • bootstrap_eras now sources params via unwrap_live() and ensure_* accessors. LGTM.

Also applies to: 96-103

crates/cardano/src/lib.rs (1)

328-332: Effective pparams source updated to unwrap_live(): LGTM.

This matches the EpochValue semantics and avoids mixing in scheduled-next.

crates/cardano/src/estart/nonces.rs (1)

44-48: Era transition source switched to pparams: LGTM.

Sourcing via ended_state.pparams.era_transition() matches the new scheduling model.

Double-check that all BoundaryVisitors use the same pparams-based era transition source for consistency.

crates/cardano/src/ewrap/commit.rs (1)

20-22: Good: transition read is scoped under pparams

Using ending_state.pparams.era_transition() tightens the coupling to the source of truth for era changes. LGTM.

crates/cardano/src/ewrap/loading.rs (2)

26-29: Correct: exclude retired pools from “existing” set

Filtering on snapshot.is_retired prevents stale operators from being marked existing. LGTM.


73-121: Snapshot visitor removal verified — no lingering dependencies

Verification confirms complete removal of the snapshot visitor from the codebase:

  • No visitor_snapshot references exist
  • No SnapshotVisitor struct/impl definitions found
  • No snapshot visitor patterns in any modules
  • Only the three expected visitors (retires, govactions, wrapup) remain active in compute_deltas

The consolidation is sound and the new delta flow is clean.

src/bin/dolos/data/dump_logs.rs (1)

66-99: Safer optional chaining in EpochState row

Caching pparams/rolling and using as_ref()/unwrap_or_default() avoids panics and duplicate lookups. LGTM.

crates/cardano/src/rewards/mocking.rs (1)

314-318: iter_all_pools now by PoolHash—good alignment

Iterating keys and converting to PoolHash matches the updated RewardsContext and reduces coupling. LGTM.

crates/cardano/src/rewards/mod.rs (1)

306-308: Trait API change verified: all implementors properly updated

Verification confirms:

  • Both implementors (RupdWork in loading.rs and MockContext in mocking.rs) implement all three trait methods: iter_all_pools, pool_params, and pool_delegators
  • iter_all_pools correctly yields PoolHash as separate method
  • pool_params is properly separated as independent method
  • Call sites in mod.rs correctly use the new split API
  • No evidence of broken code or old paired signature dependencies
crates/cardano/src/roll/pools.rs (1)

77-87: No off-by-one error; scheduling is correct.

The schedule() function documentation states it "Schedules the next value to be applied on the next epoch transition." The function signature schedule(&mut self, current_epoch: Epoch, next: Option<T>) accepts the current epoch and schedules the value for activation at the transition TO the next epoch. The code correctly passes self.epoch (the current epoch), so the pool snapshot will be applied on the next epoch transition, exactly as the comment states. No fix needed here.

crates/cardano/src/ewrap/retires.rs (1)

86-90: ****

The review comment incorrectly assumes a double-wrapped Option. The EpochValue::next() method returns Option<&T>, not Option<&Option<T>>. For EpochValue<PoolHash>, calling .next().cloned() correctly produces Option<PoolHash>, and no .flatten() is needed. The original code is correct as written.

Likely an incorrect or invalid review comment.

crates/cardano/src/rupd/loading.rs (2)

166-172: LGTM on using stake snapshot for delegation and stake amount.

Pulling pool and stake from stake_epoch aligns with “params at snapshot epoch, blocks at performance epoch.”

Please confirm that registration status (added elsewhere) is also evaluated at stake_epoch if that’s a protocol requirement.


255-256: pparams source change: confirm semantic intent.

Switch to epoch.pparams.mark().cloned() looks correct with unified pparams handling. Verify it always reflects the set expected by rewards at load time (not next-epoch scheduled).

crates/cardano/src/estart/reset.rs (3)

81-96: EpochTransition.apply order looks correct; confirm pparams transition epoch number.

You set number, then transition rolling and pparams with self.new_epoch. Validate that transition() expects the epoch you’re transitioning into (not from), and that this aligns with “params active at start of new_epoch.”


144-165: LGTM on per-entity delta collection.

visit_account and visit_pool enqueue fine-grained transitions; keeps ordering explicit and reduces contention.


166-174: No off-by-one risk. The code is correct.

The starting_epoch_no() function returns self.ended_state.number + 1, which represents the epoch being transitioned to, not the one that ended. Setting EpochTransition.new_epoch = ctx.starting_epoch_no() is semantically correct and aligns with consistent usage patterns throughout the codebase. No fix required.

Comment on lines +6 to 10
estart::{AccountId, PoolId, WorkContext},
pots::{apply_delta, PotDelta, Pots},
EpochState, FixedNamespace as _, PParamsSet, RollingStats, CURRENT_EPOCH_KEY,
AccountState, CardanoDelta, EpochState, FixedNamespace as _, PParamsSet, PoolState,
RollingStats, CURRENT_EPOCH_KEY,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

CI warning: remove unused imports.

PParamsSet and RollingStats are unused. Clean them up to fix CI warnings.

-    AccountState, CardanoDelta, EpochState, FixedNamespace as _, PParamsSet, PoolState,
-    RollingStats, CURRENT_EPOCH_KEY,
+    AccountState, CardanoDelta, EpochState, FixedNamespace as _, PoolState, CURRENT_EPOCH_KEY,
📝 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
estart::{AccountId, PoolId, WorkContext},
pots::{apply_delta, PotDelta, Pots},
EpochState, FixedNamespace as _, PParamsSet, RollingStats, CURRENT_EPOCH_KEY,
AccountState, CardanoDelta, EpochState, FixedNamespace as _, PParamsSet, PoolState,
RollingStats, CURRENT_EPOCH_KEY,
};
estart::{AccountId, PoolId, WorkContext},
pots::{apply_delta, PotDelta, Pots},
AccountState, CardanoDelta, EpochState, FixedNamespace as _, PoolState, CURRENT_EPOCH_KEY,
};
🧰 Tools
🪛 GitHub Actions: CI

[warning] 8-9: unused imports: PParamsSet and RollingStats

🤖 Prompt for AI Agents
In crates/cardano/src/estart/reset.rs around lines 6 to 10, the imports include
PParamsSet and RollingStats which are unused and triggering CI warnings; remove
PParamsSet and RollingStats from the import list (and tidy commas/whitespace if
needed) so the file only imports actually used symbols.

Comment on lines +32 to 34
let stake = entity.stake.unwrap_live_mut();
stake.rewards_sum += self.reward;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against missing live stake instead of panicking

unwrap_live_mut() will panic if stake is absent for a valid account (e.g., edge cases during transitions). Fail soft with a warning like the missing account path.

-        let stake = entity.stake.unwrap_live_mut();
-        stake.rewards_sum += self.reward;
+        if let Some(stake) = entity.stake.live_mut() {
+            stake.rewards_sum = stake.rewards_sum.saturating_add(self.reward);
+        } else {
+            warn!("missing live stake when assigning rewards");
+        }

If live_mut() isn’t available, add a helper or map unwrap_live_mut() error into ChainError.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In crates/cardano/src/estart/rewards.rs around lines 32–34, calling
unwrap_live_mut() on entity.stake will panic if the live stake is missing;
change this to handle the None case gracefully by matching or using
map_ok/map_err to convert the missing-stake into a ChainError and emit a warning
(or log) instead of panicking. Specifically, replace the direct
unwrap_live_mut() with a match or helper that returns an error variant (with a
short warning message) when there is no live stake, or implement a small helper
that maps unwrap_live_mut() failure into ChainError and then update the call to
use that helper so stake.rewards_sum += self.reward only runs when the stake is
present.

Comment on lines +52 to 56
let pparams = self.ending_state().pparams.unwrap_live();

let expiring_epoch = last_activity_epoch + pparams.ensure_drep_inactivity_period()?;

Ok(expiring_epoch <= self.starting_epoch_no())
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid panic in DRep expiry calc

unwrap_live() can panic if pparams are not yet live at boundary; prefer a fallible path.

-        let pparams = self.ending_state().pparams.unwrap_live();
+        let pparams = self
+            .ending_state()
+            .pparams
+            .live()
+            .ok_or_else(|| ChainError::from(BrokenInvariant::BadBootstrap))?;
 
-        let expiring_epoch = last_activity_epoch + pparams.ensure_drep_inactivity_period()?;
+        let expiring_epoch = last_activity_epoch + pparams.ensure_drep_inactivity_period()?;
🤖 Prompt for AI Agents
In crates/cardano/src/ewrap/loading.rs around lines 52 to 56, replace the
panicking call to pparams.unwrap_live() with a fallible retrieval and handle the
non-live case instead of panicking: obtain the live protocol parameters via the
API's fallible method (or match on the Option/Result), propagate an error if
appropriate or return an early Ok(false)/appropriate result when params are not
yet live, then continue using the safely obtained live params for the
expiring_epoch calculation and return.

Comment on lines 92 to 95
fn undo(&self, entity: &mut Option<AccountState>) {
let Some(entity) = entity else {
warn!("missing delegator");
return;
};

debug!(delegator=%self.delegator, "restoring pool delegator");

entity.pool = self.prev_pool.clone().expect("called with undo data");
todo!()
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Implement undo for PoolDelegatorDrop.

Leaving todo!() will panic on reorg; restore prior scheduled value (or clear if none).

     fn undo(&self, entity: &mut Option<AccountState>) {
-        todo!()
+        if let Some(entity) = entity {
+            entity.pool.schedule_unchecked(self.prev_pool.clone());
+        }
     }
📝 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
fn undo(&self, entity: &mut Option<AccountState>) {
let Some(entity) = entity else {
warn!("missing delegator");
return;
};
debug!(delegator=%self.delegator, "restoring pool delegator");
entity.pool = self.prev_pool.clone().expect("called with undo data");
todo!()
}
}
fn undo(&self, entity: &mut Option<AccountState>) {
if let Some(entity) = entity {
entity.pool.schedule_unchecked(self.prev_pool.clone());
}
}
}
🤖 Prompt for AI Agents
In crates/cardano/src/ewrap/retires.rs around lines 92–95, replace the todo!()
with logic that restores the prior scheduled delegation value when undoing a
PoolDelegatorDrop: if entity is Some(ref mut state) set
state.scheduled_delegation (or the field name used for the scheduled value) to
self.prior.clone() (or None if self.prior is None); if entity is None and
self.prior.is_some(), create a new AccountState with scheduled_delegation set to
self.prior.clone() and assign it to *entity; ensure you handle cloning and avoid
unwraps so no panic occurs on reorg.

Comment on lines 159 to 167
fn apply(&mut self, entity: &mut Option<AccountState>) {
let Some(entity) = entity else {
warn!("missing delegator");
return;
};
let entity = entity.as_mut().expect("existing account");

debug!(delegator=%self.delegator, "dropping drep delegator");

// save undo info
self.prev_drep = Some(entity.drep.clone());

// apply changes
entity.drep.replace_unchecked(None);
entity.drep.schedule_unchecked(None);
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

DRepDelegatorDrop doesn’t save previous value; undo can’t restore.

Capture prev_next before scheduling None and implement undo accordingly.

 pub struct DRepDelegatorDrop {
     delegator: AccountId,
     // undo
-    prev_drep: Option<DRep>,
+    prev_drep: Option<DRep>,
 }
@@
     fn apply(&mut self, entity: &mut Option<AccountState>) {
-        let entity = entity.as_mut().expect("existing account");
+        let entity = entity.as_mut().expect("existing account");
         debug!(delegator=%self.delegator, "dropping drep delegator");
-        // apply changes
+        // capture undo
+        self.prev_drep = entity.drep.next().cloned();
+        // apply changes
         entity.drep.schedule_unchecked(None);
     }
@@
-    fn undo(&self, entity: &mut Option<AccountState>) {
-        todo!()
+    fn undo(&self, entity: &mut Option<AccountState>) {
+        if let Some(entity) = entity {
+            entity.drep.schedule_unchecked(self.prev_drep.clone());
+        }
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In crates/cardano/src/ewrap/retires.rs around lines 159–167, the apply() call
clears the drep delegator but does not record the previous value so undo cannot
restore it; capture the existing next value from entity.drep (clone or take into
a new field on DRepDelegatorDrop, e.g. prev_next: Option<_>) before calling
schedule_unchecked(None), store that prev_next on the DRepDelegatorDrop
instance, and update undo() to call entity.drep.schedule_unchecked(prev_next) to
restore the original value (handle cloning/ownership to avoid borrow issues).

Comment on lines +25 to +28
// we need to use a delta approach instead of simple increments because the total size of moved
// lovelace can be higher than u64, causing overflows
utxo_delta: i64,

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

UTXO accounting is incorrect: net delta loses produced/consumed totals.

Using a single utxo_delta and updating produced_utxos or consumed_utxos based only on its sign records net movement per block, not the actual amounts produced and consumed. Example: inputs=990, outputs=1000 → utxo_delta=+10, but produced=1000 and consumed=990 should both be counted. Current logic would add only 10 to produced. This breaks epoch stats.

Fix by tracking per-side totals in the update and applying both:

@@
-    // we need to use a delta approach instead of simple increments because the total size of moved
-    // lovelace can be higher than u64, causing overflows
-    utxo_delta: i64,
+    // track per-side totals to avoid netting out consumption vs production
+    utxo_consumed: u64,
+    utxo_produced: u64,
@@
-        if self.utxo_delta > 0 {
-            stats.produced_utxos += self.utxo_delta.abs() as u64;
-        } else {
-            stats.consumed_utxos += self.utxo_delta.abs() as u64;
-        }
+        stats.produced_utxos += self.utxo_produced;
+        stats.consumed_utxos += self.utxo_consumed;
@@
-        if self.utxo_delta > 0 {
-            stats.produced_utxos -= self.utxo_delta.abs() as u64;
-        } else {
-            stats.consumed_utxos -= self.utxo_delta.abs() as u64;
-        }
+        stats.produced_utxos -= self.utxo_produced;
+        stats.consumed_utxos -= self.utxo_consumed;
@@
-        let amount = resolved.value().coin();
-        self.stats_delta.as_mut().unwrap().utxo_delta -= amount as i64;
+        let amount = resolved.value().coin();
+        self.stats_delta.as_mut().unwrap().utxo_consumed =
+            self.stats_delta.as_ref().unwrap().utxo_consumed.saturating_add(amount);
@@
-        let amount = output.value().coin();
-        self.stats_delta.as_mut().unwrap().utxo_delta += amount as i64;
+        let amount = output.value().coin();
+        self.stats_delta.as_mut().unwrap().utxo_produced =
+            self.stats_delta.as_ref().unwrap().utxo_produced.saturating_add(amount);

Also applies to: 49-54, 78-83, 246-247, 260-261

🤖 Prompt for AI Agents
In crates/cardano/src/roll/epochs.rs around lines 25-28 (and also adjust update
sites at 49-54, 78-83, 246-247, 260-261), replace the single utxo_delta approach
with explicit per-side totals: add fields for produced_utxo_amount and
consumed_utxo_amount (types large enough to avoid overflow), and in each update
compute and add both the produced and consumed amounts separately instead of
only applying the signed net delta to one side; update all places that currently
set or accumulate utxo_delta to compute inputs and outputs independently and
increment both produced and consumed totals accordingly, ensuring aggregation
uses the new fields.

Comment on lines 168 to 170
fn undo(&self, entity: &mut Option<EpochState>) {
if let Some(entity) = entity {
if let Some(prev_value) = &self.prev_value {
entity.pparams.live_mut_unchecked().set(prev_value.clone());
} else {
entity
.pparams
.live_mut_unchecked()
.clear(self.to_update.kind());
}
}
todo!()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Implement PParamsUpdate.undo to support rollbacks.

Undo is todo!(), which will panic on rollback. Restore the scheduled-next value previously captured in prev_value:

@@
 impl dolos_core::EntityDelta for PParamsUpdate {
@@
     fn apply(&mut self, entity: &mut Option<EpochState>) {
         let entity = entity.as_mut().expect("epoch state missing");
         debug!(value = ?self.to_update, "applying pparam update");
-        let next = entity.pparams.scheduled_or_default();
-        // undo data
-        self.prev_value = next.get(self.to_update.kind()).cloned();
-        next.set(self.to_update.clone());
+        let next = entity.pparams.scheduled_or_default();
+        // capture previous scheduled value to allow precise undo
+        self.prev_value = next.get(self.to_update.kind()).cloned();
+        next.set(self.to_update.clone());
     }
@@
-    fn undo(&self, entity: &mut Option<EpochState>) {
-        todo!()
-    }
+    fn undo(&self, entity: &mut Option<EpochState>) {
+        let entity = entity.as_mut().expect("epoch state missing");
+        let next = entity
+            .pparams
+            .next_mut()
+            .expect("scheduled pparams missing on undo");
+
+        match &self.prev_value {
+            Some(prev) => next.set(prev.clone()),
+            None => next.clear(self.to_update.kind()),
+        }
+    }

Optional: also track whether “next” existed before apply to restore emptiness precisely (e.g., a boolean flag), but the above is sufficient functionally for rollbacks now.

Also applies to: 155-166, 160-166

🤖 Prompt for AI Agents
In crates/cardano/src/roll/epochs.rs around lines 168-170, implement
PParamsUpdate::undo so it restores the epoch state's scheduled-next value from
self.prev_value instead of todo!(); specifically, if the provided entity is
Some(epoch_state) then set epoch_state.scheduled_next = self.prev_value.clone();
if entity is None simply return (don't panic). Optionally you can add a boolean
to track existence for an exact empty-vs-some restore, but restoring
prev_value.clone() is sufficient for rollbacks.

Comment on lines +128 to 151
// if pool.snapshot.snapshot_at(stake_epoch).is_none() {
// continue;
// };

let Some(pool_snapshot) = pool.snapshot.snapshot_at(performance_epoch) else {
continue;
};

if pool_snapshot.is_retired {
warn!(operator = %pool.operator, "skipping retired or pending pool are stake epoch");
if pool_snapshot.blocks_minted == 0 {
continue;
}

snapshot
.pool_params
.insert(pool.operator, pool_snapshot.params.clone());

// for tracking blocks we switch to the performance epoch (previous epoch, the
// one we're computing rewards for)
.pool_blocks
.insert(pool.operator, pool_snapshot.blocks_minted as u64);

let Some(pool_snapshot) = pool.snapshot.snapshot_at(performance_epoch) else {
let Some(pool_snapshot) = pool.snapshot.snapshot_at(stake_epoch) else {
continue;
};

snapshot
.pool_blocks
.insert(pool.operator, pool_snapshot.blocks_minted as u64);
.pool_params
.insert(pool.operator, pool_snapshot.params.clone());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Key-set mismatch between pool_blocks and pool_params can cause panics and missed rewards.

You insert pool_blocks before verifying stake_epoch params exist. For pools that minted in performance_epoch but lack a stake_epoch snapshot (e.g., newly registered after snapshot), pool_params will be missing. This breaks:

  • Display impl (unwrap)
  • RewardsContext::pool_params (unwrap) when iterating pools

Fix by inserting blocks only after confirming stake_epoch params are present, or by collecting both first and inserting atomically.

-            let Some(pool_snapshot) = pool.snapshot.snapshot_at(performance_epoch) else {
+            let Some(perf_s) = pool.snapshot.snapshot_at(performance_epoch) else {
                 continue;
             };
-            if pool_snapshot.blocks_minted == 0 {
+            if perf_s.blocks_minted == 0 {
                 continue;
             }
-
-            snapshot
-                .pool_blocks
-                .insert(pool.operator, pool_snapshot.blocks_minted as u64);
-
-            let Some(pool_snapshot) = pool.snapshot.snapshot_at(stake_epoch) else {
+            // Only proceed if stake-epoch params exist; keep maps in sync
+            let Some(stake_s) = pool.snapshot.snapshot_at(stake_epoch) else {
                 continue;
             };
-
-            snapshot
-                .pool_params
-                .insert(pool.operator, pool_snapshot.params.clone());
+            snapshot
+                .pool_blocks
+                .insert(pool.operator, perf_s.blocks_minted as u64);
+            snapshot
+                .pool_params
+                .insert(pool.operator, stake_s.params.clone());
🤖 Prompt for AI Agents
In crates/cardano/src/rupd/loading.rs around lines 128 to 151, you currently
insert pool_blocks as soon as a performance_epoch snapshot exists which can
leave pool_params missing if the stake_epoch snapshot is absent; change the
logic to first attempt to obtain both snapshots (performance_epoch and
stake_epoch) for a pool, then only if the stake_epoch snapshot exists and
blocks_minted > 0 insert both pool_blocks and pool_params (or collect both
values and insert them together), ensuring the two maps are updated atomically
and preserving key-set parity.

Comment on lines +302 to 308
fn iter_all_pools(&self) -> impl Iterator<Item = PoolHash> {
self.snapshot.pool_blocks.keys().cloned()
}

fn pool_params(&self, pool: PoolHash) -> &PoolParams {
self.snapshot.pool_params.get(&pool).unwrap()
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Prevent invariant violations: iterate only pools that have both blocks and params.

iter_all_pools currently yields keys from pool_blocks, but pool_params unwraps and can panic if any key is missing. Either enforce the map-population invariant (see earlier diff) or add a defensive intersection here.

-    fn iter_all_pools(&self) -> impl Iterator<Item = PoolHash> {
-        self.snapshot.pool_blocks.keys().cloned()
-    }
+    fn iter_all_pools(&self) -> impl Iterator<Item = PoolHash> {
+        self.snapshot
+            .pool_blocks
+            .keys()
+            .filter(|k| self.snapshot.pool_params.contains_key(*k))
+            .cloned()
+    }

Additionally, consider replacing unwrap in pool_params with expect carrying a clear message to aid debugging.

-    fn pool_params(&self, pool: PoolHash) -> &PoolParams {
-        self.snapshot.pool_params.get(&pool).unwrap()
-    }
+    fn pool_params(&self, pool: PoolHash) -> &PoolParams {
+        self.snapshot
+            .pool_params
+            .get(&pool)
+            .expect("pool_params missing for iterated pool")
+    }
📝 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
fn iter_all_pools(&self) -> impl Iterator<Item = PoolHash> {
self.snapshot.pool_blocks.keys().cloned()
}
fn pool_params(&self, pool: PoolHash) -> &PoolParams {
self.snapshot.pool_params.get(&pool).unwrap()
}
fn iter_all_pools(&self) -> impl Iterator<Item = PoolHash> {
self.snapshot
.pool_blocks
.keys()
.filter(|k| self.snapshot.pool_params.contains_key(*k))
.cloned()
}
fn pool_params(&self, pool: PoolHash) -> &PoolParams {
self.snapshot
.pool_params
.get(&pool)
.expect("pool_params missing for iterated pool")
}
🤖 Prompt for AI Agents
In crates/cardano/src/rupd/loading.rs around lines 302 to 308, iter_all_pools
currently yields keys from snapshot.pool_blocks only which can lead to
pool_params calling unwrap and panicking if a pool key is missing; change
iter_all_pools to return an iterator over the intersection of
snapshot.pool_blocks.keys() and snapshot.pool_params.keys() (so only pools that
have both blocks and params are produced), and replace the unwrap in pool_params
with expect carrying a clear message like "missing pool params for {pool}" to
make any remaining invariant violations explicit for debugging.

Comment on lines 207 to 245
"{} ({})",
self.snapshot.go().map(|x| x.is_pending).unwrap_or_default(),
self.snapshot
.go()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or_default() - 3,
),
format!(
"{} ({})",
self.snapshot
.set()
.map(|x| x.is_pending)
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or_default() - 2
),
format!(
"{} ({})",
self.snapshot
.mark()
.map(|x| x.is_pending)
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or_default() - 1
),
format!(
"{} ({})",
self.snapshot.live().is_pending,
self.snapshot
.live()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or_default()
),
format!(
"{} ({})",
self.snapshot
.next()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or_default()
),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Epoch arithmetic underflow in display.

Subtracting 3/2/1 from epoch without checks will wrap for early epochs (u64 underflow).

Use saturating_sub:

-                self.snapshot.epoch().unwrap_or_default() - 3,
+                self.snapshot.epoch().unwrap_or(0).saturating_sub(3),
@@
-                self.snapshot.epoch().unwrap_or_default() - 2
+                self.snapshot.epoch().unwrap_or(0).saturating_sub(2)
@@
-                self.snapshot.epoch().unwrap_or_default() - 1
+                self.snapshot.epoch().unwrap_or(0).saturating_sub(1)
@@
-                self.snapshot.epoch().unwrap_or_default()
+                self.snapshot.epoch().unwrap_or(0)
@@
-                self.snapshot.epoch().unwrap_or_default()
+                self.snapshot.epoch().unwrap_or(0)

Alternatively, surface epoch numbers from mark()/set()/go() helpers to avoid manual math. [Based on learnings]

📝 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
"{} ({})",
self.snapshot.go().map(|x| x.is_pending).unwrap_or_default(),
self.snapshot
.go()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or_default() - 3,
),
format!(
"{} ({})",
self.snapshot
.set()
.map(|x| x.is_pending)
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or_default() - 2
),
format!(
"{} ({})",
self.snapshot
.mark()
.map(|x| x.is_pending)
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or_default() - 1
),
format!(
"{} ({})",
self.snapshot.live().is_pending,
self.snapshot
.live()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or_default()
),
format!(
"{} ({})",
self.snapshot
.next()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or_default()
),
"{} ({})",
self.snapshot
.go()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or(0).saturating_sub(3),
),
format!(
"{} ({})",
self.snapshot
.set()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or(0).saturating_sub(2)
),
format!(
"{} ({})",
self.snapshot
.mark()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or(0).saturating_sub(1)
),
format!(
"{} ({})",
self.snapshot
.live()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or(0)
),
format!(
"{} ({})",
self.snapshot
.next()
.map(|x| x.params.pledge)
.unwrap_or_default(),
self.snapshot.epoch().unwrap_or(0)
),
🤖 Prompt for AI Agents
In src/bin/dolos/data/dump_state.rs around lines 207 to 245, the code subtracts
constants (3,2,1) from snapshot.epoch() which can underflow for early epochs
(u64 wrap); replace these subtractions with saturating_sub calls (e.g.,
epoch.saturating_sub(3/2/1)) or, preferably, use the epoch values returned by
go()/set()/mark()/live()/next() helpers when available instead of manual
arithmetic so no underflow occurs and displayed epochs are correct.

@scarmuega scarmuega merged commit 55f7365 into main Oct 20, 2025
8 of 9 checks passed
@scarmuega scarmuega deleted the fix/pool-params-timing branch October 20, 2025 08:37
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.

2 participants