Skip to content

refactor(cardano): use deltas for the sweep process - #717

Merged
scarmuega merged 3 commits into
mainfrom
refactor/delta-sweep
Sep 25, 2025
Merged

refactor(cardano): use deltas for the sweep process#717
scarmuega merged 3 commits into
mainfrom
refactor/delta-sweep

Conversation

@scarmuega

@scarmuega scarmuega commented Sep 24, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Undoable pool de-registration/retirement and DRep expiration flows; automatic pool reward-account detection and initialization.
    • Reward calculation & distribution to pool operators and delegators at epoch boundaries.
    • Batched epoch boundary transitions for pools, accounts and DReps (atomic, undoable state moves).
  • Refactor

    • Domain-driven sweep/commit with batched, namespace-wide delta application for more efficient commits.
  • Bug Fixes

    • Improved pool-registration detection during epoch processing and transaction mapping.

@coderabbitai

coderabbitai Bot commented Sep 24, 2025

Copy link
Copy Markdown

Walkthrough

Adds visitor-driven, domain-based boundary processing producing undoable Cardano deltas (retirements, expirations, rewards, transitions); extends model enums/fields; consolidates commit to apply whole namespaces; replaces per-entity sweep storage with WorkDeltas; renames a pallas helper and adds pool reward account helper.

Changes

Cohort / File(s) Summary
Model & Delta additions
crates/cardano/src/model.rs
Adds PoolState.is_retired and DRepState.expired; extends CardanoDelta with many new variants; updates key/apply/undo routing and imports.
Pallas helper & call sites
crates/cardano/src/pallas_extras.rs, crates/cardano/src/roll/epochs.rs, crates/minibf/src/mapping.rs
Renames cert_to_pool_statecert_as_pool_registration, adds pool_reward_account(...), and updates call sites to use the registration check.
Roll pools / pool account detection
crates/cardano/src/roll/pools.rs
Renames PoolRetirementPoolDeRegistration; adds PoolAccountDetected delta (with undo) and wires new deltas into cert processing.
Sweep framework: mod/loading/compute/commit
crates/cardano/src/sweep/mod.rs, .../loading.rs, .../compute.rs, .../commit.rs
Introduces BoundaryVisitor trait; replaces pool/drep per-structure storage with WorkDeltas; BoundaryWork now holds deltas; compute is domain-driven; commit applies whole namespaces via apply_whole_namespace and removes per-record rotation helpers.
Sweep visitors: retires / rewards / transition
crates/cardano/src/sweep/retires.rs, .../rewards.rs, .../transition.rs
New visitor modules produce CardanoDeltas: pool/DRep retirements & delegator drops, pool+delegator reward assignments, and account/pool transitions; each delta implements apply/undo and is flushed into BoundaryWork.
Core batch utility
crates/core/src/batch.rs
Adds #[derive(Debug)] to WorkDeltas<C: ChainLogic>.
Tx mapping change
crates/minibf/src/mapping.rs
Switches certificate check to cert_as_pool_registration(...) for pool-deposit logic.
Tests / vectors & toy domain
crates/test-vectors/src/build.rs, crates/testing/src/toy_domain.rs
Updates StateStore import paths; initializes new is_retired and expired fields in serialized states; rewires ToyDomain archive/state types to new paths.
Redb3 minor import fix
crates/redb3/src/state/utxoset.rs
Adjusted test import path for StateStore (no functional change).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Runner as Sweep Runner
  participant Domain
  participant Boundary as BoundaryWork
  participant Visitors as BoundaryVisitors
  participant Deltas as WorkDeltas
  participant Commit as commit()

  Runner->>Boundary: new BoundaryWork(...)
  Runner->>Boundary: compute(domain)
  Boundary->>Domain: state() iter Pool/DRep/Account
  loop per entity
    Boundary->>Visitors: visit_pool / visit_drep / visit_account
    Visitors->>Deltas: push CardanoDelta (retire/expire/drop/reward/transition)
  end
  Boundary->>Visitors: flush()
  Visitors->>Deltas: finalize pending deltas
  Runner->>Boundary: commit(domain)
  Boundary->>Commit: apply_whole_namespace for Account/Pool/DRep
  Commit->>Domain: writer()
  Commit->>Domain: write updated entities (apply deltas)
  Commit->>Boundary: run epoch lifecycle (drop/promote)
Loading
sequenceDiagram
  autonumber
  participant BW as BoundaryWork
  participant Rewards as RewardsVisitor
  participant Pools as PoolState
  participant Delegs as DelegatorMap
  participant Deltas as WorkDeltas

  BW->>Rewards: visit_pool(pool)
  Rewards->>Pools: read pool params & stake
  Rewards->>Rewards: compute_pool_reward()
  alt pool reward account present
    Rewards->>Deltas: AssignPoolRewards(pool, account, operator_share)
  else
    Note right of Rewards: warn missing reward account
  end
  loop each delegator
    Rewards->>Rewards: compute_delegator_reward()
    Rewards->>Deltas: AssignDelegatorRewards(account, reward)
  end
Loading
sequenceDiagram
  autonumber
  participant BW as BoundaryWork
  participant Retires as RetiresVisitor
  participant Pool as PoolState
  participant Accounts as AccountState
  participant Deltas as WorkDeltas

  BW->>Retires: visit_pool(pool)
  Retires->>Retires: should_retire_pool()
  alt retire now
    Retires->>Deltas: PoolDeRegistration / PoolDelegatorDrop (per delegator)
  end
  BW->>Retires: visit_drep(drep)
  Retires->>Retires: should_expire_drep()
  alt expire now
    Retires->>Deltas: DRepExpiration / DRepDelegatorDrop (per delegator)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

A rabbit taps the epoch drum, hop-hop—new day begun,
Visitors gather deltas, tiny hops of work well done.
Pools bow out, DReps snooze, rewards fall like clover,
Undo leaves gentle footprints, so we can step back over.
Boundaries swept, carrots kept—commit, and onward rover! 🥕

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 accurately conveys that the pull request refactors the Cardano sweep process to use deltas, reflecting the main change in the codebase; it is concise, specific, and follows the conventional commit format, making it easy for teammates to understand the primary intent at a glance.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/delta-sweep

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: 10

Caution

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

⚠️ Outside diff range comments (5)
crates/minibf/src/mapping.rs (1)

592-596: Remove dbg! calls from deposit calculation.

Leaking cert data and deposit amounts to stderr in production is noisy and can bloat logs.

Apply this diff:

-                if pallas_extras::cert_as_stake_registration(x).is_some() {
-                    dbg!(x);
-                    return Some(dbg!(key_deposit));
-                }
+                if pallas_extras::cert_as_stake_registration(x).is_some() {
+                    return Some(key_deposit);
+                }
crates/core/src/batch.rs (1)

12-16: Deriving Debug here imposes C::Delta: Debug and may break existing ChainLogic implementors.

#[derive(Debug)] on WorkDeltas<C> requires C::Delta: Debug. If any Delta doesn’t implement Debug, this won’t compile. Prefer a manual Debug impl that avoids extra bounds.

Apply this diff to drop the derive:

-#[derive(Debug)]
 pub struct WorkDeltas<C: ChainLogic> {
     pub entities: HashMap<NsKey, Vec<C::Delta>>,
     pub slot: SlotTags,
 }

Add this manual Debug impl elsewhere in the file:

impl<C: ChainLogic> std::fmt::Debug for WorkDeltas<C> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("WorkDeltas")
            .field("entities_len", &self.entities.len())
            .field("slot", &self.slot)
            .finish()
    }
}
crates/cardano/src/roll/epochs.rs (3)

40-46: Bug: stake deregistration uses pool deposit instead of key deposit.

Decayed deposits from stake deregistration should reference the key deposit, not the pool deposit.

Apply this diff:

-        entity.decayed_deposits +=
-            self.stake_deregistration_count * entity.pparams.pool_deposit_or_default();
+        entity.decayed_deposits +=
+            self.stake_deregistration_count * entity.pparams.key_deposit_or_default();

Also fix the undo accordingly (Lines 60-62):

-        entity.decayed_deposits -=
-            self.stake_deregistration_count * entity.pparams.pool_deposit_or_default();
+        entity.decayed_deposits -=
+            self.stake_deregistration_count * entity.pparams.key_deposit_or_default();

1101-1121: Off-by-one in delegation active epoch.

You compute active_epoch as epoch + 1 upstream and add +1 again here, resulting in epoch + 2.

Apply this diff:

-        active_epoch: active_epoch + 1,
+        active_epoch,

260-269: Replace the first duplicate mapping with the correct getter
Change line 260 from

all_proposed_ada_per_utxo_byte => MinUtxoValue,

to

all_proposed_min_utxo_value => MinUtxoValue,

and keep only the existing all_proposed_ada_per_utxo_byte => AdaPerUtxoByte entry.

🧹 Nitpick comments (6)
crates/cardano/src/sweep/commit.rs (1)

88-119: Consider optimizing namespace iteration to avoid removing all entities upfront.

The current implementation iterates through all entities in a namespace and removes matching deltas from the map. For namespaces with many entities but few deltas, this could be inefficient. Consider iterating over deltas first and loading only the entities that need updates.

crates/cardano/src/sweep/compute.rs (1)

298-340: Consider extracting visitor orchestration into a helper method.

The repetitive pattern of visitor initialization, entity iteration, and flush could be extracted into a reusable helper to reduce code duplication and improve maintainability.

Example refactor:

fn apply_visitors<D: Domain>(
    &mut self,
    domain: &D,
    visitors: &mut [&mut dyn BoundaryVisitor],
) -> Result<(), ChainError> {
    // Apply to pools
    let pools = domain.state().iter_entities_typed::<PoolState>(PoolState::NS, None)?;
    for pool in pools {
        let (pool_id, pool) = pool?;
        for visitor in visitors.iter_mut() {
            visitor.visit_pool(self, &pool_id, &pool)?;
        }
    }
    // Similar for dreps and accounts...
    // Then flush all
    for visitor in visitors.iter_mut() {
        visitor.flush(self)?;
    }
    Ok(())
}
crates/cardano/src/model.rs (1)

1035-1035: Clean up commented code or add explanation.

The commented-out AssignEpochRewards variant appears in multiple places. Either remove it completely or add a comment explaining why it's kept for future use.

Also applies to: 1098-1098

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

199-212: Consider batching delegator reward deltas.

Creating individual deltas for each delegator could be memory-intensive for large pools. Consider batching or streaming the deltas.

Instead of collecting all deltas in a vector first, apply them directly:

-let mut delegators = vec![];
-
 for (delegator, stake) in ctx.active_snapshot.accounts_by_pool.iter_delegators(id) {
     let reward = compute_delegator_reward(total_pool_reward, pool_stake, *stake);
-
-    delegators.push(AssignDelegatorRewards {
+    ctx.add_delta(AssignDelegatorRewards {
         account: delegator.clone(),
         reward,
     });
 }
-
-for delta in delegators {
-    ctx.add_delta(delta);
-}
crates/cardano/src/sweep/mod.rs (1)

80-95: Consider documenting the EntityKey change rationale.

The change from PoolId to EntityKey in DelegatorMap enables handling both pools and DReps uniformly, which is a good architectural improvement. However, this breaking change may impact external consumers of the API.

Consider adding a doc comment explaining the generic nature of the map:

 #[derive(Debug, Default, Clone)]
+/// Maps entities (pools or DReps) to their delegators and stake amounts
 pub struct DelegatorMap(HashMap<EntityKey, HashMap<AccountId, u64>>);
crates/cardano/src/sweep/retires.rs (1)

89-121: Consolidate duplicate DRepExpiration implementation
Extract the DRepExpiration struct and its EntityDelta impl (including unified warn/debug logging) into a shared module, then import it in both retires.rs and transition.rs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d99431a and c2b3359.

📒 Files selected for processing (13)
  • crates/cardano/src/model.rs (10 hunks)
  • crates/cardano/src/pallas_extras.rs (2 hunks)
  • crates/cardano/src/roll/epochs.rs (1 hunks)
  • crates/cardano/src/roll/pools.rs (6 hunks)
  • crates/cardano/src/sweep/commit.rs (2 hunks)
  • crates/cardano/src/sweep/compute.rs (5 hunks)
  • crates/cardano/src/sweep/loading.rs (2 hunks)
  • crates/cardano/src/sweep/mod.rs (5 hunks)
  • crates/cardano/src/sweep/retires.rs (1 hunks)
  • crates/cardano/src/sweep/rewards.rs (1 hunks)
  • crates/cardano/src/sweep/transition.rs (1 hunks)
  • crates/core/src/batch.rs (1 hunks)
  • crates/minibf/src/mapping.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
crates/minibf/src/mapping.rs (1)
crates/cardano/src/pallas_extras.rs (1)
  • cert_as_pool_registration (34-86)
crates/cardano/src/roll/epochs.rs (1)
crates/cardano/src/pallas_extras.rs (1)
  • cert_as_pool_registration (34-86)
crates/cardano/src/sweep/retires.rs (5)
crates/cardano/src/model.rs (8)
  • sweep (727-737)
  • key (1105-1135)
  • from (866-868)
  • from (872-874)
  • from (878-880)
  • from (884-887)
  • apply (1137-1167)
  • undo (1169-1199)
crates/cardano/src/sweep/mod.rs (4)
  • sweep (161-177)
  • visit_pool (33-40)
  • visit_drep (53-60)
  • flush (63-65)
crates/cardano/src/roll/pools.rs (12)
  • key (39-42)
  • key (73-75)
  • key (99-101)
  • key (138-141)
  • apply (44-57)
  • apply (77-81)
  • apply (103-107)
  • apply (143-158)
  • undo (59-61)
  • undo (83-87)
  • undo (109-113)
  • undo (160-164)
crates/cardano/src/sweep/rewards.rs (7)
  • key (63-68)
  • key (102-104)
  • apply (70-79)
  • apply (106-115)
  • undo (81-90)
  • undo (117-126)
  • visit_pool (160-215)
crates/cardano/src/sweep/transition.rs (14)
  • key (23-25)
  • key (65-67)
  • key (97-99)
  • key (125-127)
  • apply (27-36)
  • apply (69-77)
  • apply (101-105)
  • apply (129-137)
  • undo (38-50)
  • undo (79-86)
  • undo (107-111)
  • undo (139-143)
  • visit_pool (176-194)
  • flush (215-221)
crates/cardano/src/sweep/rewards.rs (7)
crates/cardano/src/model.rs (10)
  • sweep (727-737)
  • k (618-620)
  • a0 (622-624)
  • key (1105-1135)
  • from (866-868)
  • from (872-874)
  • from (878-880)
  • from (884-887)
  • apply (1137-1167)
  • undo (1169-1199)
crates/cardano/src/sweep/mod.rs (2)
  • sweep (161-177)
  • visit_pool (33-40)
crates/cardano/src/roll/pools.rs (11)
  • key (39-42)
  • key (73-75)
  • key (99-101)
  • key (138-141)
  • apply (44-57)
  • apply (77-81)
  • apply (103-107)
  • undo (59-61)
  • undo (83-87)
  • undo (109-113)
  • undo (160-164)
crates/cardano/src/sweep/retires.rs (3)
  • key (19-21)
  • undo (34-43)
  • visit_pool (195-220)
crates/cardano/src/roll/accounts.rs (21)
  • key (44-47)
  • key (69-72)
  • key (109-112)
  • key (151-154)
  • key (193-196)
  • key (239-242)
  • key (274-277)
  • apply (49-52)
  • apply (74-79)
  • apply (114-121)
  • apply (156-163)
  • apply (198-205)
  • apply (244-255)
  • apply (279-282)
  • undo (54-57)
  • undo (81-84)
  • undo (123-126)
  • undo (165-168)
  • undo (207-210)
  • undo (257-262)
  • undo (284-287)
crates/cardano/src/sweep/transition.rs (1)
  • visit_pool (176-194)
crates/cardano/src/pallas_extras.rs (1)
  • pool_reward_account (288-291)
crates/cardano/src/sweep/transition.rs (3)
crates/cardano/src/model.rs (4)
  • sweep (727-737)
  • key (1105-1135)
  • apply (1137-1167)
  • undo (1169-1199)
crates/cardano/src/sweep/mod.rs (4)
  • sweep (161-177)
  • visit_pool (33-40)
  • visit_account (43-50)
  • flush (63-65)
crates/cardano/src/sweep/retires.rs (15)
  • key (19-21)
  • key (57-59)
  • key (96-98)
  • key (134-136)
  • apply (23-32)
  • apply (61-74)
  • apply (100-109)
  • apply (138-151)
  • undo (34-43)
  • undo (76-85)
  • undo (111-120)
  • undo (153-162)
  • should_retire_pool (165-175)
  • visit_pool (195-220)
  • flush (254-260)
crates/cardano/src/sweep/mod.rs (6)
crates/cardano/src/sweep/commit.rs (2)
  • commit (122-148)
  • domain (97-97)
crates/cardano/src/sweep/compute.rs (4)
  • compute (294-349)
  • domain (302-304)
  • domain (314-316)
  • domain (326-328)
crates/cardano/src/sweep/retires.rs (3)
  • visit_pool (195-220)
  • visit_drep (222-252)
  • flush (254-260)
crates/cardano/src/sweep/rewards.rs (1)
  • visit_pool (160-215)
crates/cardano/src/sweep/transition.rs (3)
  • visit_pool (176-194)
  • visit_account (196-213)
  • flush (215-221)
crates/cardano/src/roll/epochs.rs (1)
  • flush (291-301)
crates/cardano/src/sweep/commit.rs (4)
crates/cardano/src/sweep/mod.rs (1)
  • sweep (161-177)
crates/cardano/src/sweep/compute.rs (3)
  • domain (302-304)
  • domain (314-316)
  • domain (326-328)
crates/cardano/src/sweep/loading.rs (1)
  • domain (53-55)
crates/core/src/state.rs (2)
  • commit (270-270)
  • commit (498-500)
crates/cardano/src/roll/pools.rs (3)
crates/cardano/src/model.rs (11)
  • roll (706-724)
  • new (228-249)
  • new (556-561)
  • new (836-845)
  • key (1105-1135)
  • from (866-868)
  • from (872-874)
  • from (878-880)
  • from (884-887)
  • apply (1137-1167)
  • undo (1169-1199)
crates/cardano/src/roll/accounts.rs (26)
  • new (26-32)
  • new (97-103)
  • new (139-145)
  • new (181-187)
  • new (225-233)
  • key (44-47)
  • key (69-72)
  • key (109-112)
  • key (151-154)
  • key (193-196)
  • key (239-242)
  • key (274-277)
  • apply (49-52)
  • apply (74-79)
  • apply (114-121)
  • apply (156-163)
  • apply (198-205)
  • apply (244-255)
  • apply (279-282)
  • undo (54-57)
  • undo (81-84)
  • undo (123-126)
  • undo (165-168)
  • undo (207-210)
  • undo (257-262)
  • undo (284-287)
crates/cardano/src/pallas_extras.rs (2)
  • cert_as_pool_registration (34-86)
  • pool_reward_account (288-291)
crates/cardano/src/sweep/loading.rs (5)
crates/cardano/src/model.rs (2)
  • drep_to_entity_key (801-811)
  • sweep (727-737)
crates/cardano/src/eras.rs (1)
  • load_active_era (182-197)
crates/cardano/src/utils.rs (1)
  • mutable_slots (12-15)
crates/cardano/src/sweep/mod.rs (1)
  • sweep (161-177)
crates/core/src/batch.rs (1)
  • default (19-24)
crates/cardano/src/sweep/compute.rs (9)
crates/cardano/src/model.rs (2)
  • sweep (727-737)
  • rewards (788-792)
crates/cardano/src/sweep/mod.rs (1)
  • sweep (161-177)
crates/cardano/src/sweep/commit.rs (1)
  • domain (97-97)
crates/cardano/src/sweep/loading.rs (1)
  • domain (53-55)
crates/minibf/src/routes/network.rs (1)
  • domain (191-200)
crates/minibf/src/routes/pools.rs (2)
  • domain (83-84)
  • domain (150-151)
crates/minibf/src/routes/epochs/mod.rs (1)
  • domain (26-28)
crates/cardano/src/eras.rs (1)
  • domain (183-185)
crates/cardano/src/lib.rs (3)
  • domain (136-138)
  • domain (156-158)
  • domain (164-166)
🔇 Additional comments (20)
crates/minibf/src/mapping.rs (1)

597-599: Switch to cert_as_pool_registration looks correct.

This aligns minibf’s deposit logic with the renamed helper and preserves semantics across eras.

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

231-233: Use of cert_as_pool_registration is consistent with the API rename.

Matches the updated helper and keeps the counting logic straightforward.

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

103-104: Initialize deltas with WorkDeltas::default() — good.

This matches the new delta-driven sweep flow.

crates/cardano/src/pallas_extras.rs (2)

34-86: Renamed helper cert_as_pool_registration is well-formed.

Covers both Alonzo-compatible and Conway PoolRegistration variants and normalizes fields.


288-291: Verify reward account bytes represent full address in all era variants
Confirm that every reward_account in MultiEraPoolRegistration encodes a complete stake/reward address (including network tag and credential) so Address::from_bytes(...).ok()? and address_as_stake_cred always yield the correct StakeCredential.

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

91-91: LGTM: Renaming aligns with blockchain terminology standards.

The rename from PoolRetirement to PoolDeRegistration better matches the terminology used in the codebase where "registration/deregistration" pairs are consistent (e.g., StakeRegistration/StakeDeregistration).


125-133: LGTM: Pool account initialization ensures rewards can be tracked.

The new PoolAccountDetected delta properly handles pool reward account initialization, which is essential since pool reward accounts don't go through standard stake registration. This ensures reward distribution works correctly for pool operators.

Also applies to: 189-199


148-150: Fix incorrect entity key derivation.

The function creates an EntityKey from account_key bytes but then wraps it again with EntityKey::from(). This double-wrapping is redundant since account_key is already an EntityKey.

Apply this fix:

 let account_key = minicbor::to_vec(&self.reward_account).unwrap();
-let account_key = EntityKey::from(account_key);
-debug!(operator=%self.operator, account_key=%account_key, "initializing pool account");
+debug!(operator=%self.operator, account_key=%hex::encode(&account_key), "initializing pool account");

Likely an incorrect or invalid review comment.

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

211-212: LGTM: Boolean flags for retirement state tracking.

Adding is_retired to PoolState and expired to DRepState provides clear state tracking for entity lifecycle management.

Also applies to: 831-832

crates/cardano/src/sweep/mod.rs (4)

3-10: LGTM!

The imports are properly organized and the new WorkDeltas type aligns well with the delta-driven architecture being introduced.


31-66: Well-designed visitor pattern implementation.

The BoundaryVisitor trait provides a clean abstraction for the sweep process with sensible defaults. The #[allow(unused_variables)] pragmas are appropriate for trait methods with default implementations.


150-158: Clean helper methods for BoundaryWork.

The starting_epoch_no() and add_delta() methods provide good encapsulation. The delta addition pattern is consistent with the new architecture.


166-166: compute retains genesis access

The new signature still calls domain.genesis() in define_era_transition, so all required genesis data remains available.

crates/cardano/src/sweep/retires.rs (4)

23-32: Good defensive programming with entity checks.

The pattern of checking for entity existence before applying changes and using appropriate logging levels (warn for missing, debug for operations) is well implemented.


207-217: Consider populating prev_pool_id for undo support.

The PoolDelegatorDrop instances are created with prev_pool_id: None, which means undo operations won't restore the previous pool delegation. This might be intentional for retirements, but consider whether preservation of undo information is needed.

-            self.deltas.push(
-                PoolDelegatorDrop {
-                    delegator: delegator.clone(),
-                    prev_pool_id: None,
-                }
-                .into(),
-            );
+            // For retirements, we intentionally don't preserve prev_pool_id
+            // as retired pools shouldn't be restored through undo
+            self.deltas.push(
+                PoolDelegatorDrop {
+                    delegator: delegator.clone(),
+                    prev_pool_id: None,
+                }
+                .into(),
+            );

177-187: Verify DRep inactivity period calculation
ctx.valid_drep_inactivity_period() delegates to PParams::ensure_drep_inactivity_period() in chain-libs; manually confirm it returns the exact on-chain drep inactivity period (correct units and defaults).


11-14: Naming distinction is intentional: PoolDeRegistration models the on-chain de-registration certificate in the roll pipeline, while PoolRetirement represents the subsequent sweep-level retirement event. No change required.

Likely an incorrect or invalid review comment.

crates/cardano/src/sweep/transition.rs (3)

89-144: Remove duplicate delta types.

As noted in the retires.rs review, DRepExpiration and DRepDelegatorDrop are duplicated between this file and retires.rs. This duplication should be resolved.


182-182: Confirmed get_pool_stake returns 0 for missing pools. No additional checks needed.


27-36: Document and verify AccountTransition.apply order
In crates/cardano/src/sweep/transition.rs’s AccountTransition::apply, add a // order matters comment (as in PoolTransition) and confirm that the sequence—

  1. active_pool = latest_pool
  2. active_drep = latest_drep
  3. active_stake = wait_stake
  4. wait_stake = live_stake()
    —accurately reflects Cardano’s epoch boundary rules.


let genesis =
Genesis::from_file_paths(byron, shelley, alonzo, conway, force_protocol).unwrap();
let domain = todo!();

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

Replace todo!() with proper test setup.

Using todo!() in tests will cause panics when the test runs. This should be replaced with a proper mock or test domain implementation.

The test needs a proper domain implementation. Consider creating a test-specific domain mock or using a test helper to create a domain instance.

🤖 Prompt for AI Agents
In crates/cardano/src/sweep/compute.rs around line 447, replace the placeholder
let domain = todo!() with a real test domain setup: create or reuse a
test-domain/mock that implements the same trait(s) the sweep code expects,
construct it with the minimal state needed for this test (UTxO set, script
contexts, keys, or config fields), import the test helper or mock module, and
instantiate domain before use; ensure the mock implements any async or trait
methods called by the code and populate fields so the test runs
deterministically instead of panicking.

Comment thread crates/cardano/src/sweep/retires.rs
Comment on lines +19 to +26
fn compute_delegator_reward(
available_rewards: u64,
total_delegated: u64,
delegator_stake: u64,
) -> u64 {
let share = (delegator_stake as f64 / total_delegated as f64) * available_rewards as f64;
share.round() as u64
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential precision loss in reward calculations.

Using floating-point arithmetic for financial calculations can lead to rounding errors. Consider using fixed-point arithmetic or the num-rational crate for precise calculations.

Consider using rational arithmetic similar to other parts of the codebase:

use num_rational::Rational64;

fn compute_delegator_reward(
    available_rewards: u64,
    total_delegated: u64,
    delegator_stake: u64,
) -> u64 {
    let numerator = (available_rewards as i128) * (delegator_stake as i128);
    let denominator = total_delegated as i128;
    ((numerator + denominator / 2) / denominator) as u64  // Round to nearest
}
🤖 Prompt for AI Agents
In crates/cardano/src/sweep/rewards.rs around lines 19 to 26, the current
implementation uses floating-point arithmetic which can lose precision for
financial reward calculations; replace the float-based computation with integer
(or rational) arithmetic to avoid rounding errors — compute numerator =
available_rewards * delegator_stake as a 128-bit integer, compute denominator =
total_delegated as 128-bit, then perform integer division with rounding to
nearest (add denominator/2 before dividing) and cast the result back to u64;
ensure you handle potential zero/overflow edge cases (e.g., total_delegated ==
0) and use signed 128-bit temporaries to match the example in the review
comment.

Comment on lines +36 to +50
let z0 = 1.0 / k as f64;
let sigma = pool_stake as f64 / total_active_stake as f64;
let s = pool.declared_pledge as f64 / total_active_stake as f64;
let sigma_prime = sigma.min(z0);

let r = total_rewards as f64;
let a0 = a0.numerator as f64 / a0.denominator as f64;
let r_pool = r * (sigma_prime + s.min(sigma) * a0 * (sigma_prime - sigma));

let r_pool_u64 = r_pool.round() as u64;
let after_cost = r_pool_u64.saturating_sub(pool.fixed_cost);
let pool_margin_cost = pool.margin_cost.numerator as f64 / pool.margin_cost.denominator as f64;
let operator_share = pool.fixed_cost + ((after_cost as f64) * pool_margin_cost).round() as u64;

(r_pool_u64, operator_share)

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

Add validation for protocol parameters.

The function should validate that k > 0 and a0 denominator is non-zero to prevent division by zero.

Add validation at the beginning of the function:

if k == 0 {
    return (0, 0);
}
if a0.denominator == 0 {
    return (0, 0);
}
🤖 Prompt for AI Agents
In crates/cardano/src/sweep/rewards.rs around lines 36 to 50, the code divides
by k and by a0.denominator without validating them, which can cause
division-by-zero panics; add early checks at the start of the function that if k
== 0 or a0.denominator == 0 then immediately return (0, 0), ensuring no further
calculations occur with invalid protocol params.

Comment thread crates/cardano/src/sweep/rewards.rs
Comment on lines +38 to +50
fn undo(&self, entity: &mut Option<AccountState>) {
let Some(entity) = entity else {
return;
};

entity.latest_drep = entity.active_drep.clone();
entity.latest_pool = entity.active_pool.clone();
entity.wait_stake = entity.active_stake;

entity.active_pool = self.prev_pool.clone();
entity.active_drep = self.prev_drep.clone();
entity.active_stake = self.prev_stake.unwrap_or(0);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential data loss in undo if prev values aren't captured.

The undo implementation relies on prev_pool, prev_drep, and prev_stake being set, but the apply method doesn't populate these fields. This could lead to incorrect state restoration.

The apply method should capture the previous values before modifying the entity:

 fn apply(&mut self, entity: &mut Option<AccountState>) {
     let Some(entity) = entity else {
         return;
     };

+    // Save current state for undo
+    self.prev_pool = entity.active_pool.clone();
+    self.prev_drep = entity.active_drep.clone(); 
+    self.prev_stake = Some(entity.active_stake);
+
     entity.active_pool = entity.latest_pool.clone();
     entity.active_drep = entity.latest_drep.clone();
     entity.active_stake = entity.wait_stake;
     entity.wait_stake = entity.live_stake();
 }
📝 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 {
return;
};
entity.latest_drep = entity.active_drep.clone();
entity.latest_pool = entity.active_pool.clone();
entity.wait_stake = entity.active_stake;
entity.active_pool = self.prev_pool.clone();
entity.active_drep = self.prev_drep.clone();
entity.active_stake = self.prev_stake.unwrap_or(0);
}
fn apply(&mut self, entity: &mut Option<AccountState>) {
let Some(entity) = entity else {
return;
};
// Save current state for undo
self.prev_pool = entity.active_pool.clone();
self.prev_drep = entity.active_drep.clone();
self.prev_stake = Some(entity.active_stake);
entity.active_pool = entity.latest_pool.clone();
entity.active_drep = entity.latest_drep.clone();
entity.active_stake = entity.wait_stake;
entity.wait_stake = entity.live_stake();
}
🤖 Prompt for AI Agents
In crates/cardano/src/sweep/transition.rs around lines 38 to 50, the undo method
assumes prev_pool, prev_drep and prev_stake were populated but apply does not
capture them, risking incorrect restoration; update the apply implementation to
read the entity's current active_pool, active_drep and active_stake and assign
them into self.prev_pool, self.prev_drep and self.prev_stake (wrap the stake in
Some), before mutating the entity fields, and ensure prev_* are Option<T>
semantics are respected so undo can reliably restore the prior values.

Comment thread crates/cardano/src/sweep/transition.rs
Comment thread crates/cardano/src/sweep/transition.rs Outdated
Comment on lines +146 to +168
fn should_retire_pool(ctx: &mut BoundaryWork, pool: &PoolState) -> bool {
if pool.is_retired {
return false;
}

let Some(retiring_epoch) = pool.retiring_epoch else {
return false;
};

retiring_epoch <= ctx.starting_epoch_no()
}

fn should_expire_drep(ctx: &mut BoundaryWork, drep: &DRepState) -> Result<bool, ChainError> {
let last_activity_slot = drep
.last_active_slot
.unwrap_or(drep.initial_slot.unwrap_or_default());

let (last_activity_epoch, _) = ctx.active_era.slot_epoch(last_activity_slot);

let expiring_epoch = last_activity_epoch as u64 + ctx.valid_drep_inactivity_period()?;

Ok(expiring_epoch <= ctx.starting_epoch_no())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider extracting shared helper functions.

The should_retire_pool and should_expire_drep functions are duplicated from retires.rs. Consider moving these to a shared module or the parent mod.rs.

Move these functions to crates/cardano/src/sweep/mod.rs as they're used by multiple visitor implementations:

-fn should_retire_pool(ctx: &mut BoundaryWork, pool: &PoolState) -> bool {
-    // implementation
-}
-
-fn should_expire_drep(ctx: &mut BoundaryWork, drep: &DRepState) -> Result<bool, ChainError> {
-    // implementation  
-}
+// Import from parent module
+use super::{should_retire_pool, should_expire_drep};

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

Comment thread crates/cardano/src/sweep/transition.rs Outdated

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2b3359 and 7ae517a.

📒 Files selected for processing (2)
  • crates/cardano/src/sweep/commit.rs (2 hunks)
  • crates/cardano/src/sweep/rewards.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
crates/cardano/src/sweep/rewards.rs (5)
crates/cardano/src/sweep/mod.rs (2)
  • sweep (161-177)
  • visit_pool (33-40)
crates/cardano/src/model.rs (10)
  • sweep (727-737)
  • k (618-620)
  • a0 (622-624)
  • key (1105-1135)
  • from (866-868)
  • from (872-874)
  • from (878-880)
  • from (884-887)
  • apply (1137-1167)
  • undo (1169-1199)
crates/cardano/src/roll/pools.rs (11)
  • key (39-42)
  • key (73-75)
  • key (99-101)
  • key (138-141)
  • apply (44-57)
  • apply (77-81)
  • apply (103-107)
  • undo (59-61)
  • undo (83-87)
  • undo (109-113)
  • undo (160-164)
crates/cardano/src/sweep/transition.rs (1)
  • visit_pool (176-194)
crates/cardano/src/pallas_extras.rs (1)
  • pool_reward_account (288-291)
crates/cardano/src/sweep/commit.rs (3)
crates/cardano/src/sweep/mod.rs (1)
  • sweep (161-177)
crates/cardano/src/sweep/loading.rs (1)
  • domain (53-55)
crates/cardano/src/sweep/compute.rs (3)
  • domain (302-304)
  • domain (314-316)
  • domain (326-328)
🔇 Additional comments (1)
crates/cardano/src/sweep/commit.rs (1)

131-133: Do not silently drop leftover deltas.

When self.deltas.entities stays non-empty we skip applying those deltas and still commit, so we lose state updates. Please surface this as an error (e.g., introduce a BrokenInvariant::UncommittedDeltas and return it) instead of logging a warning.

-        if !self.deltas.entities.is_empty() {
-            warn!(quantity = %self.deltas.entities.len(), "uncommitted deltas");
-        }
+        if !self.deltas.entities.is_empty() {
+            return Err(ChainError::from(BrokenInvariant::UncommittedDeltas));
+        }

Comment on lines +173 to +205
let (total_pool_reward, operator_share) = compute_pool_reward(
pot_delta.available_rewards,
ctx.active_snapshot.total_stake,
pool,
pool_stake,
ctx.valid_k()?,
&ctx.valid_a0()?,
);

self.total_rewards += total_pool_reward;

if let Some(pool_reward_account) = pallas_extras::pool_reward_account(&pool.reward_account)
{
debug!(pool=%id, "should assign pool rewards");

ctx.add_delta(AssignPoolRewards {
pool: id.clone(),
pool_reward_account,
operator_share,
});
} else {
warn!(pool=%id, "missing pool reward account");
}

let mut delegators = vec![];

for (delegator, stake) in ctx.active_snapshot.accounts_by_pool.iter_delegators(id) {
let reward = compute_delegator_reward(total_pool_reward, pool_stake, *stake);

delegators.push(AssignDelegatorRewards {
account: delegator.clone(),
reward,
});

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

Avoid double-counting rewards.

total_pool_reward already contains the operator’s share. We add that share to the pool account and then distribute the full total_pool_reward again to delegators, minting extra ADA. Clamp the operator share to the pool total and subtract it before computing delegator rewards.

-        let (total_pool_reward, operator_share) = compute_pool_reward(
+        let (total_pool_reward, mut operator_share) = compute_pool_reward(
             pot_delta.available_rewards,
             ctx.active_snapshot.total_stake,
             pool,
             pool_stake,
             ctx.valid_k()?,
             &ctx.valid_a0()?,
         );
 
         self.total_rewards += total_pool_reward;
 
-        if let Some(pool_reward_account) = pallas_extras::pool_reward_account(&pool.reward_account)
+        operator_share = operator_share.min(total_pool_reward);
+        let delegators_budget = total_pool_reward.saturating_sub(operator_share);
+
+        if let Some(pool_reward_account) = pallas_extras::pool_reward_account(&pool.reward_account)
         {
             debug!(pool=%id, "should assign pool rewards");
 
             ctx.add_delta(AssignPoolRewards {
                 pool: id.clone(),
                 pool_reward_account,
                 operator_share,
             });
         } else {
             warn!(pool=%id, "missing pool reward account");
         }
 
+        if pool_stake == 0 || delegators_budget == 0 {
+            return Ok(());
+        }
+
         let mut delegators = vec![];
 
         for (delegator, stake) in ctx.active_snapshot.accounts_by_pool.iter_delegators(id) {
-            let reward = compute_delegator_reward(total_pool_reward, pool_stake, *stake);
+            let reward = compute_delegator_reward(delegators_budget, pool_stake, *stake);
 
             delegators.push(AssignDelegatorRewards {
                 account: delegator.clone(),
                 reward,
             });
         }
📝 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
let (total_pool_reward, operator_share) = compute_pool_reward(
pot_delta.available_rewards,
ctx.active_snapshot.total_stake,
pool,
pool_stake,
ctx.valid_k()?,
&ctx.valid_a0()?,
);
self.total_rewards += total_pool_reward;
if let Some(pool_reward_account) = pallas_extras::pool_reward_account(&pool.reward_account)
{
debug!(pool=%id, "should assign pool rewards");
ctx.add_delta(AssignPoolRewards {
pool: id.clone(),
pool_reward_account,
operator_share,
});
} else {
warn!(pool=%id, "missing pool reward account");
}
let mut delegators = vec![];
for (delegator, stake) in ctx.active_snapshot.accounts_by_pool.iter_delegators(id) {
let reward = compute_delegator_reward(total_pool_reward, pool_stake, *stake);
delegators.push(AssignDelegatorRewards {
account: delegator.clone(),
reward,
});
let (total_pool_reward, mut operator_share) = compute_pool_reward(
pot_delta.available_rewards,
ctx.active_snapshot.total_stake,
pool,
pool_stake,
ctx.valid_k()?,
&ctx.valid_a0()?,
);
self.total_rewards += total_pool_reward;
// Clamp operator share and compute the budget for delegators
operator_share = operator_share.min(total_pool_reward);
let delegators_budget = total_pool_reward.saturating_sub(operator_share);
if let Some(pool_reward_account) = pallas_extras::pool_reward_account(&pool.reward_account)
{
debug!(pool=%id, "should assign pool rewards");
ctx.add_delta(AssignPoolRewards {
pool: id.clone(),
pool_reward_account,
operator_share,
});
} else {
warn!(pool=%id, "missing pool reward account");
}
// If there's no stake or no budget left, skip delegator distribution
if pool_stake == 0 || delegators_budget == 0 {
return Ok(());
}
let mut delegators = vec![];
for (delegator, stake) in ctx.active_snapshot.accounts_by_pool.iter_delegators(id) {
let reward = compute_delegator_reward(delegators_budget, pool_stake, *stake);
delegators.push(AssignDelegatorRewards {
account: delegator.clone(),
reward,
});
}
🤖 Prompt for AI Agents
In crates/cardano/src/sweep/rewards.rs around lines 173 to 205,
total_pool_reward currently includes the operator's share but is later used
intact to compute delegator rewards, causing double-counting; clamp
operator_share to at most total_pool_reward (operator_share =
operator_share.min(total_pool_reward)) and subtract it from the distributable
pool before computing delegator rewards (let distributable = total_pool_reward -
operator_share) then pass distributable (not total_pool_reward) into
compute_delegator_reward for each delegator; keep adding operator_share to the
pool account as before and ensure subtraction uses a saturating/min-safe
operation to avoid underflow.

@scarmuega
scarmuega merged commit 5e40a86 into main Sep 25, 2025
7 of 9 checks passed
@scarmuega
scarmuega deleted the refactor/delta-sweep branch September 25, 2025 01:16

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

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/roll/pools.rs (1)

90-114: Remove stale PoolRetirement entity delta

  • crates/cardano/src/sweep/retires.rs: rename or remove struct PoolRetirement and its EntityDelta impl
  • crates/cardano/src/model.rs: update import, enum variant, delta_from!, and apply/undo dispatch from PoolRetirement to PoolDeRegistration
crates/cardano/src/sweep/compute.rs (1)

252-256: Prevent u64 underflow when computing largest_stable_slot

epoch_start - nonce_stability_window can underflow for small epochs/windows. Use saturating_sub.

-            largest_stable_slot: self
-                .active_era
-                .epoch_start(self.ending_state.number as u64 + 2)
-                - nonce_stability_window(self.active_protocol.into(), genesis),
+            largest_stable_slot: {
+                let start = self
+                    .active_era
+                    .epoch_start(self.ending_state.number as u64 + 2);
+                start.saturating_sub(nonce_stability_window(self.active_protocol.into(), genesis))
+            },
🧹 Nitpick comments (4)
crates/cardano/src/sweep/loading.rs (1)

47-78: Account-driven snapshot load — LGTM

Iterating AccountState and populating ending/active snapshots looks correct.

Consider a unit test to assert snapshot tallies (pool/drep/total stake) for mixed accounts. I can draft one if useful.

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

143-153: Prefer Debug for EntityKey in tracing to avoid Display bound

Use ? for account_key to avoid requiring Display.

-                debug!(operator=%self.operator, account_key=%account_key, "initializing pool account");
+                debug!(operator=%self.operator, account_key=?account_key, "initializing pool account");
crates/cardano/src/sweep/compute.rs (1)

13-27: Consider widening ratio math to i128 to avoid intermediate overflow

reserves/fees can be large; Rational64 keeps headroom tight. Using Ratio for as_ratio!/into_ratio! and casting back at the end is safer.

If you prefer to keep i64, please confirm max inputs (reserves + fees + decayed_deposits) always fit within i63 during all epochs.

Also applies to: 35-56

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

79-96: Iter ergonomics: prefer flat_map over flatten (optional)

Option::into_iter().flatten() works; flat_map(|m| m.iter()) can be a tad clearer to some readers.

-    ) -> impl Iterator<Item = (&AccountId, &u64)> {
-        self.0.get(entity_id).into_iter().flatten()
+    ) -> impl Iterator<Item = (&AccountId, &u64)> {
+        self.0
+            .get(entity_id)
+            .into_iter()
+            .flat_map(|m| m.iter())
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ae517a and d11b76c.

📒 Files selected for processing (16)
  • crates/cardano/src/model.rs (10 hunks)
  • crates/cardano/src/pallas_extras.rs (2 hunks)
  • crates/cardano/src/roll/epochs.rs (1 hunks)
  • crates/cardano/src/roll/pools.rs (6 hunks)
  • crates/cardano/src/sweep/commit.rs (2 hunks)
  • crates/cardano/src/sweep/compute.rs (5 hunks)
  • crates/cardano/src/sweep/loading.rs (2 hunks)
  • crates/cardano/src/sweep/mod.rs (5 hunks)
  • crates/cardano/src/sweep/retires.rs (1 hunks)
  • crates/cardano/src/sweep/rewards.rs (1 hunks)
  • crates/cardano/src/sweep/transition.rs (1 hunks)
  • crates/core/src/batch.rs (1 hunks)
  • crates/minibf/src/mapping.rs (1 hunks)
  • crates/redb3/src/state/utxoset.rs (1 hunks)
  • crates/test-vectors/src/build.rs (9 hunks)
  • crates/testing/src/toy_domain.rs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • crates/cardano/src/pallas_extras.rs
  • crates/cardano/src/roll/epochs.rs
  • crates/core/src/batch.rs
  • crates/minibf/src/mapping.rs
  • crates/cardano/src/sweep/retires.rs
  • crates/cardano/src/sweep/rewards.rs
🧰 Additional context used
🧬 Code graph analysis (9)
crates/testing/src/toy_domain.rs (3)
crates/core/src/lib.rs (5)
  • state (622-622)
  • archive (623-623)
  • storage_config (617-617)
  • wal (621-621)
  • default (370-381)
src/adapters.rs (6)
  • state (73-75)
  • archive (77-79)
  • storage_config (85-87)
  • wal (69-71)
  • wal (102-102)
  • default (14-16)
crates/cardano/src/model.rs (1)
  • build_schema (1004-1015)
crates/cardano/src/model.rs (1)
crates/cardano/src/sweep/mod.rs (1)
  • sweep (160-176)
crates/cardano/src/roll/pools.rs (4)
crates/cardano/src/roll/accounts.rs (21)
  • key (44-47)
  • key (69-72)
  • key (109-112)
  • key (151-154)
  • key (193-196)
  • key (239-242)
  • key (274-277)
  • apply (49-52)
  • apply (74-79)
  • apply (114-121)
  • apply (156-163)
  • apply (198-205)
  • apply (244-255)
  • apply (279-282)
  • undo (54-57)
  • undo (81-84)
  • undo (123-126)
  • undo (165-168)
  • undo (207-210)
  • undo (257-262)
  • undo (284-287)
crates/cardano/src/sweep/rewards.rs (2)
  • key (63-66)
  • undo (79-88)
crates/core/src/state.rs (5)
  • from (16-21)
  • from (25-27)
  • from (31-33)
  • from (58-63)
  • from (77-79)
crates/cardano/src/pallas_extras.rs (2)
  • cert_as_pool_registration (34-86)
  • pool_reward_account (288-291)
crates/test-vectors/src/build.rs (1)
crates/cardano/src/sweep/commit.rs (1)
  • state (39-39)
crates/cardano/src/sweep/compute.rs (7)
crates/cardano/src/sweep/mod.rs (1)
  • sweep (160-176)
crates/cardano/src/model.rs (2)
  • sweep (733-743)
  • rewards (794-798)
crates/core/src/lib.rs (1)
  • genesis (618-618)
crates/cardano/src/sweep/commit.rs (1)
  • domain (97-97)
crates/cardano/src/sweep/loading.rs (1)
  • domain (51-53)
crates/cardano/src/eras.rs (1)
  • domain (201-203)
crates/cardano/src/lib.rs (3)
  • domain (144-146)
  • domain (164-166)
  • domain (172-174)
crates/cardano/src/sweep/transition.rs (5)
crates/cardano/src/sweep/mod.rs (4)
  • sweep (160-176)
  • visit_pool (33-40)
  • visit_account (43-50)
  • flush (63-65)
crates/cardano/src/model.rs (11)
  • sweep (733-743)
  • new (234-255)
  • new (562-567)
  • new (842-851)
  • key (1113-1143)
  • from (872-874)
  • from (878-880)
  • from (884-886)
  • from (890-893)
  • apply (1145-1175)
  • undo (1177-1207)
crates/cardano/src/roll/pools.rs (14)
  • new (27-33)
  • new (126-132)
  • key (39-42)
  • key (73-75)
  • key (99-101)
  • key (138-141)
  • apply (44-57)
  • apply (77-81)
  • apply (103-107)
  • apply (143-158)
  • undo (59-61)
  • undo (83-87)
  • undo (109-113)
  • undo (160-164)
crates/cardano/src/sweep/rewards.rs (7)
  • key (63-66)
  • key (100-102)
  • apply (68-77)
  • apply (104-113)
  • undo (79-88)
  • undo (115-124)
  • visit_pool (158-213)
crates/cardano/src/sweep/retires.rs (13)
  • key (19-21)
  • key (57-59)
  • key (96-98)
  • key (134-136)
  • apply (23-32)
  • apply (61-74)
  • apply (100-109)
  • undo (34-43)
  • undo (76-85)
  • undo (111-120)
  • undo (153-162)
  • visit_pool (195-220)
  • flush (254-260)
crates/cardano/src/sweep/mod.rs (7)
crates/cardano/src/sweep/commit.rs (2)
  • commit (122-144)
  • domain (97-97)
crates/cardano/src/sweep/compute.rs (4)
  • compute (300-355)
  • domain (308-310)
  • domain (320-322)
  • domain (332-334)
crates/cardano/src/sweep/transition.rs (3)
  • visit_pool (125-137)
  • visit_account (139-148)
  • flush (150-156)
crates/cardano/src/sweep/rewards.rs (1)
  • visit_pool (158-213)
crates/cardano/src/sweep/retires.rs (3)
  • visit_pool (195-220)
  • visit_drep (222-252)
  • flush (254-260)
crates/cardano/src/roll/epochs.rs (1)
  • flush (291-301)
crates/cardano/src/sweep/loading.rs (1)
  • domain (51-53)
crates/cardano/src/sweep/commit.rs (3)
crates/cardano/src/sweep/mod.rs (1)
  • sweep (160-176)
crates/cardano/src/sweep/loading.rs (1)
  • domain (51-53)
crates/cardano/src/sweep/compute.rs (3)
  • domain (308-310)
  • domain (320-322)
  • domain (332-334)
crates/cardano/src/sweep/loading.rs (3)
crates/cardano/src/model.rs (1)
  • drep_to_entity_key (807-817)
crates/cardano/src/eras.rs (1)
  • load_active_era (200-215)
crates/core/src/batch.rs (1)
  • default (19-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check Build
🔇 Additional comments (26)
crates/test-vectors/src/build.rs (9)

167-169: Signature updated to dolos_redb3::state::StateStore — LGTM

Matches the refactor.


262-264: Duplicate: state param type update OK

Same change as in handle_account_state.


333-334: Duplicate: state param type update OK

Same change as in handle_account_state.


445-446: Duplicate: state param type update OK

Same change as in handle_account_state.


645-646: Duplicate: state param type update OK

Same change as in handle_account_state.


813-814: Duplicate: state param type update OK

Same change as in handle_account_state.


586-589: PoolState.is_retired properly defined and updated
PoolState declares is_retired: bool (default false), and sweep/retires.rs sets it to true on retire and back to false on undo.


118-123: Approve code changes – no remaining references to dolos_redb3::StateStore found.


870-873: No action needed: DRepState.expired default correct
Default expired: bool initializes to false, and the sweep logic in sweep/retires.rs sets it to true on expiration as expected.

crates/testing/src/toy_domain.rs (5)

8-10: Seed helper now uses dolos_redb3::state::StateStore::in_memory — LGTM


78-84: ToyDomain storage types updated (state/archive) — LGTM

Matches the new domain-oriented storage split.


93-95: State initialization path updated — LGTM


104-107: Archive initialized via dolos_redb3::archive::ArchiveStore — LGTM


141-146: No lingering old associated-type definitions detected — LGTM
Ran grep for dolos_redb3::StateStore and dolos_redb3::archive::ChainStore; no occurrences found.

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

116-165: New PoolAccountDetected delta — LGTM

Keying by minicbor-encoded StakeCredential matches existing account key convention; undo path is correct.

crates/redb3/src/state/utxoset.rs (1)

442-442: Approve import path alignment
No lingering old imports for StateStore; using crate::state::StateStore is consistent.

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

80-105: Approve code changes — no residual references to removed per-entity fields

crates/cardano/src/sweep/compute.rs (1)

452-455: Replace todo!() with a real Domain test setup (duplicate)

This test will panic. Provide a minimal test domain/mock implementing Domain used by compute().

I can sketch a minimal in-memory Domain mock for this test if helpful.

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

1019-1046: LGTM: CardanoDelta variants extended coherently

New variants align with the delta-driven sweep design.

Please confirm external routing (apply/undo) is updated anywhere else these variants are matched over (if any).


1082-1109: LGTM: delta_from mappings are complete and consistent

All new variants are covered, avoiding construction friction.


1113-1142: LGTM: key() routing includes new variants

Key computation appears exhaustive for the expanded enum.


1145-1174: LGTM: apply() routing updated

Downcast apply paths wired for new variants.


1177-1206: LGTM: undo() routing updated

Downcast undo paths present for all new variants.

crates/cardano/src/sweep/mod.rs (3)

31-66: LGTM: BoundaryVisitor abstraction

Clear extensibility point; default no-ops keep visitor wiring lightweight.


149-157: LGTM: deltas batching helpers

starting_epoch_no and add_delta keep call sites clean; aligns with WorkDeltas usage.


165-168: LGTM: domain-centric compute/commit integration

Switching to compute(domain) and commit(domain) matches the new visitor/deltas flow.

Comment on lines +217 to 219
#[n(14)]
pub is_retired: bool,
}

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

🧩 Analysis chain

Backwards-compat risk: adding non-optional field to CBOR struct PoolState

New field is_retired: bool may break decoding older states unless minicbor supplies a default. If minicbor doesn’t default missing fields, consider making it Option (unwrap_or(false)) or annotate with a decode-default attribute.

Also applies to: 253-254


🌐 Web query:

minicbor derive: how to provide a default when a struct field is missing during decoding (e.g., attribute to default absent fields)?

💡 Result:

Use the derive attributes provided by minicbor_derive:

  • #[cbor(default)] on a field — when the field is missing during decoding the field is set to Default::default() for its type. Field type must implement Default. [1]

  • #[cbor(skip)] on a field — that field is not encoded and is initialised with Default::default() when decoding. Field type must implement Default. [1]

  • Alternatively make the field Option; absent -> None (optional fields default to None). [2]

  • For custom logic, use #[cbor(decode_with = "")] to run your own decode function for that field. [1]

References: minicbor_derive docs (attributes: default, skip, decode_with; optional fields behavior). [1] [2]


Annotate new is_retired fields with #[cbor(default)]
Missing fields will default to false on decode; apply to the is_retired: bool definitions at lines 217–219 and 253–254.

🤖 Prompt for AI Agents
In crates/cardano/src/model.rs around lines 217–219 and 253–254, the new boolean
field is_retired must be annotated with #[cbor(default)] so missing fields
decode to false; update both is_retired: bool declarations to add the
#[cbor(default)] attribute immediately above the field (same style as other cbor
defaults) so decoding absent values yields false.

Comment on lines +837 to 839
#[n(5)]
pub expired: bool,
}

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

🧩 Analysis chain

Backwards-compat risk: DRepState.expired added as non-optional

Same concern as PoolState. Verify minicbor’s behavior or switch to Option/decode default to avoid breaking previously persisted states.

Also applies to: 849-850


🌐 Web query:

With minicbor's #[derive(Decode)], how to mark a field optional or give it a default when absent in the encoded map/array?

💡 Result:

Use Option for optional fields (missing → None), or annotate a field with #[cbor(default)] / #[cbor(skip)] to get Default::default() when the entry is absent.

Examples:

  • Optional: field: Option — if missing during Decode it becomes None. [1]
  • Default value: #[cbor(default)] field: String — if missing it is set to String::default() (same for any type implementing Default). [1][2]
  • Skip encoding + default on decode: #[cbor(skip)] field: T (T: Default) — not encoded; decoded as Default::default(). [1]

For custom decoding/default logic use #[cbor(decode_with = "")] and implement your own decoder. [1]

Sources: minicbor derive docs. [1][2]

References
[1] minicbor-derive docs (attributes: default, skip, decode_with).
[2] minicbor-derive overview (Option defaults to None; behavior for maps/arrays).


Annotate new expired fields as optional or provide a default

In both DRepState (lines 837–839) and PoolState (849–850), add #[cbor(default)] to expired: bool (or change its type to Option<bool>) so missing entries decode to false (or None) and avoid breaking existing persisted data.

🤖 Prompt for AI Agents
In crates/cardano/src/model.rs around lines 837–839 (DRepState.expired) and
849–850 (PoolState.expired), the new expired: bool field will break decoding of
existing persisted data; add the serde-cbor default annotation by applying
#[cbor(default)] to both expired fields so missing entries decode to false
(alternatively change the field type to Option<bool> if you prefer explicit None
handling).

Comment on lines +191 to 200
if let Some(cert) = pallas_extras::cert_as_pool_registration(cert) {
deltas.add_for_entity(PoolRegistration::new(block.slot(), cert.clone()));

// Reward accounts for pool don't need to go through the standard stake
// registration process. This is why we need to track the account directly on
// pool registration.

let cred = pallas_extras::pool_reward_account(&cert.reward_account).unwrap();
deltas.add_for_entity(PoolAccountDetected::new(cert.operator, cred));
}

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

Avoid panic on invalid reward account; handle None from pool_reward_account

unwrap() can crash on malformed on-chain data. Guard and warn instead.

Apply:

-            let cred = pallas_extras::pool_reward_account(&cert.reward_account).unwrap();
-            deltas.add_for_entity(PoolAccountDetected::new(cert.operator, cred));
+            if let Some(cred) = pallas_extras::pool_reward_account(&cert.reward_account) {
+                deltas.add_for_entity(PoolAccountDetected::new(cert.operator, cred));
+            } else {
+                warn!(operator=%cert.operator, "invalid pool reward account; skipping PoolAccountDetected");
+            }

And add warn to imports:

-use tracing::{debug, trace};
+use tracing::{debug, trace, warn};
🤖 Prompt for AI Agents
In crates/cardano/src/roll/pools.rs around lines 191 to 200, the code calls
pallas_extras::pool_reward_account(...).unwrap(), which may panic on malformed
on-chain data; change this to handle the Option return value safely by matching
or using if let Some(cred) =
pallas_extras::pool_reward_account(&cert.reward_account) {
deltas.add_for_entity(PoolAccountDetected::new(cert.operator, cred)); } else {
log a warning including the problematic cert or reward_account bytes/context and
skip adding the PoolAccountDetected }, and add the warn log import (e.g., use
tracing::warn or whatever logging crate is used) to the imports.

Comment on lines +131 to +133
if !self.deltas.entities.is_empty() {
warn!(quantity = %self.deltas.entities.len(), "uncommitted deltas");
}

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

Leftover deltas are discarded instead of applied.

After the namespace walk we can exit with entries still sitting in self.deltas.entities. Those correspond to keys that didn’t exist in the store (e.g. pool reward accounts) and never get applied—we simply log and drop them, losing state updates. We must consume and persist those deltas (or fail hard) rather than continue.

Please extend apply_whole_namespace so every remaining entry for that namespace is applied against None (creating the entity when needed) and removed from the map, or return an error if anything is left. Leaving this as a warning will corrupt ledger state.

🤖 Prompt for AI Agents
In crates/cardano/src/sweep/commit.rs around lines 131 to 133, leftover entries
in self.deltas.entities are only warned-about and dropped, which loses state;
change apply_whole_namespace so after the namespace walk you iterate over every
remaining entry for that namespace, call the existing apply logic with a None
base (so the entity is created when needed), persist the result and remove the
entry from self.deltas.entities; if any apply/persist fails return an Err
instead of continuing, ensuring no deltas are silently discarded.

Comment on lines +44 to +46
self.prev_pool = entity.latest_pool.clone();
self.prev_drep = entity.latest_drep.clone();
self.prev_stake = Some(entity.active_stake);

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

Undo currently restores the latest delegation instead of the prior active one.

We’re snapshotting latest_* before mutating, so undo puts the account back into the latest delegation instead of the real pre-rotation active delegation/stake. That breaks rollback correctness. Snapshot the active fields instead.

-        self.prev_pool = entity.latest_pool.clone();
-        self.prev_drep = entity.latest_drep.clone();
+        self.prev_pool = entity.active_pool.clone();
+        self.prev_drep = entity.active_drep.clone();
         self.prev_stake = Some(entity.active_stake);
🤖 Prompt for AI Agents
In crates/cardano/src/sweep/transition.rs around lines 44 to 46, the code
snapshots entity.latest_pool/latest_drep so undo restores the latest delegation
instead of the prior active one; change the snapshots to use
entity.active_pool.clone() and entity.active_drep.clone() (keep prev_stake =
Some(entity.active_stake) as-is), ensuring you clone the active fields so undo
will restore the true pre-rotation active delegation/stake.

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