Skip to content

Replace cardano-api's own era tag types with cardano-ledger's era types #1269

Description

@carbolymer

Summary

cardano-api defines its own uninhabited era tag types (data ByronEra, data ShelleyEra, ..., data DijkstraEra) in Cardano.Api.Era.Internal.Core, and bridges them to cardano-ledger's era types through type families.
cardano-ledger already defines the same eight tags (Byron included) as bare data declarations of kind Type, exported from Cardano.Ledger.Api.Era.
This issue proposes deleting cardano-api's copies and re-exporting ledger's types instead, so that the api era and the ledger era are the same type and every bridging family collapses to identity.
The switch happens inside Cardano.Api.Era.Internal.Core: the module keeps exporting the same names through the same export chain (Cardano.Api.Era, Cardano.Api, Cardano.Api.Experimental), so the transition is seamless for consumers - no import changes, no renamed types, only the type identity underneath changes.

We currently maintain four hand-written copies of the same bijection:

  • ShelleyLedgerEra (closed, injective, 7 equations) in Cardano.Api.Era.Internal.Eon.ShelleyBasedEra
  • CardanoLedgerEra (closed, injective, Byron to Conway, missing Dijkstra, zero internal consumers, still exported from Cardano.Api) in Cardano.Api.Era.Internal.Core
  • LedgerEra (Conway and Dijkstra only) in Cardano.Api.Experimental.Era
  • ToApiEra (the reverse direction, all 8 eras, private) in Cardano.Api.Experimental.Tx.Internal.Type

None of these needs to exist after the switch; the publicly exported ones are kept as deprecated identity synonyms for a deprecation period, then removed.

Why now

The experimental API is already halfway there.
UnsignedTx, Exp.TxOut, TxBodyContent, AnyWitness, Certificate and the other experimental leaf types are parameterised by the ledger era directly (#1089, implemented in #1093).
LedgerEra era exists only as glue between tag-indexed signatures (Era era, IsEra era) and those ledger-indexed data types, and users have to write Exp.TxOut (LedgerEra era) instead of Exp.TxOut era because GHC cannot see the two eras are equal without obtainCommonConstraints in scope.
EraCommonConstraints even carries ShelleyLedgerEra era ~ LedgerEra era (in both directions) to keep the old and new families in agreement.
Unifying the tags removes this entire class of friction rather than documenting around it.

On the ledger side, the crypto parameter removal (IntersectMBO/cardano-ledger#4223, implemented in #4788) left the era types as plain phantom tags: data ShelleyEra with no parameters, kind Type, exactly the shape of our own tags.
Ledger also defines a deliberately minimal ByronEra (an Era instance and nothing else, kept alive so that PreviousEra ShelleyEra resolves), so even Byron has a direct counterpart.
Consensus already sits on ledger's types: Ouroboros.Consensus.Shelley.Eras re-exports them, so Consensus.ShelleyEra in our code is already ledger's ShelleyEra.
cardano-api is the only layer left with its own era universe.

Alternatives considered

  • Keep the status quo and document the friction.
    The four bridging families stay, every new era adds an equation to each of them, and the Exp.TxOut (LedgerEra era) workaround stays with them.
    This is the baseline cost the proposal removes, and it grows with each era.
  • Unify only the experimental API.
    The experimental leaf types are already ledger-parameterised, so this is nearly done, but it leaves ShelleyLedgerEra and its ~613 use sites untouched in the old API and keeps the SignedTx-style seams at the old/new boundary.
  • Bridge with something weaker than type identity.
    The tags are phantom types with no values, so there is nothing to coerce or wrap; a type family is the only possible bridge between distinct tags, and that is exactly what we maintain today.
  • Push in the other direction, i.e. ledger adopts cardano-api's tags.
    Wrong dependency direction: consensus and ledger already agree on ledger's types, cardano-api is the odd one out.

Proposed change

  1. In Cardano.Api.Era.Internal.Core, delete the eight data XEra declarations and re-export the ledger types instead (imported from Cardano.Ledger.Api.Era, the sanctioned export point; the defining module in cardano-ledger-core is warning-tagged internal).
    This is the seamlessness requirement: the tags stay reachable under the exact same names from the exact same modules, so no consumer changes an import line.
  2. Keep every existing instance where it is.
    All instances on the tags (IsCardanoEra, HasTypeProxy with its AsType data instances, IsShelleyBasedEra, the era-onwards classes, HasScriptLanguageInEra, IsEra, ...) are instances of cardano-api-owned classes, so they stay non-orphan when the type moves to ledger.
  3. Replace all three public families with deprecated identity synonyms: type ShelleyLedgerEra era = era, and likewise CardanoLedgerEra and the experimental LedgerEra.
    Existing signatures keep compiling (now with a deprecation warning), and applications to concrete eras (ShelleyLedgerEra ConwayEra) reduce to the era itself.
    As synonyms the families also become total: CardanoLedgerEra gains the Dijkstra case the current family is missing, and LedgerEra applies beyond Conway and Dijkstra.
  4. Delete the private ToApiEra family.
    It is not exported (its module exports only UnsignedTx (..)), so no deprecation period is needed.
  5. Simplify the constraint bundles.
    Every eon bundle (ShelleyBasedEraConstraints and the ten others) constrains L.EraX (ShelleyLedgerEra era); these become L.EraX era.
    The ShelleyLedgerEra era ~ LedgerEra era pair in EraCommonConstraints and the ShelleyBlock (ConsensusProtocol era) (ShelleyLedgerEra era) ~ ConsensusBlockForEra era equalities become trivial and can go.
  6. Align SignedTx with UnsignedTx.
    SignedTx is currently the one experimental type still keyed on the api tag via ShelleyLedgerEra (a deliberate choice in Widen Exp.SignedTx to all Shelley-based eras #1199, because LedgerEra only covers two eras and SignedTx needs all Shelley-based ones).
    With unified tags both conventions denote the same type, so the special case disappears.

How broad is the change

Numbers from a survey of cardano-api and its main consumers (cardano-cli, all cardano-api-consuming packages of the cardano-node repo including the bench family, cardano-testnet and a few smaller tools) against cardano-api commit bf5c2a916 (2026-07-27), with cardano-ledger-core 1.20.0.0 and cardano-ledger-api 1.13.0.0 pinned.

Inside the cardano-api repo:

Measure Count
ShelleyLedgerEra occurrences ~613 in ~54 files (cardano-api, tests, cardano-rpc)
... of which applied to a type variable 90 to 95 % (compile unchanged with the identity synonym)
LedgerEra (experimental) occurrences 341 in 21 modules
obtainCommonConstraints call sites 171 in 22 modules (including cardano-wasm and cardano-rpc)
Instance declarations on concrete era tags 80, all in the main library, all stay well-typed
Files that mix unqualified api tags with qualified ledger tags 8 (the real edit sites)
Concrete @XEra type applications effectively zero

The edits concentrate in Cardano.Api.Era.Internal.Core (the definition site), Cardano.Api.Experimental.Era, Cardano.Api.Era.Internal.Eon.ShelleyBasedEra, and the 11 eon modules' constraint bundles.
The rest of the library compiles unchanged or loses redundant qualifiers.
No DataKinds promotion of the tags exists anywhere, no instance head mentions ShelleyLedgerEra era, and the three TestEquality instances match on term-level constructors, so none of the usual type-swap traps apply.

Downstream (surveyed consumers):

Repo ShelleyLedgerEra sites Notes
cardano-cli 34 in 11 files one identity shim (fromShelleyLedgerPParamsShim) becomes deletable; the rest compiles via the synonym
cardano-node 7 in 2 files tracing instances with ShelleyLedgerEra era ~ ledgerera constraints, which become trivially satisfied
cardano-testnet 12 in 6 files plain projections, become no-ops
tx-generator (cardano-node bench) 4 in 2 files generic era projections, compile unchanged via the synonym
locli, plutus-scripts-bench, adawallet, gov tools, faucet, submit-api, chairman 0 era handling goes through eon witnesses, unaffected

Zero downstream instances are declared on a concrete api era tag, so no instance overlap arises in any surveyed repo.

Effort estimate: one core PR for steps 1 to 5 (roughly 10 files of real edits plus mechanical constraint-bundle changes, 2 to 4 days including CI matrix fallout on GHC 9.6/9.10/9.12), one small follow-up PR for SignedTx and experimental cleanup, and trivial downstream PRs (hours each).
A major version bump of cardano-api either way.

What actually breaks

The baseline: a consumer that reaches the tags and ShelleyLedgerEra through Cardano.Api's public surface does not break at the source level.
The names and modules are unchanged, the identity synonym keeps every ShelleyLedgerEra era signature compiling, and all term-level code (era witnesses, eon dispatch, TestEquality) is untouched, so for the ~90-95 % generic-era majority the change arrives as a recompile against a new major version.

What does break:

  • Nothing breaks immediately from the family collapse itself, because ShelleyLedgerEra, CardanoLedgerEra and the experimental LedgerEra all stay on as deprecated identity synonyms for the deprecation period.
    Even signatures written in today's recommended style, such as f :: Era era -> Exp.TxOut (LedgerEra era) -> ..., keep compiling, now with a deprecation warning.
    Consumers building with -Werror and without -Wno-deprecations turn those warnings into errors; that is the usual cost of that setting, and cardano-api itself avoids it because the core PR rewrites all internal uses (it must, since the repo builds with -Werror).
    The real source break is deferred to the synonyms' removal at the end of the deprecation period: across the surveyed repos that would be ~613 ShelleyLedgerEra and 341 LedgerEra occurrences, nearly all inside cardano-api and already rewritten by the core PR, leaving only the small downstream counts in the table above.
  • A package that defines instances of the same class for both cardano-api's XEra and ledger's XEra gets a duplicate-instance error, since the two heads become identical.
    Zero such cases exist in the surveyed repos; possible in principle on Hackage/CHaP, so the changelog must call it out.
  • Code that deliberately told the two type universes apart becomes redundant rather than wrong.
    The only such code found is bridging glue: cardano-cli's fromShelleyLedgerPParamsShim and cardano-api's own ToApiEra, both deletable.
  • Typeable fingerprints and fully qualified TypeRep output for the eight tags change, since the types now originate in ledger's defining module.
    Nothing in the surveyed code persists or compares these across versions, but it is observable behaviour.
  • Inside cardano-api the transitional compile errors concentrate in the 8 dual-namespace files and the 11 eon constraint bundles: duplicate imports of the now-identical types, redundant qualifiers, and hlint duplicate-import warnings.
    All of this is contained in the core PR; it is listed here so reviewers know where the diff noise will come from.

Risks

  • Ledger's ByronEra carries a doc comment saying it "cannot have any other class instances, except for the Era type class".
    Our HasTypeProxy/IsCardanoEra instances are on cardano-api-owned classes, so they do not violate coherence or create orphans, but the intent of that comment should be clarified with the ledger team before we rely on the type.
  • The identity synonyms are total where the old families were partial: ShelleyLedgerEra ByronEra used to be a stuck type, now it reduces to ByronEra, and likewise LedgerEra accepts all eras instead of two and CardanoLedgerEra quietly gains Dijkstra.
    Misuse still fails to compile, only later: ledger gives ByronEra no EraTx/EraTxOut/... instances, so any Shelley-based constraint remains unsatisfiable for Byron.
    The term-level guards (ShelleyBasedEra era witnesses) are untouched.
  • The duplicate-instance breakage above cannot be surveyed exhaustively: the surveyed repos are clean, but wallet and plutus tooling on Hackage/CHaP were outside the survey's reach, so the first ecosystem build after release is the real test.
  • Type errors change shape: users will see ledger era types where they previously saw the family application.
    Mostly an improvement (the family reduced to those types in error messages anyway once concrete), but expect some confusion during the transition.
  • Prior art cuts both ways.
    PR Merge ShelleyLedgerEra and CardanoLedgerEra to a single type family LedgerEra #361 (merging ShelleyLedgerEra and CardanoLedgerEra into one family) was rejected for blurring the Byron/Shelley-based distinction; this proposal does not touch that distinction, since CardanoEra, ShelleyBasedEra and all eon witnesses stay exactly as they are, and Byron remains excluded from Shelley-based machinery by instance absence and witnesses.
    PR refactor(api): migrate ToJSON instances to use experimental Era type #1038 (migrating JSON instances to the experimental Era) was reverted by Revert "refactor(api): migrate ToJSON instances to use experimental Era type" #1048 after breaking cardano-cli query utxo in old eras; this proposal narrows nothing, all eight eras keep working with the same witnesses, and the cardano-cli test suite should gate the release.

Downsides: what we lose with the switch

  • We lose the insulation layer.
    Today ledger can rename, move or restructure its era types and cardano-api absorbs it in one family definition; afterwards such changes propagate straight into cardano-api's public API and to every consumer.
    In practice the tags have been stable since the crypto parameter removal, and the coupling to ledger is already tight everywhere else, but the escape hatch is gone.
  • We lose control over the era set.
    cardano-api can currently introduce or withhold an era tag independently of what the pinned ledger exposes.
    After the switch the pinned ledger's tag set is our tag set; a ledger bump that adds an era makes the type nameable downstream immediately, before cardano-api wires any support for it.
    (Witness-driven dispatch means a nameable type with no Era/IsCardanoEra constructor support is inert, so this is mostly cosmetic.)
  • We lose the Byron stuckness guard (covered under risks): misuse fails at instance resolution instead of at the family application.
  • We lose the option of api-only eras, or of splitting one ledger era into several api-visible ones.
    Never used in the project's history, but the option goes away.
  • We pay a one-time ecosystem cost: a hard major bump where the type identity of eight exported types changes, requiring coordinated releases (CHaP, downstream pins) even though most sources compile unchanged.

Implementation plan

  1. Prerequisites (no code): confirm the ByronEra doc-comment question with the ledger team, and agree ordering with the author of the in-flight era-ranges work (see open questions).
  2. Preparation PR: deprecate CardanoLedgerEra (dead code, missing Dijkstra), with a message pointing at ShelleyLedgerEra for now.
    Independently mergeable today, regardless of the rest.
  3. Core PR: steps 1 to 5 above in one atomic change (the tag swap, the identity synonym, constraint bundle simplification).
    Acceptance: the full build and both test suites pass, the whole GHC matrix (9.6/9.10/9.12) passes in CI, and cardano-cli's golden and round-trip tests pass across all eight eras - the coverage that caught the refactor(api): migrate ToJSON instances to use experimental Era type #1038 regression.
  4. Experimental cleanup PR: re-parameterise SignedTx, swap LedgerEra to its deprecated identity synonym, delete the private ToApiEra, drop the now-tautological equalities from EraCommonConstraints, and update the experimental API documentation (the Exp.TxOut (LedgerEra era) gotcha disappears).
  5. Downstream PRs: cardano-cli (delete the shim), cardano-node (optional tidy-up of the ~ ledgerera constraints), cardano-testnet (optional removal of redundant projections).
    These are verified against the branch before release (via source-repository-package pins) and land once the cardano-api release is on CHaP.
  6. Deprecation follow-through: keep ShelleyLedgerEra, CardanoLedgerEra and LedgerEra as deprecated synonyms for at least one major release, then remove them together (exact window to be agreed in review).

Open questions

  • Recommendation needing a yes/no: Cardano.Api.Ledger should start re-exporting the era tags too.
    After unification it would re-export the same entities as Cardano.Api, which GHC handles fine (importing the same entity from two modules is legal), and it would make Cardano.Api.Ledger self-contained.
  • Is Cardano.Ledger.Api.Era a stability-guaranteed export point from the ledger team's perspective, or should we import the tags from the per-era packages?
  • How does this interact with the in-progress era-ranges design (the REMOVEME: era ranges plan commits on master, which sketch a different representation of era spans)?
    Ordering needs agreeing with its author before the core PR (implementation plan step 1).

Prior art

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions