{-# LANGUAGE DeriveAnyClass     #-}
{-# LANGUAGE DeriveGeneric      #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase         #-}
{-# LANGUAGE OverloadedStrings  #-}
{-# LANGUAGE RecordWildCards    #-}
{-# LANGUAGE TypeApplications   #-}
{-# LANGUAGE TypeOperators      #-}

module Ouroboros.Consensus.HardFork.History.EraParams (
    -- * API
    EraParams (..)
  , SafeZone (..)
    -- * Defaults
  , defaultEraParams
  ) where

import           Codec.CBOR.Decoding (Decoder, decodeListLen, decodeWord8)
import           Codec.CBOR.Encoding (Encoding, encodeListLen, encodeWord8)
import           Codec.Serialise (Serialise (..))
import           Control.Monad (void)
import           Data.Word
import           GHC.Generics (Generic)
import           NoThunks.Class (NoThunks)

import           Cardano.Binary (enforceSize)

import           Ouroboros.Consensus.Block
import           Ouroboros.Consensus.BlockchainTime.WallClock.Types
import           Ouroboros.Consensus.Config.SecurityParam

{-------------------------------------------------------------------------------
  OVERVIEW

  The overall chain consists of various /era/s. Each era has its own set of era
  parameters such as the slot length and epoch size, as well as its own block
  format, ledger format, ledger rules, etc. It is assumed that the overall
  /shape/ of the chain is known. In other words, we statically know which eras
  we expect and what their parameters are; adding an additional era would
  require a code update. What we /don't/ know precisely is /when/ we transition
  from one era to the next, i.e., the hard fork transition points.

  When we are at genesis, the chain therefore looks something like this:

  > *-------------------?--------------------?--------------------
  > ^
  > \-- tip

  where we have (in this example) three eras (say, Byron, Shelley and Goguen)
  and therefore two hard fork transitions. Hard forks happen at epoch
  boundaries; the exact 'EpochNo' of each hard fork is determined by the era
  preceding it. Naturally, the exact 'EpochNo' of /past/ hard forks is known:

  > ---------------A--------------*----------?--------------------
  >                               ^
  >                               \-- tip

  where A is a known hard fork transition, and the next hard fork transition
  is still unknown.

  SAFE ZONES

  Future hard fork points may be known or unknown, where "known" means
  "certain"; i.e., for Byron, it would mean an update proposal has been voted
  on, confirmed, endorsed, and that endorsement is at least @k@ blocks deep into
  the chain; for Shelley it means an update proposal is voted on and accepted,
  and that acceptance is at least @k@ blocks deep into the chain.

  When a hard fork point is still unknown, we assume that each era determines a
  "safe zone": a number of slots from the tip of the ledger in which it is
  guaranteed that the hard fork will not happen.

  > CASE (i)
  >
  > ---------------A--------------*----------?--------------------
  >                               \..../
  >                                safe
  >                                zone

  Since the hard fork will not happen in the safe zone, we can extend the use of
  the current set of era parameters past the tip into the safe zone, giving us a
  limited ability to make predictions for the future (such as converting between
  slot numbers and wallclock time).

  We assume that once a transition point is known (and no longer subject to
  roll-back), this is guaranteed not to change anymore and we can use the era's
  parameters up to the transition point:

  > CASE (ii)
  >
  > ---------------A--------------*----------------B--------------
  >                               \.............../
  >                                implicitly safe

  Moreover, we assume that we can extend B's safe zone from the point of the
  hard fork transition:

  > CASE (iii)
  >
  > ---------------A--------------*----------------B--------------
  >                               \.............../\..../
  >                                implicitly safe  safe
  >                                                 zone

  This is justified because the safe zones arise from stability requirements
  for the transactions that establish the transition point. The earliest point
  such a transaction could be included in the chain is after the hard fork
  transition, since it must be a transaction from the /new/ era.

  NOTE ON STABILITY

  If we used as yet /unconfirmed/ update proposals to determine hard fork
  transition points, then any of the resulting time conversions would be
  subject to rollback; if we switched to a different fork, time conversions
  might suddenly look different. Whilst this /may/ be doable, in practice this
  is a headache we would very much like to avoid. For example, it might mean
  that when a block comes in and we determine that it's from the future,
  we might have prematurely marked it as invalid. So, we insist that time
  conversions must be based on update propsals that are /certain/ (no longer
  subject to rollback). This means that the "safe zone" we have been discussing
  above must extend from the point of stability forward. Moreover, the safe zone
  must be long enough to include a sufficient number of blocks such that we can
  evaluate enough headers of an alternative fork (without having its blocks)
  to decide that we want to switch to that fork; since in the worst case that
  means we have to evaluate @k@ headers (or @k+1@), the safe zone must be long
  enough to cover @k@ blocks (and therefore a safe zone of @2k@ slots for Byron,
  and (probably) a safe zone of @3k/f@ slots for Shelley). Effectively, this
  means that consensus wants "stability itself to be stable"; we need a double
  safe zone after an update proposal has been confirmed.
-------------------------------------------------------------------------------}

-- | Parameters that can vary across hard forks
data EraParams = EraParams {
      EraParams -> EpochSize
eraEpochSize  :: !EpochSize
    , EraParams -> SlotLength
eraSlotLength :: !SlotLength
    , EraParams -> SafeZone
eraSafeZone   :: !SafeZone
    }
  deriving stock    (Int -> EraParams -> ShowS
[EraParams] -> ShowS
EraParams -> String
(Int -> EraParams -> ShowS)
-> (EraParams -> String)
-> ([EraParams] -> ShowS)
-> Show EraParams
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [EraParams] -> ShowS
$cshowList :: [EraParams] -> ShowS
show :: EraParams -> String
$cshow :: EraParams -> String
showsPrec :: Int -> EraParams -> ShowS
$cshowsPrec :: Int -> EraParams -> ShowS
Show, EraParams -> EraParams -> Bool
(EraParams -> EraParams -> Bool)
-> (EraParams -> EraParams -> Bool) -> Eq EraParams
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: EraParams -> EraParams -> Bool
$c/= :: EraParams -> EraParams -> Bool
== :: EraParams -> EraParams -> Bool
$c== :: EraParams -> EraParams -> Bool
Eq, (forall x. EraParams -> Rep EraParams x)
-> (forall x. Rep EraParams x -> EraParams) -> Generic EraParams
forall x. Rep EraParams x -> EraParams
forall x. EraParams -> Rep EraParams x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cto :: forall x. Rep EraParams x -> EraParams
$cfrom :: forall x. EraParams -> Rep EraParams x
Generic)
  deriving anyclass (Context -> EraParams -> IO (Maybe ThunkInfo)
Proxy EraParams -> String
(Context -> EraParams -> IO (Maybe ThunkInfo))
-> (Context -> EraParams -> IO (Maybe ThunkInfo))
-> (Proxy EraParams -> String)
-> NoThunks EraParams
forall a.
(Context -> a -> IO (Maybe ThunkInfo))
-> (Context -> a -> IO (Maybe ThunkInfo))
-> (Proxy a -> String)
-> NoThunks a
showTypeOf :: Proxy EraParams -> String
$cshowTypeOf :: Proxy EraParams -> String
wNoThunks :: Context -> EraParams -> IO (Maybe ThunkInfo)
$cwNoThunks :: Context -> EraParams -> IO (Maybe ThunkInfo)
noThunks :: Context -> EraParams -> IO (Maybe ThunkInfo)
$cnoThunks :: Context -> EraParams -> IO (Maybe ThunkInfo)
NoThunks)

-- | Default 'EraParams'
--
-- We set
--
-- * epoch size to @10k@ slots
-- * the safe zone to @2k@ slots
-- * the upper bound to 'NoLowerBound'
--
-- This is primarily useful for tests.
defaultEraParams :: SecurityParam -> SlotLength -> EraParams
defaultEraParams :: SecurityParam -> SlotLength -> EraParams
defaultEraParams (SecurityParam Word64
k) SlotLength
slotLength = EraParams :: EpochSize -> SlotLength -> SafeZone -> EraParams
EraParams {
      eraEpochSize :: EpochSize
eraEpochSize  = Word64 -> EpochSize
EpochSize (Word64
k Word64 -> Word64 -> Word64
forall a. Num a => a -> a -> a
* Word64
10)
    , eraSlotLength :: SlotLength
eraSlotLength = SlotLength
slotLength
    , eraSafeZone :: SafeZone
eraSafeZone   = Word64 -> SafeZone
StandardSafeZone (Word64
k Word64 -> Word64 -> Word64
forall a. Num a => a -> a -> a
* Word64
2)
    }

-- | Zone in which it is guaranteed that no hard fork can take place
data SafeZone =
    -- | Standard safe zone
    --
    -- We record
    --
    -- * Number of slots from the tip of the ledger.
    --   This should be (at least) the number of slots in which we are
    --   guaranteed to have @k@ blocks.
    -- * Optionally, an 'EpochNo' before which no hard fork can take place.
    StandardSafeZone !Word64

    -- | Pretend the transition to the next era will not take place.
    --
    -- This constructor is marked as unsafe because it effectively extends
    -- the safe zone of this era indefinitely into the future. This means that
    -- we might reach invalid conclusions when doing
    --
    -- * slot to time conversions for blocks that are past the actual safe zone
    -- * time to slot conversions for the current time, when behind in syncing
    --
    -- This is safe when the code is simply not yet ready to transition to the
    -- next era, because in that case, we can be sure that blocks that come in
    -- are still from this era. It also means that we can always /produce/ a
    -- block, no matter how far ahead of the current ledger we are.
    --
    -- If the code is ready for the transition, just awaiting an update
    -- proposal, then 'LowerBound' can be used instead.
    --
    -- This constructor can be regarded as an " extreme " version of
    -- 'LowerBound', and can be used for similar reasons.
  | UnsafeIndefiniteSafeZone
  deriving stock    (Int -> SafeZone -> ShowS
[SafeZone] -> ShowS
SafeZone -> String
(Int -> SafeZone -> ShowS)
-> (SafeZone -> String) -> ([SafeZone] -> ShowS) -> Show SafeZone
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [SafeZone] -> ShowS
$cshowList :: [SafeZone] -> ShowS
show :: SafeZone -> String
$cshow :: SafeZone -> String
showsPrec :: Int -> SafeZone -> ShowS
$cshowsPrec :: Int -> SafeZone -> ShowS
Show, SafeZone -> SafeZone -> Bool
(SafeZone -> SafeZone -> Bool)
-> (SafeZone -> SafeZone -> Bool) -> Eq SafeZone
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: SafeZone -> SafeZone -> Bool
$c/= :: SafeZone -> SafeZone -> Bool
== :: SafeZone -> SafeZone -> Bool
$c== :: SafeZone -> SafeZone -> Bool
Eq, (forall x. SafeZone -> Rep SafeZone x)
-> (forall x. Rep SafeZone x -> SafeZone) -> Generic SafeZone
forall x. Rep SafeZone x -> SafeZone
forall x. SafeZone -> Rep SafeZone x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cto :: forall x. Rep SafeZone x -> SafeZone
$cfrom :: forall x. SafeZone -> Rep SafeZone x
Generic)
  deriving anyclass (Context -> SafeZone -> IO (Maybe ThunkInfo)
Proxy SafeZone -> String
(Context -> SafeZone -> IO (Maybe ThunkInfo))
-> (Context -> SafeZone -> IO (Maybe ThunkInfo))
-> (Proxy SafeZone -> String)
-> NoThunks SafeZone
forall a.
(Context -> a -> IO (Maybe ThunkInfo))
-> (Context -> a -> IO (Maybe ThunkInfo))
-> (Proxy a -> String)
-> NoThunks a
showTypeOf :: Proxy SafeZone -> String
$cshowTypeOf :: Proxy SafeZone -> String
wNoThunks :: Context -> SafeZone -> IO (Maybe ThunkInfo)
$cwNoThunks :: Context -> SafeZone -> IO (Maybe ThunkInfo)
noThunks :: Context -> SafeZone -> IO (Maybe ThunkInfo)
$cnoThunks :: Context -> SafeZone -> IO (Maybe ThunkInfo)
NoThunks)

{-------------------------------------------------------------------------------
  Serialisation
-------------------------------------------------------------------------------}

instance Serialise SafeZone where
  encode :: SafeZone -> Encoding
encode = \case
      StandardSafeZone Word64
safeFromTip -> [Encoding] -> Encoding
forall a. Monoid a => [a] -> a
mconcat [
          Word -> Encoding
encodeListLen Word
3
        , Word8 -> Encoding
encodeWord8 Word8
0
        , Word64 -> Encoding
forall a. Serialise a => a -> Encoding
encode Word64
safeFromTip
          -- For backward compatibility we still encode safeBeforeEpoch
        , Encoding
encodeSafeBeforeEpoch
        ]
      SafeZone
UnsafeIndefiniteSafeZone -> [Encoding] -> Encoding
forall a. Monoid a => [a] -> a
mconcat [
          Word -> Encoding
encodeListLen Word
1
        , Word8 -> Encoding
encodeWord8 Word8
1
        ]
  decode :: Decoder s SafeZone
decode = do
    Int
size <- Decoder s Int
forall s. Decoder s Int
decodeListLen
    Word8
tag  <- Decoder s Word8
forall s. Decoder s Word8
decodeWord8
    case (Int
size, Word8
tag) of
      (Int
3, Word8
0) -> Word64 -> SafeZone
StandardSafeZone (Word64 -> SafeZone) -> Decoder s Word64 -> Decoder s SafeZone
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Decoder s Word64
forall a s. Serialise a => Decoder s a
decode Decoder s SafeZone -> Decoder s () -> Decoder s SafeZone
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f a
<* Decoder s ()
forall s. Decoder s ()
decodeSafeBeforeEpoch
      (Int
1, Word8
1) -> SafeZone -> Decoder s SafeZone
forall (m :: * -> *) a. Monad m => a -> m a
return SafeZone
UnsafeIndefiniteSafeZone
      (Int, Word8)
_      -> String -> Decoder s SafeZone
forall (m :: * -> *) a. MonadFail m => String -> m a
fail (String -> Decoder s SafeZone) -> String -> Decoder s SafeZone
forall a b. (a -> b) -> a -> b
$ String
"SafeZone: invalid size and tag " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> (Int, Word8) -> String
forall a. Show a => a -> String
show (Int
size, Word8
tag)

-- | Artificial encoder for backward compatibility, see #2646.
encodeSafeBeforeEpoch :: Encoding
encodeSafeBeforeEpoch :: Encoding
encodeSafeBeforeEpoch = Word -> Encoding
encodeListLen Word
1 Encoding -> Encoding -> Encoding
forall a. Semigroup a => a -> a -> a
<> Word8 -> Encoding
encodeWord8 Word8
0

-- | Artificial decoder for backward compatibility, see #2646.
decodeSafeBeforeEpoch :: Decoder s ()
decodeSafeBeforeEpoch :: Decoder s ()
decodeSafeBeforeEpoch = do
    Int
size <- Decoder s Int
forall s. Decoder s Int
decodeListLen
    Word8
tag  <- Decoder s Word8
forall s. Decoder s Word8
decodeWord8
    case (Int
size, Word8
tag) of
      (Int
1, Word8
0) -> () -> Decoder s ()
forall (m :: * -> *) a. Monad m => a -> m a
return ()
      (Int
2, Word8
1) -> Decoder s EpochNo -> Decoder s ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (Decoder s EpochNo -> Decoder s ())
-> Decoder s EpochNo -> Decoder s ()
forall a b. (a -> b) -> a -> b
$ forall s. Serialise EpochNo => Decoder s EpochNo
forall a s. Serialise a => Decoder s a
decode @EpochNo
      (Int, Word8)
_      -> String -> Decoder s ()
forall (m :: * -> *) a. MonadFail m => String -> m a
fail (String -> Decoder s ()) -> String -> Decoder s ()
forall a b. (a -> b) -> a -> b
$ String
"SafeBeforeEpoch: invalid size and tag " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> (Int, Word8) -> String
forall a. Show a => a -> String
show (Int
size, Word8
tag)

instance Serialise EraParams where
  encode :: EraParams -> Encoding
encode EraParams{SlotLength
EpochSize
SafeZone
eraSafeZone :: SafeZone
eraSlotLength :: SlotLength
eraEpochSize :: EpochSize
eraSafeZone :: EraParams -> SafeZone
eraSlotLength :: EraParams -> SlotLength
eraEpochSize :: EraParams -> EpochSize
..} = [Encoding] -> Encoding
forall a. Monoid a => [a] -> a
mconcat [
        Word -> Encoding
encodeListLen Word
3
      , Word64 -> Encoding
forall a. Serialise a => a -> Encoding
encode (EpochSize -> Word64
unEpochSize EpochSize
eraEpochSize)
      , SlotLength -> Encoding
forall a. Serialise a => a -> Encoding
encode SlotLength
eraSlotLength
      , SafeZone -> Encoding
forall a. Serialise a => a -> Encoding
encode SafeZone
eraSafeZone
      ]

  decode :: Decoder s EraParams
decode = do
      Text -> Int -> Decoder s ()
forall s. Text -> Int -> Decoder s ()
enforceSize Text
"EraParams" Int
3
      EpochSize
eraEpochSize  <- Word64 -> EpochSize
EpochSize (Word64 -> EpochSize) -> Decoder s Word64 -> Decoder s EpochSize
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Decoder s Word64
forall a s. Serialise a => Decoder s a
decode
      SlotLength
eraSlotLength <- Decoder s SlotLength
forall a s. Serialise a => Decoder s a
decode
      SafeZone
eraSafeZone   <- Decoder s SafeZone
forall a s. Serialise a => Decoder s a
decode
      EraParams -> Decoder s EraParams
forall (m :: * -> *) a. Monad m => a -> m a
return EraParams :: EpochSize -> SlotLength -> SafeZone -> EraParams
EraParams{SlotLength
EpochSize
SafeZone
eraSafeZone :: SafeZone
eraSlotLength :: SlotLength
eraEpochSize :: EpochSize
eraSafeZone :: SafeZone
eraSlotLength :: SlotLength
eraEpochSize :: EpochSize
..}