IntersectMBO / IntersectMBO/cardano-api
Replace cardano-api's own era tag types with cardano-ledger's era types
- Dominant language
- Haskell
- Stars
- 40
- Forks
- 30
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 30
Description
## 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 #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 #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 #1038 (migrating JSON instances to the experimental `Era`) was reverted by #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 #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
- #1089 / #1093: `UnsignedTx` re-parameterised on ledger's era; the direct precursor, scoped to one type.
- #1199: widened `Exp.SignedTx` to all Shelley-based eras, and documents why `LedgerEra` was kept narrower than `ShelleyLedgerEra`.
- PR #361: earlier attempt to merge the two api-side families, rejected; see risks for why this proposal differs.
- #1038 / #1048: JSON instance migration to the experimental `Era`, merged and reverted (see risks).
- ADR-004 (cardano-node-wiki): introduced the experimental `Era`/`LedgerEra` machinery and explicitly called it scaffolding to be removed later; this proposal is that removal.
- IntersectMBO/cardano-ledger#4223 / #4788: the crypto parameter removal that made ledger's era types identical in shape to ours.
Contributor guide
Research direction
Start by reading Cardano.Api.Era.Internal.Core alongside Cardano.Ledger.Api.Era, then trace the bridging families in Cardano.Api.Era.Internal.Eon.ShelleyBasedEra and Cardano.Api.Experimental.Era. Review the eon constraint bundles and compile the library across the stated GHC matrix; done means the unified era types, simplified constraints, and downstream compatibility all pass CI.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- haskell
- Domain
- api, backend-api-design
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100