Skip to content

feat(cardano): committee, constitution and governance singleton state - #1130

Open
scarmuega wants to merge 4 commits into
mainfrom
feat/governance-gov-state
Open

feat(cardano): committee, constitution and governance singleton state#1130
scarmuega wants to merge 4 commits into
mainfrom
feat/governance-gov-state

Conversation

@scarmuega

@scarmuega scarmuega commented Jul 25, 2026

Copy link
Copy Markdown
Member

Plan

Phase 3 of the Trellis plan plans/dolos-governance-gaps.md (Brain/txpipe, commit 35819c5): Committee, constitution, gov singleton — new GovState entity; handle the two committee certificates; epoch-based DRep expiry + dormancy bookkeeping on DRepState. Technical spec: solution/dolos/kb/cardano-ledger-governance-gaps/dolos-implementation-design.md (§3.2, §3.3, §4 roll-time deltas, §5.1); semantics source: haskell-governance-research.md (§2.6, §3.1, §3.3), cross-checked against IntersectMBO/cardano-ledger (Conway/Rules/GovCert.hs, Conway/Rules/Certs.hs).

Done criterion (phase 3) — met: the GovState singleton exists and is seeded, both committee certificates mutate it, and DRepState carries Haskell-style epoch-based expiry with dormancy-release bookkeeping. Ratification-side consumers of this state (tallies, dormancy tick, committee GC, roots updates) are phases 4–5 by design.

What changed

New GovState entity (ns "gov", key b"0")model/gov.rs

  • constitution, committee (cold cred → term epoch, threshold), committee_auths (slot-stamped append-only histories, as-of readable), prev_gov_action_ids (four per-purpose roots, written by phase 5), num_dormant_epochs.
  • Seeded from the Conway genesis at the era boundary entering protocol 9 (GovGenesisInit, emitted in ESTART finalize with a targeted commit path that tolerates the entity not existing) and at bootstrap for force_protocol >= 9 devnets. Stores upgraded in place past Chang get no seed — stale data would be worse than the documented "history absent" gap (design §6).

Committee certificatesroll/dreps.rs, pallas_extras.rs

  • AuthCommitteeHotCommitteeAuth; ResignCommitteeColdCommitteeResign; both append to the cold credential's history. Valid (phase-2) txs only, matching the phase-2 precedent for new visitors.

Epoch-based DRep expirymodel/dreps.rs, roll/dreps.rs, ewrap/loading.rs

  • New DRepState.expiry: Option<DRepExpiry> at CBOR index 8: (current, updated_in, prev) — stored without dormancy credit exactly like Haskell's drepExpiry; the epoch-rotated pair gives the as-of-previous-boundary read phase 5's tally needs.
  • DRepExpiryUpdate emitted on RegDRepCert (PV9 bootstrap ignores dormancy credit, per computeDRepExpiryVersioned), UpdateDRepCert, and every DRep vote (only_if_registered, mirroring the Haskell Map.adjust). A refresh clears the expired flag (expiry is implicit in the ledger).
  • EWRAP's should_expire_drep now uses stored + num_dormant_epochs < starting_epoch (exact ledger semantics; the old slot heuristic remains as fallback for pre-upgrade rows, which self-heal).

Dormancy releaseroll/dreps.rs, roll/mod.rs

  • First proposal after a dormant stretch: per-DRep DRepDormancyRelease fan-out (skips long-expired and legacy rows) + GovDormancyReset. Runs before certs/votes of the same tx — verified against the Haskell CERTS rule, whose terminal case runs first. The counter is threaded through the batch so later blocks see the reset. (The counter increment is the EPOCH-rule dormancy tick — phase 5 per the plan; until then the counter stays 0 and the release machinery is dormant but exact.)

Frozen-format discipline (design §6)

  • All CardanoDelta variants appended after VoteCast; no existing variant or field layout touched.
  • Entity changes are Option/defaulted additions at unused CBOR indexes (DRepState index 8; "gov" is a fresh namespace, no migration burden). Compat tests encode pre-phase-3 row replicas and decode them with the new fields empty.
  • New namespace creation on existing stores verified: redb opens/creates all schema tables in initialize_schema; fjall uses a unified keyspace with hashed namespace prefixes (no per-namespace setup).

Coder interpretations to sample

  • Design §9.2 (open point): snapshot-safe representation. Chose the epoch-rotated (value, updated_in, prev) record for DRep expiry (avoids coupling DRepState to the ESTART EpochValue rotation pass) and slot-stamped histories for committee auths (named directly by design §3.3). Both alternatives deliver identical observable behavior; this is the implementation reading, recorded here for review.
  • Committee.members is a BTreeMap (Haskell: Map ColdCred EpochNo) rather than the design sketch's Vec<(cold, epoch)> — dedup + lookup semantics match the ledger; encodes fine.
  • Design's DRepUpdate { anchor, expiry_refresh } is realized as the existing phase-1 DRepAnchorUpdate plus the new DRepExpiryUpdate; the design's DormancyFold is the sanctioned per-DRep fan-out (DRepDormancyRelease + GovDormancyReset).
  • Genesis seeding at the Chang boundary is included in this phase (the phase title names the constitution, and nothing later ever creates initial committee/constitution values); deliberately not applied to in-place-upgraded stores.

Verification

  • cargo +nightly fmt --all -- --check — clean.
  • cargo clippy --all-targets --all-features -- -D warnings — clean.
  • cargo test --workspace --all-targets — all suites pass locally (new: entity CBOR round-trips, legacy-row decode compat for DRepState and ProposalState, delta apply/undo + WAL-serde proptests for all six new deltas, DRepExpiry rotation/as-of unit tests, dormancy fold semantics incl. no-resurrect, mainnet Conway genesis mapping).
  • Not run locally: the --ignored network/OCI suites (smoke, sync, snapshot, epoch fixtures) — CI covers these; the epoch fixtures are the only end-to-end exercise of the Chang-boundary seeding path.

Escalations

None.

Breaking note for dev stores

The gov singleton's existence is now an invariant (created at bootstrap for every store, active_since flags pre-Conway vs active, and a one-time CARDANO-006 startup migration covers stores bootstrapped before the invariant). Stores synced with earlier commits of this branch must be wiped: the WAL shapes of GovGenesisInit/CommitteeAuth/CommitteeResign/GovDormancyReset changed, and pre-existing gov rows carry no active_since. No released store is affected — these shapes never shipped. In-place-upgraded stores past Chang get the row from the migration with enact-state unset; complete governance content still requires a fresh sync.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added governance singleton tracking (constitution, committee, auth/resign history, dormancy counter) and initialization at Conway era entry, including forced-protocol Conway networks.
    • Introduced epoch-based DRep expiry with automatic rotation and dormancy-aware expiry updates/releases.
    • Added singleton-based loading/application for epoch and governance state, plus improved Conway entry detection for era summaries.
  • Bug Fixes

    • Ensured governance and DRep-related updates apply correctly across era boundaries and only for valid transactions/certificates.
    • Added a startup self-heal/migration to create missing governance singleton state and a clearer “missing governance” error when needed.
    • Added targeted unit test coverage for governance and DRep singleton-delta behaviors.

… state

Phase 3 of the Conway governance work: introduce the `GovState` singleton
entity (ns "gov") carrying the enacted constitution, committee, committee
hot-key authorization histories, per-purpose governance roots, and the
dormant-epoch counter; handle the two committee certificates; and move
DRep expiry to epoch-based, Haskell-style bookkeeping on `DRepState`.

- `GovState` seeded from the Conway genesis at the Chang era boundary
  (`GovGenesisInit` in ESTART finalize) or at bootstrap for networks that
  force-start in Conway; committee auths tracked as slot-stamped
  histories so boundary tallies can read them as-of a past slot.
- `AuthCommitteeHot` / `ResignCommitteeCold` certificates now emit
  `CommitteeAuth` / `CommitteeResign` deltas (valid txs only).
- `DRepState.expiry` (new Option field at CBOR index 8): stored without
  dormancy credit, refreshed on registration (with the PV9 bootstrap
  exception), update certs, and DRep votes; snapshot-safe via an
  epoch-rotated `(current, updated_in, prev)` record.
- Dormancy release (research §3.3.1): the first proposal after a dormant
  stretch fans out `DRepDormancyRelease` over the dreps namespace and
  resets the counter (`GovDormancyReset`), matching the Haskell CERTS
  rule ordering (release before certs/votes of the same tx).
- `should_expire_drep` at EWRAP now compares the stored epoch expiry
  (+ dormancy credit) exactly as the ledger does, falling back to the
  legacy slot heuristic for pre-upgrade rows.

Entity changes are backward-compatible `Option` additions at unused CBOR
indexes; all `CardanoDelta` variants are appended, never reordered. Old
rows decode with the new fields empty (covered by compat tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds persisted governance state, Conway committee handling, epoch-based DRep expiry, dormancy propagation, singleton key abstractions, and lifecycle integration across genesis, Conway transitions, rolling, boundary processing, migration, and finalization.

Changes

Governance and DRep lifecycle

Layer / File(s) Summary
Governance state and delta model
crates/cardano/src/model/gov.rs, crates/cardano/src/model/mod.rs, crates/cardano/src/pallas_extras.rs
Adds GovState, committee authorization histories, governance deltas, singleton/entity dispatch, schema registration, and Conway committee certificate parsers.
DRep expiry persistence and maintenance
crates/cardano/src/model/dreps.rs
Adds epoch-based expiry storage, expiry and dormancy deltas, undo behavior, legacy CBOR compatibility, and roundtrip tests.
DRep rolling and dormancy propagation
crates/cardano/src/roll/dreps.rs, crates/cardano/src/roll/mod.rs
Carries dormancy context across blocks and emits DRep expiry, dormancy-release, reset, and committee effects under transaction validity rules.
Conway initialization and singleton lifecycle
crates/cardano/src/genesis/*, crates/cardano/src/estart/*, crates/cardano/src/ewrap/*, crates/cardano/src/lib.rs, crates/cardano/src/migrate.rs, crates/cardano/src/model/epochs.rs, crates/cardano/src/model/eras.rs, crates/cardano/src/rupd/*
Seeds governance at genesis or the Conway boundary, migrates missing governance state, reads and applies singleton state, standardizes epoch keying, and incorporates dormant epochs into boundary DRep expiry checks.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GenesisOrBoundary
  participant GovState
  participant DeltaBuilder
  participant DRepStateVisitor
  participant Finalizer
  GenesisOrBoundary->>GovState: seed Conway constitution and committee
  DeltaBuilder->>GovState: read dormant epoch state
  DeltaBuilder->>DRepStateVisitor: provide dormancy context
  DRepStateVisitor->>GovState: emit dormancy reset and governance deltas
  Finalizer->>GovState: apply queued singleton deltas
Loading

Possibly related PRs

  • txpipe/dolos#688: Both changes modify singleton keying and EpochState delta wiring.
  • txpipe/dolos#703: Both changes modify DRep rolling and emitted DRep-related deltas.
  • txpipe/dolos#846: Both changes modify the Cardano commit pipeline and namespace/singleton update handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: adding governance singleton state for committee and constitution handling.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/governance-gov-state

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.

@scarmuega
scarmuega marked this pull request as ready for review July 25, 2026 16:05

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

🧹 Nitpick comments (4)
crates/cardano/src/model/dreps.rs (1)

27-83: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

set assumes non-decreasing epochs.

If called with epoch < updated_in (an out-of-order or replayed write) the rotation clobbers prev with the newer current and moves updated_in backwards, corrupting the one-epoch look-back. Chain-ordered application makes this unreachable today, but a debug_assert!(epoch >= self.updated_in) would pin the invariant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cardano/src/model/dreps.rs` around lines 27 - 83, Add a debug_assert!
at the start of DRepExpiry::set enforcing that the incoming epoch is greater
than or equal to self.updated_in, before any rotation or state mutation;
preserve the existing update behavior for valid non-decreasing epochs.
crates/cardano/src/model/gov.rs (2)

316-354: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

pop_auth pops then removes when the entry was created.

When created is true the history has exactly one entry, so the pop() is dead work; harmless but slightly obscures intent. Consider early-returning on created.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cardano/src/model/gov.rs` around lines 316 - 354, Update pop_auth to
handle created entries with an early return after removing the cold key,
avoiding the unnecessary history.pop() when created is true. Preserve the
existing was_new handling and normal history pop behavior for entries that were
not newly created.

139-150: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

committee_auth_as_of silently depends on ascending slot order.

The reverse scan returns the first entry with at <= slot, which is only the effective authorization if the history is sorted by slot. Appends are naturally monotonic during rolling, but nothing enforces it (and any_auth_history generates unsorted slots). A debug assertion or a max_by_key fallback would make the invariant explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cardano/src/model/gov.rs` around lines 139 - 150, Update Committee
authorization history handling in committee_auth_as_of to avoid silently relying
on ascending slot order: either enforce the ordering invariant with a debug
assertion at history creation/update points, or select the authorization with
the greatest at value not exceeding slot via a max-by-key fallback. Account for
unsorted histories produced by any_auth_history while preserving the as-of
behavior.
crates/cardano/src/roll/mod.rs (1)

580-589: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Key-only scan decodes every DRepState needlessly.

iter_entities_typed decodes each value just to discard it; iter_entities (raw) would give the same keys without the CBOR work. Only hit when a dormant stretch is active, so low priority.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cardano/src/roll/mod.rs` around lines 580 - 589, Update the
dormant-stretch branch in the roll logic to use the raw state iteration method
instead of iter_entities_typed::<DRepState>, collecting each record’s key
without decoding DRepState values. Preserve the existing error propagation and
assignment to dormancy.drep_keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cardano/src/ewrap/loading.rs`:
- Around line 275-283: Update the GovState::num_dormant_epochs transition logic
at epoch/transaction boundaries so it increments when no live proposals are
present, rather than only resetting. Preserve reset behavior when new proposals
occur, and ensure the updated counter is used by the expiry adjustment and
DRepDormancyRelease fan-out across consecutive zero-proposal epochs and
transactions.

In `@crates/cardano/src/roll/dreps.rs`:
- Around line 124-142: Update the dormancy-release handling around
DRepDormancyRelease to track keys registered by RegDRepCert certificates during
the batch crawl and union them with self.dormancy.drep_keys before fan-out.
Ensure newly registered DReps receive the same release delta using the
pre-release dormant epoch count, while preserving the existing reset behavior
and excluding invalid transactions.

---

Nitpick comments:
In `@crates/cardano/src/model/dreps.rs`:
- Around line 27-83: Add a debug_assert! at the start of DRepExpiry::set
enforcing that the incoming epoch is greater than or equal to self.updated_in,
before any rotation or state mutation; preserve the existing update behavior for
valid non-decreasing epochs.

In `@crates/cardano/src/model/gov.rs`:
- Around line 316-354: Update pop_auth to handle created entries with an early
return after removing the cold key, avoiding the unnecessary history.pop() when
created is true. Preserve the existing was_new handling and normal history pop
behavior for entries that were not newly created.
- Around line 139-150: Update Committee authorization history handling in
committee_auth_as_of to avoid silently relying on ascending slot order: either
enforce the ordering invariant with a debug assertion at history creation/update
points, or select the authorization with the greatest at value not exceeding
slot via a max-by-key fallback. Account for unsorted histories produced by
any_auth_history while preserving the as-of behavior.

In `@crates/cardano/src/roll/mod.rs`:
- Around line 580-589: Update the dormant-stretch branch in the roll logic to
use the raw state iteration method instead of iter_entities_typed::<DRepState>,
collecting each record’s key without decoding DRepState values. Preserve the
existing error propagation and assignment to dormancy.drep_keys.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ac779916-b868-4c97-93c4-6fd976ee3d00

📥 Commits

Reviewing files that changed from the base of the PR and between 11ee34d and 481f042.

📒 Files selected for processing (12)
  • crates/cardano/src/estart/commit.rs
  • crates/cardano/src/estart/loading.rs
  • crates/cardano/src/ewrap/loading.rs
  • crates/cardano/src/ewrap/mod.rs
  • crates/cardano/src/genesis/mod.rs
  • crates/cardano/src/model/dreps.rs
  • crates/cardano/src/model/eras.rs
  • crates/cardano/src/model/gov.rs
  • crates/cardano/src/model/mod.rs
  • crates/cardano/src/pallas_extras.rs
  • crates/cardano/src/roll/dreps.rs
  • crates/cardano/src/roll/mod.rs

Comment on lines +275 to +283
// Epoch-based expiry, Haskell-style: the stored value carries no
// dormancy credit, so add the counter back. A DRep is active while
// `epoch <= expiry + dormant`, i.e. it expires entering the first
// epoch strictly greater.
if let Some(expiry) = &drep.expiry {
let actual = expiry.current + self.num_dormant_epochs;

return Ok(actual < self.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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Any writer of num_dormant_epochs other than the reset delta
rg -nP -C4 'num_dormant_epochs' crates/cardano/src

Repository: txpipe/dolos

Length of output: 8729


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Files referencing GovDormancyReset / num_dormant_epochs in model/gov.rs =="
rg -n -C 6 'GovDormancyReset|num_dormant_epochs|DRepDormancyRelease' crates/cardano/src/model/gov.rs

echo
echo "== Write/delete/usages in cardano crate =="
rg -n 'num_dormant_epochs|GovState' crates/cardano/src | sed -n '1,220p'

echo
 echo "== Candidate model functions around GovDormancyReset =="
sed -n '360,490p' crates/cardano/src/model/gov.rs

Repository: txpipe/dolos

Length of output: 12842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== All references to GovDormancyReset =="
rg -n -C 4 'GovDormancyReset|DRepDormancyRelease' crates/cardano/src || true

echo
echo "== Cardano model delta variants involving gov/dreps =="
sed -n '60,185p' crates/cardano/src/model/mod.rs

Repository: txpipe/dolos

Length of output: 22229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== roll modulo declarations for dreps/governance =="
sed -n '1,120p' crates/cardano/src/roll/mod.rs
echo
sed -n '120,230p' crates/cardato/src/roll/mod.rs 2>/dev/null || sed -n '120,230p' crates/cardano/src/roll/mod.rs

echo
echo "== All references to DormancyContext / dormant_epochs =="
rg -n -C 4 'DormancyContext|dormant_epochs' crates/cardano/src

Repository: txpipe/dolos

Length of output: 24989


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DRepStateVisitor proposal/cert handling =="
sed -n '110,175p' crates/cardano/src/roll/dreps.rs

echo
echo "== Any non-test mutation of num_dormant_epochs/dormant_epochs in .rs =="
python3 - <<'PY'
from pathlib import Path
import re
for path in sorted(Path('crates/cardano/src').rglob('*.rs')):
    text = path.read_text()
    if re.search(r'\bnum_dormant_epochs\s*=\s*|^\.num_dormant_epochs\s*=', text, re.M) or re.search(r'\bdormant_epochs\s*=\s*|^\.dormant_epochs\s*=', text, re.M):
        lines = text.splitlines()
        for i,line in enumerate(lines,1):
            if re.search(r'num_dormant_epochs\s*[+\-*/&^<>=]=|dormant_epochs\s*[+\-*/&^<>=]=', line) or (re.search(r'^\s*(num_dormant_epochs|dormant_epochs)\s*=', line) and 'let mut' in text[:len(''.join(lines[:i]))].count('let mut') % 2 == 0):
                print(f'{path}:{i}:{line}')
PY

Repository: txpipe/dolos

Length of output: 2581


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== All write sites for the relevant dormant counters =="
rg -n -C 3 'num_dormant_epochs\s*[+\-*/&^<>=]?=\s*|dormant_epochs\s*[+\-*/&^<>=]?=\s*' crates/cardano/src

echo
echo "== DeltaBuilder construction / iteration around BlockVisitor calls =="
sed -n '230,320p' crates/cardano/src/roll/mod.rs

Repository: txpipe/dolos

Length of output: 5264


Increment the dormant-epoch counter on epoch/transaction boundaries with no new proposals.

Every writer of GovState::num_dormant_epochs only resets it, so epochs without live proposals keep the boundary value at 0. That makes this expiry adjustment and the DRepDormancyRelease fan-out only work if the previous epoch ended with a non-zero counter; the missing increment should preserve/drop the per-epoch activity state and account for multi-tx zero-proposal epochs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cardano/src/ewrap/loading.rs` around lines 275 - 283, Update the
GovState::num_dormant_epochs transition logic at epoch/transaction boundaries so
it increments when no live proposals are present, rather than only resetting.
Preserve reset behavior when new proposals occur, and ensure the updated counter
is used by the expiry adjustment and DRepDormancyRelease fan-out across
consecutive zero-proposal epochs and transactions.

Comment thread crates/cardano/src/roll/dreps.rs
scarmuega and others added 2 commits July 25, 2026 13:51
Review follow-ups on the phase-3 governance PR:

- The dormancy-release fan-out targeted only the DRep key set
  snapshotted at batch start, missing DReps registered in an earlier
  block or tx of the same batch. Haskell's updateDormantDRepExpiry folds
  over the live DRep map, which includes them. The visitor now records
  registrations seen while the counter is non-zero and the release
  targets the deduplicated union (a re-registration would otherwise be
  boosted twice); the whole DormancyContext is threaded across the
  batch's builders instead of just the counter.
- Collect fan-out keys via the raw entity iterator, skipping the
  per-row CBOR decode.
- Document the DRepRegistration-precedes-DRepExpiryUpdate ordering
  invariant at the defensive creation site in DRepExpiryUpdate::apply.
- Cover the review's test gaps: entering_conway boundary math,
  bootstrap_gov gating on force_protocol (via ToyDomain), the
  entity-absent targeted gov commit path in estart, and the
  release-target union/dedup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gov singleton landed with ad-hoc plumbing around machinery shaped
for entity populations: a bespoke apply_gov_state_deltas in the estart
commit (streams can't create absent rows), inline (ns, key) derivation
at every read site, and duplicated genesis seeding between bootstrap_gov
and GovGenesisInit. EpochState had grown its own parallel workarounds:
an EpochState-specific apply variant in the ewrap commit and
full-namespace streams to touch one known row.

Name the pattern instead: a SingletonEntity trait (fixed key over
FixedNamespace) implemented by GovState and EpochState, a generic
creation-capable WorkDeltas::apply_singleton used by all boundary
commit paths, and a read_singleton helper for direct reads. The two
gov birth paths now share GovState::seed_genesis so they can't drift.

No behavior change; the estart gov tests now exercise the generic path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

Caution

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

⚠️ Outside diff range comments (1)
crates/cardano/src/roll/dreps.rs (1)

151-170: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject phase-2-invalid transactions before DRep processing.

visit_tx skips dormancy release for invalid transactions but still emits DRepActivity for their votes. visit_cert similarly gates committee effects only, then can emit DRep registration, anchor, unregistration, and activity effects. These effects must not alter persisted governance state.

  • crates/cardano/src/roll/dreps.rs#L151-L170: return early for !tx.is_valid() before processing voting procedures.
  • crates/cardano/src/roll/dreps.rs#L211-L225: return early for !tx.is_valid() before processing DRep certificates.
Proposed fix
 let MultiEraTx::Conway(conway_tx) = tx else {
     return Ok(());
 };
+if !tx.is_valid() {
+    return Ok(());
+}

-if tx.is_valid() && self.dormancy.dormant_epochs > 0 && !tx.gov_proposals().is_empty() {
+if self.dormancy.dormant_epochs > 0 && !tx.gov_proposals().is_empty() {
-// Committee certificates target the governance singleton.
-if tx.is_valid() {
-    // ...
+if !tx.is_valid() {
+    return Ok(());
 }
+// Committee and DRep certificate effects are valid-only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cardano/src/roll/dreps.rs` around lines 151 - 170, Reject
phase-2-invalid transactions at the start of visit_tx’s voting-procedure
processing, before dormancy release and any DRep activity effects; update
crates/cardano/src/roll/dreps.rs:151-170 accordingly. Add the same
!tx.is_valid() early return at the start of visit_cert’s DRep-certificate
processing in crates/cardano/src/roll/dreps.rs:211-225, before registration,
anchor, unregistration, or activity effects are emitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/cardano/src/roll/dreps.rs`:
- Around line 151-170: Reject phase-2-invalid transactions at the start of
visit_tx’s voting-procedure processing, before dormancy release and any DRep
activity effects; update crates/cardano/src/roll/dreps.rs:151-170 accordingly.
Add the same !tx.is_valid() early return at the start of visit_cert’s
DRep-certificate processing in crates/cardano/src/roll/dreps.rs:211-225, before
registration, anchor, unregistration, or activity effects are emitted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d20beec-655f-4693-9a2e-77c720ecafcb

📥 Commits

Reviewing files that changed from the base of the PR and between 481f042 and 24d0dee.

📒 Files selected for processing (14)
  • crates/cardano/src/estart/commit.rs
  • crates/cardano/src/ewrap/commit.rs
  • crates/cardano/src/ewrap/loading.rs
  • crates/cardano/src/genesis/mod.rs
  • crates/cardano/src/lib.rs
  • crates/cardano/src/model/dreps.rs
  • crates/cardano/src/model/epochs.rs
  • crates/cardano/src/model/eras.rs
  • crates/cardano/src/model/gov.rs
  • crates/cardano/src/model/mod.rs
  • crates/cardano/src/roll/batch.rs
  • crates/cardano/src/roll/dreps.rs
  • crates/cardano/src/roll/mod.rs
  • crates/cardano/src/rupd/work_unit.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/cardano/src/model/eras.rs
  • crates/cardano/src/ewrap/loading.rs
  • crates/cardano/src/model/gov.rs
  • crates/cardano/src/model/dreps.rs

The gov singleton had conditional, era-dependent existence: born at
genesis only for forced-Conway devnets, at the Chang boundary otherwise,
and never on in-place-upgraded stores. Every delta and read paid for
that with absence tolerance — was_new undo flags, get_or_insert apply
paths, unwrap_or_default reads — a tax each future gov delta would
inherit.

Flip the model to an invariant plus a migration:

- The row now exists on every store regardless of era. Bootstrap always
  creates it; a new active_since field (None = pre-Conway) flags whether
  governance is enabled, set to 0 for forced-Conway networks and to the
  boundary epoch by GovGenesisInit at Chang.
- A one-time startup migration (CARDANO-006) creates the row on stores
  bootstrapped before the invariant, deriving active_since from the era
  summary via the new ChainSummary::first_conway_epoch. Enact-state
  fields stay unset — content missed since the fork only comes from a
  fresh sync. Runs in CardanoLogic::initialize, before WAL catch-up
  replays deltas that now expect the row.
- Deltas assert existence: GovGenesisInit activates (and undoes to) the
  inactive row instead of creating/removing it; CommitteeAuth,
  CommitteeResign and GovDormancyReset drop their was_new machinery.
- Reads assert too: load_gov replaces the tolerant unwrap_or_default
  reads, erroring with CARDANO-007 (MissingGovState) on a broken store.

Note for dev environments: stores synced with earlier commits of this
branch must be wiped — the WAL shapes of the gov deltas changed and
existing gov rows carry no active_since. No released store is affected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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