DC-EPOCH-36
DC derived enforcedAfter the epoch-boundary decision for a block at slot, the ledger's epoch MUST equal the venue era
schedule's epoch for that slot. A disagreement in EITHER direction is a durable-state contradiction and
fails closed as a typed LedgerError::EpochAgreement, halting the apply with ZERO mutation. This is
strictly STRONGER than the detection it guards: detect_epoch_transition fires only on
schedule > ledger, so it is structurally blind to a ledger AHEAD of the schedule. The check is a
POST-condition (the epoch legitimately changes during the crossing), and an UNLOCATABLE slot -- before
the schedule's first era, which the mainnet corpus contains -- makes the invariant unverifiable, NOT
violated, preserving pre-P5 behaviour exactly. Enforcement is structural rather than remembered:
detect_epoch_transition is pub(crate) with EXACTLY ONE non-test caller,
rules::cross_epoch_boundary_for_slot, which pairs detection with the check.
- Source
docs/clusters/PREPROD-ENTRY-AUTHORITY/SLICE-P5-epoch-agreement-and-venue-constant-containment.md
- Introduced in
- PREPROD-ENTRY-AUTHORITY-P5
Enforcement trace
Code
Tests 4
- epoch_agreement_rejects_a_stale_ledger_epoch
- epoch_agreement_rejects_a_ledger_ahead_of_the_schedule
- epoch_agreement_accepts_agreement
- epoch_agreement_is_silent_on_an_unlocatable_slot
Cross-references
Evidence notes
Written directly from the P4 root cause (e1de7a2e), which measured what having NEITHER check costs: a preview store ran its entire life with ledger_epoch=1375 against schedule_epoch=1378 while a third authority (the epoch-accumulator) advanced correctly to 1378, and nothing compared them. The drift surfaced only three epochs later, as an opaque recovery FingerprintMismatch, when a binary that computed the epoch correctly replayed the store and applied the three skipped boundaries on the first replayed block. This invariant would have caught the originating P3 defect on the FIRST block on BOTH venues -- preview (473 vs 1375, frozen forever) and preprod (498 vs 304, phantom boundary) -- where a mainnet-shaped test corpus structurally could not. The gate was negative-tested three ways: making detect_epoch_transition bare pub, adding a second unpaired caller, and removing the check from the crossing point were each mutated and caught.