Skip to content

Commit

Permalink
Update ADR-4: Replace CurrentEra & UpcomingEra with concrete era names.
Browse files Browse the repository at this point in the history
  • Loading branch information
carbolymer committed Jul 29, 2024
1 parent 95e186d commit 9f6be99
Showing 1 changed file with 59 additions and 256 deletions.
315 changes: 59 additions & 256 deletions docs/ADR-4-Support-only-for-mainnet-and-upcoming-eras.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

## Background

In the cardano blockchain, available functionality is determined by the era (more precisely the protocol version). To capture this we defined the following types<sup>1, 2</sup>:
In the cardano blockchain, available functionality is determined by the era (more precisely the protocol version).
To capture this we defined the following types<sup>1, 2</sup>:
```haskell
data ShelleyBasedEra era where
ShelleyBasedEraShelley :: ShelleyBasedEra ShelleyEra
Expand All @@ -26,11 +27,16 @@ data CardanoEra era where
ConwayEra :: CardanoEra ConwayEra

```
Early on the idea was to parameterize functions on the era so they can have the era specific behaviour but also to capture this idea of backwards compatibility. In the Shelley era days, there was an (incorrect) assumption that later eras would be backwards compatible with previous eras, especially where transactions were concerned. This would have allowed users to in theory never update their `cardano-cli` version however this is not the case as hardforks introduce backwards incompatible changes (see Conway breaking changes as an example).
Early on the idea was to parameterize functions on the era so they can have the era specific behaviour but also to capture this idea of backwards compatibility.
In the Shelley era days, there was an (incorrect) assumption that later eras would be backwards compatible with previous eras, especially where transactions were concerned.
This would have allowed users to in theory never update their `cardano-cli` version however this is not the case as hardforks introduce backwards incompatible changes (see Conway breaking changes as an example).

The other justification for enumerating the eras like this was to allow QA to perform testing across all eras. However, the only test QA performs across the eras are hardforks. Therefore we don't need to expose all the era specific functionality for each era, rather we need to maintain the ability to submit hardfork update proposals across the eras i.e Byron -> latest era.
The other justification for enumerating the eras like this was to allow QA to perform testing across all eras.
However, the only test QA performs across the eras are hardforks.
Therefore we don't need to expose all the era specific functionality for each era, rather we need to maintain the ability to submit hardfork update proposals across the eras i.e Byron -> latest era.

Users consuming `cardano-api` are only interested in functionality relevant to mainnet and the upcoming era if they want to experiment with new features on a testnet. Therefore rather than maintaining a potentially never ending enumeration of eras, I propose to maintain only the era on mainnet and the upcoming era that will be hardforked to in the future.
Users consuming `cardano-api` are only interested in functionality relevant to mainnet and the upcoming era if they want to experiment with new features on a testnet.
Therefore rather than maintaining a potentially never ending enumeration of eras, I propose to maintain only the era on mainnet and the upcoming era that will be hardforked to in the future.

## Code

Expand All @@ -53,24 +59,26 @@ type family AvailableErasToSbe era = (r :: Type) | r -> era where


data Era era where
-- | Deprecated era, Do not use.
BabbageEra :: Era BabbageEra
-- | The era currently active on Cardano's mainnet.
CurrentEra :: Era BabbageEra
-- | The era planned for the next hardfork on Cardano's mainnet.
UpcomingEra :: Era ConwayEra

-- | After a hardfork there is usually no planned upcoming era
-- that we are able to experiment with. We force a type era
-- in this instance. See docs above.
data EraCurrentlyNonExistent

type family UninhabitableType a where
UninhabitableType EraCurrentlyNonExistent =
TypeError ('Text "There is currently no planned upcoming era. Use CurrentEra constructor instead.")
ConwayEra :: Era ConwayEra


-- | Type class interface for the 'Era' type.
class UseEra era where
useEra :: Era era

instance UseEra BabbageEra where
useEra = BabbageEra

instance UseEra ConwayEra where
useEra = ConwayEra
```

### Deprecation as seen by users
### Deprecation of an era

The following is a two stage example of how a user would consume the api and how a user would see a deprecation of an era
The following is an example of how a user would consume the api and how a user would see a deprecation of an era

```haskell
-- TODO: These constraints can be hidden
Expand All @@ -86,272 +94,67 @@ makeUnsignedTx era bc = ...


testBabbageUnsignedTx :: Either UnsignedTxError (UnsignedTx BabbageEra)
testBabbageUnsignedTx = makeUnsignedTx CurrentEra $ defaultTxBodyContent ShelleyBasedEraBabbage
testBabbageUnsignedTx = makeUnsignedTx BabbageEra $ defaultTxBodyContent ShelleyBasedEraBabbage

testConwayUnsignedTx :: Either UnsignedTxError (UnsignedTx ConwayEra)
testConwayUnsignedTx = makeUnsignedTx UpcomingEra $ defaultTxBodyContent ShelleyBasedEraConway
testConwayUnsignedTx = makeUnsignedTx ConwayEra $ defaultTxBodyContent ShelleyBasedEraConway
```

In the current state of affairs both `testBabbageUnsignedTx` and `testConwayUnsignedTx` type check without issue. However let's update the types to show how a deprecation (or a successful hardfork to the upcoming era) will show up in a user's code.
In the current state of affairs both `testBabbageUnsignedTx` and `testConwayUnsignedTx` type check without issue.
However let's update the types to show how a deprecation (or a successful hardfork to the upcoming era) will show up in a user's code.

Changes:

```haskell
-- this deprecation pragma deprecates both the type `BabbageEra` and the `BabbageEra` constructor
{-# DEPRECATED BabbageEra "We are currently in the Conway era. Please update your type Signature to ConwayEra" #-}
data BabbageEra
...
...
data Era era where
-- | Previous era, do not use.
BabbageEra :: Era BabbageEra
-- | The era currently active on Cardano's mainnet.
CurrentEra :: Era ConwayEra
-- | The era planned for the next hardfork on Cardano's mainnet.
UpcomingEra :: Era (UninhabitableType EraCurrentlyNonExistent)
```
Type errors:

```haskell
In the use of type constructor or class BabbageEra
(imported from Cardano.Api.Experimental.Eras, but defined in Cardano.Api.Protocol.AvailableEras):
Deprecated: "We are currently in the Conway era. Please update your type Signature to ConwayEra"

and

internal/Cardano/Api/Experimental/Tx.hs:34:39: error: [GHC-64725]
There is currently no planned upcoming era. Use CurrentEra instead.
In the first argument of makeUnsignedTx, namely UpcomingEra
In the first argument of ($), namely makeUnsignedTx UpcomingEra
In the expression:
makeUnsignedTx UpcomingEra
$ defaultTxBodyContent ShelleyBasedEraConway
|
34 | testConwayUnsignedTx = makeUnsignedTx UpcomingEra $ defaultTxBodyContent ShelleyBasedEraConway
```

This deprecation will happen only after the hardfork has been successful. There should be no need for consumers of the api to need the prior era's specific functionality. If users for some reason want this, we can direct them to use the ledger's api directly.

## Downsides

Because we are using GADTs our library functions will have unreachable code paths that will compile, however trying to use the unreachable values will result in a type error. E.g

### Deprecation as seen by cardano-api maintainers

Before deprecation:

```haskell
ConwayEra :: Era ConwayEra

testBabbageEra :: Either UnsignedTxError (Ledger.TxBody (ToConstrainedEra BabbageEra))
testBabbageEra = eraSpecificLedgerTxBody CurrentEra undefined $ defaultTxBodyContent ShelleyBasedEraBabbage
class UseEra era where
useEra :: Era era

testConwayEra :: Either UnsignedTxError (Ledger.TxBody (ToConstrainedEra ConwayEra))
testConwayEra = eraSpecificLedgerTxBody UpcomingEra undefined $ defaultTxBodyContent ShelleyBasedEraConway
instance TypeError ('Text "UseEra BabbageEra: Deprecated. Update to ConwayEra") => UseEra BabbageEra where
useEra = error "unreachable"


eraSpecificLedgerTxBody
:: Era era
-> Ledger.TxBody (ToConstrainedEra era)
-> TxBodyContent BuildTx (AvailableErasToSbe era)
-> Either UnsignedTxError (Ledger.TxBody (ToConstrainedEra era))
eraSpecificLedgerTxBody CurrentEra ledgerbody bc = do
sbe <- maybe (Left $ error "eraSpecificLedgerTxBody: TODO") Right $ protocolVersionToSbe CurrentEra

setUpdateProposal <- first UnsignedTxError $ convTxUpdateProposal sbe (txUpdateProposal bc)

return $ ledgerbody & L.updateTxBodyL .~ setUpdateProposal

eraSpecificLedgerTxBody UpcomingEra ledgerbody bc =
let propProcedures = txProposalProcedures bc
voteProcedures = txVotingProcedures bc
treasuryDonation = txTreasuryDonation bc
currentTresuryValue = txCurrentTreasuryValue bc
in return $
ledgerbody
& L.proposalProceduresTxBodyL .~ convProposalProcedures (maybe TxProposalProceduresNone unFeatured propProcedures)
& L.votingProceduresTxBodyL .~ convVotingProcedures (maybe TxVotingProceduresNone unFeatured voteProcedures)
& L.treasuryDonationTxBodyL .~ maybe (L.Coin 0) unFeatured treasuryDonation
& L.currentTreasuryValueTxBodyL .~ L.maybeToStrictMaybe (unFeatured <$> currentTresuryValue)
```
After deprecation:

```haskell
internal/Cardano/Api/Experimental/Tx.hs:123:26: error: [GHC-39999]
Could not deduce L.ShelleyEraTxBody
Ouroboros.Consensus.Shelley.Eras.StandardConway
arising from a use of L.updateTxBodyL
from the context: era ~ ConwayEra
bound by a pattern with constructor: CurrentEra :: Era ConwayEra,
in an equation for eraSpecificLedgerTxBody
at internal/Cardano/Api/Experimental/Tx.hs:118:25-34
In the first argument of (.~), namely L.updateTxBodyL
In the second argument of (&), namely
L.updateTxBodyL .~ setUpdateProposal
In the second argument of ($), namely
ledgerbody & L.updateTxBodyL .~ setUpdateProposal
|
123 | return $ ledgerbody & L.updateTxBodyL .~ setUpdateProposal
| ^^^^^^^^^^^^^^^

internal/Cardano/Api/Experimental/Tx.hs:132:13: error: [GHC-64725]
Cannot satisfy: Cardano.Ledger.Binary.Version.MinVersion <= Ledger.ProtVerHigh
(ToConstrainedEra era)
In the first argument of (.~), namely
L.proposalProceduresTxBodyL
In the second argument of (&), namely
L.proposalProceduresTxBodyL
.~
convProposalProcedures
(maybe TxProposalProceduresNone unFeatured propProcedures)
In the first argument of (&), namely
ledgerbody
& L.proposalProceduresTxBodyL
.~
convProposalProcedures
(maybe TxProposalProceduresNone unFeatured propProcedures)
|
132 | & L.proposalProceduresTxBodyL .~ convProposalProcedures (maybe TxProposalProceduresNone unFeatured propProcedures)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^

internal/Cardano/Api/Experimental/Tx.hs:132:13: error: [GHC-64725]
Cannot satisfy: Cardano.Ledger.Binary.Version.MinVersion <= Ledger.ProtVerLow
(ToConstrainedEra era)
In the first argument of (.~), namely
L.proposalProceduresTxBodyL
In the second argument of (&), namely
L.proposalProceduresTxBodyL
.~
convProposalProcedures
(maybe TxProposalProceduresNone unFeatured propProcedures)
In the first argument of (&), namely
ledgerbody
& L.proposalProceduresTxBodyL
.~
convProposalProcedures
(maybe TxProposalProceduresNone unFeatured propProcedures)
|
132 | & L.proposalProceduresTxBodyL .~ convProposalProcedures (maybe TxProposalProceduresNone unFeatured propProcedures)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^

internal/Cardano/Api/Experimental/Tx.hs:132:13: error: [GHC-64725]
Cannot satisfy: Ledger.ProtVerLow
(ToConstrainedEra era) <= Ledger.ProtVerHigh (ToConstrainedEra era)
In the first argument of (.~), namely
L.proposalProceduresTxBodyL
In the second argument of (&), namely
L.proposalProceduresTxBodyL
.~
convProposalProcedures
(maybe TxProposalProceduresNone unFeatured propProcedures)
In the first argument of (&), namely
ledgerbody
& L.proposalProceduresTxBodyL
.~
convProposalProcedures
(maybe TxProposalProceduresNone unFeatured propProcedures)
|
132 | & L.proposalProceduresTxBodyL .~ convProposalProcedures (maybe TxProposalProceduresNone unFeatured propProcedures)
instance UseEra ConwayEra where
useEra = ConwayEra
```

We get somewhat obscure type errors but this should not be an issue as this is a direct result of deprecating an era in cardano-api. We know we have to update our functions because of this, so it comes as no surprise

However downstream users get better type eras:

```haskell
internal/Cardano/Api/Experimental/Tx.hs:125:41: error: [GHC-64725]
There is currently no planned upcoming era. Use CurrentEra instead.
In the first argument of eraSpecificLedgerTxBody, namely
UpcomingEra
In the first argument of ($), namely
eraSpecificLedgerTxBody UpcomingEra undefined
In the expression:
eraSpecificLedgerTxBody UpcomingEra undefined
$ defaultTxBodyContent ShelleyBasedEraConway
|
125 | testConwayEra = eraSpecificLedgerTxBody UpcomingEra undefined $ defaultTxBodyContent ShelleyBasedEraConway
|
Type error:
```
and for `testBabbageEra`:
```
internal/Cardano/Api/Experimental/Tx.hs:122:18: error: [GHC-83865]
• Couldn't match type: L.ConwayTxBody
(L.ConwayEra L.StandardCrypto)
with: Cardano.Ledger.Babbage.TxBody.BabbageTxBody
(L.BabbageEra L.StandardCrypto)
Expected: Either
UnsignedTxError (Ledger.TxBody (ToConstrainedEra BabbageEra))
Actual: Either
UnsignedTxError (Ledger.TxBody (ToConstrainedEra ConwayEra))
• In the expression:
eraSpecificLedgerTxBody CurrentEra undefined
$ defaultTxBodyContent ShelleyBasedEraBabbage
In an equation for ‘testBabbageEra’:
testBabbageEra
= eraSpecificLedgerTxBody CurrentEra undefined
$ defaultTxBodyContent ShelleyBasedEraBabbage
|
122 | testBabbageEra = eraSpecificLedgerTxBody CurrentEra undefined $ defaultTxBodyContent ShelleyBasedEraBabbage
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
internal/Cardano/Api/Experimental/Tx.hs:122:65: error: [GHC-83865]
• Couldn't match type ‘Cardano.Api.Eras.Core.BabbageEra’
with ‘Cardano.Api.Eras.Core.ConwayEra’
Expected: TxBodyContent BuildTx (AvailableErasToSbe ConwayEra)
Actual: TxBodyContent BuildTx Cardano.Api.Eras.Core.BabbageEra
• In the second argument of ‘($)’, namely
‘defaultTxBodyContent ShelleyBasedEraBabbage’
In the expression:
eraSpecificLedgerTxBody CurrentEra undefined
$ defaultTxBodyContent ShelleyBasedEraBabbage
In an equation for ‘testBabbageEra’:
testBabbageEra
= eraSpecificLedgerTxBody CurrentEra undefined
$ defaultTxBodyContent ShelleyBasedEraBabbage
|
122 | testBabbageEra = eraSpecificLedgerTxBody CurrentEra undefined $ defaultTxBodyContent ShelleyBasedEraBabbage
In the use of type constructor or class ‘BabbageEra’
(imported from Cardano.Api.Experimental.Eras, but defined in Cardano.Api.Protocol.AvailableEras):
Deprecated: "We are currently in the Conway era. Please update your type Signature to ConwayEra"
```
### GADT flexibility

The flexibility of GADTs also bites us as just by looking at the code it appears the deprecated code path is reachable, but it is not:

```haskell
eraSpecificLedgerTxBody
:: Era era
-> Ledger.TxBody (ToConstrainedEra era)
-> TxBodyContent BuildTx (AvailableErasToSbe era)
-> Either UnsignedTxError (Ledger.TxBody (ToConstrainedEra era))
eraSpecificLedgerTxBody CurrentEra ledgerbody bc =
let propProcedures = txProposalProcedures bc
voteProcedures = txVotingProcedures bc
treasuryDonation = txTreasuryDonation bc
currentTresuryValue = txCurrentTreasuryValue bc
in return $
ledgerbody
& L.proposalProceduresTxBodyL .~ convProposalProcedures (maybe TxProposalProceduresNone unFeatured propProcedures)
& L.votingProceduresTxBodyL .~ convVotingProcedures (maybe TxVotingProceduresNone unFeatured voteProcedures)
& L.treasuryDonationTxBodyL .~ maybe (L.Coin 0) unFeatured treasuryDonation
& L.currentTreasuryValueTxBodyL .~ L.maybeToStrictMaybe (unFeatured <$> currentTresuryValue)

eraSpecificLedgerTxBody UpcomingEra ledgerbody _bc = return ledgerbody
-- sbe <- maybe (Left $ error "eraSpecificLedgerTxBody: TODO") Right $ protocolVersionToSbe CurrentEra
--
-- setUpdateProposal <- first UnsignedTxError $ convTxUpdateProposal sbe (txUpdateProposal bc)
--
-- return $ ledgerbody & L.updateTxBodyL .~ setUpdateProposal
```
This deprecation will happen only after the hardfork has been successful.
There should be no need for consumers of the api to need the prior era's specific functionality.
If users for some reason want this, we can direct them to use the ledger's api directly.

Type error when called:
The next stage after deprecation period, should be **removal of the deprecated constructor**.
The deprecation period should be long enough to give enough time for `cardano-api` consumers to update their codebase to the post-hardfork era.

```haskell
ghci> eraSpecificLedgerTxBody UpcomingEra undefined undefined
## Downsides

<interactive>:9:1: error: [GHC-64725]
There is currently no planned upcoming era. Use CurrentEra instead.
In a stmt of an interactive GHCi command: print it
```
`cardano-api` users will be limited up to only two eras exposed from `cardano-api`.
This will force them to keep up-to-date their code after the hardfork.
This may turn out to be a disruptive process, but necessary to make the code using `cardano-api` healthy.

# Approach

The new api should be created adjacant to the existing one. We then slowly replace the use of the existing api in cardano-api, eventually deprecating the "old" api.
The new api should be created adjacant to the existing one.
We then slowly replace the use of the existing api in cardano-api, eventually deprecating the "old" api.

With respect to cardano-cli, we should introduce top level `current-era` and `upcoming-era` commands.
We would have to release a cli version specific for post-hardfork usage, as `current-era` and `upcoming-era` commands will have different semantics to the pre-hardfork commands.
This will require a little coordination but shouldn't be too much additional overhead.

With respect to cardano-cli, we should introduce top level `current-era` and `upcoming-era` commands. We would have to release a cli version specific for post-hardfork usage, as `current-era` and `upcoming-era` commands will have different semantics to the pre-hardfork commands. This will require a little coordination but shouldn't be too much additional overhead.
# Decision

TBD
Expand Down

0 comments on commit 9f6be99

Please sign in to comment.