Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add epochCeiling function to Primitive.Types. #1114

Merged
merged 6 commits into from
Dec 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions lib/core/src/Cardano/Wallet/Primitive/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ module Cardano.Wallet.Primitive.Types
, SlotNo (..)
, EpochNo (..)
, unsafeEpochNo
, epochStartTime
, epochPred
, epochSucc
, epochCeiling
, epochFloor
, SlotParameters (..)
, SlotLength (..)
, EpochLength (..)
Expand Down Expand Up @@ -1095,6 +1100,54 @@ unsafeEpochNo epochNo
maxEpochNo :: Word32
maxEpochNo = fromIntegral @Word31 $ unEpochNo maxBound

-- | Calculate the time at which an epoch begins.
epochStartTime :: SlotParameters -> EpochNo -> UTCTime
epochStartTime sps e = slotStartTime sps $ SlotId e 0

-- | Return the epoch immediately before the given epoch, or 'Nothing' if there
-- is no representable epoch before the given epoch.
epochPred :: EpochNo -> Maybe EpochNo
epochPred (EpochNo e)
| e == minBound = Nothing
| otherwise = Just $ EpochNo $ pred e

-- | Return the epoch immediately after the given epoch, or 'Nothing' if there
-- is no representable epoch after the given epoch.
epochSucc :: EpochNo -> Maybe EpochNo
epochSucc (EpochNo e)
| e == maxBound = Nothing
| otherwise = Just $ EpochNo $ succ e

-- | For the given time 't', calculate the number of the earliest epoch with
-- start time 's' such that 't ≤ s'.
--
-- Returns 'Nothing' if the calculation would result in an epoch number that is
-- not representable.
epochCeiling :: SlotParameters -> UTCTime -> Maybe EpochNo
epochCeiling sps t
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved
| t < timeMin = Just minBound
| t > timeMax = Nothing
| otherwise = case slotCeiling sps t of
SlotId epoch 0 -> Just epoch
SlotId epoch _ -> epochSucc epoch
where
timeMin = epochStartTime sps minBound
timeMax = epochStartTime sps maxBound

-- | For the given time 't', calculate the number of the latest epoch with
-- start time 's' such that 's ≤ t'.
--
-- Returns 'Nothing' if the calculation would result in an epoch number that is
-- not representable.
epochFloor :: SlotParameters -> UTCTime -> Maybe EpochNo
epochFloor sps t
| t < timeMin = Nothing
| t > timeMax = Just maxBound
| otherwise = epochNumber <$> slotFloor sps t
where
timeMin = epochStartTime sps minBound
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion]

   timeMin = epochStartTime sps minBound
    timeMax = epochStartTime sps maxBound

is also in lines 1134-5 - maybe it is good idea to not duplicate this and put outside where once?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well spotted!

I was thinking about doing this. However, I'm not (yet) convinced that it would actually help us much. Let's suppose we did define the following functions:

-- | Calculates the start time of the earliest representable epoch.
epochStartTimeMinBound :: SlotParameters -> UTCTime
epochStartTimeMinBound = flip epochStartTime minBound

--- | Calculates the start time of the latest representable epoch.
epochStartTimeMaxBound :: SlotParameters -> UTCTime
epochStartTimeMaxBound = flip epochStartTime maxBound

We'd then be able to replace all instances of:

epochStartTime sps minBound

With:

epochStartTimeMinBound sps

Do you think this would be worth it?

(I'm trying to think of a compelling reason to convince myself. Feel free to suggest a reason if I've missed something obvious!)

timeMax = epochStartTime sps maxBound

instance NFData SlotId

instance Buildable SlotId where
Expand Down
225 changes: 223 additions & 2 deletions lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ import Cardano.Wallet.Primitive.Types
, WalletName (..)
, balance
, computeUtxoStatistics
, epochCeiling
, epochFloor
, epochPred
, epochStartTime
, epochSucc
, excluding
, flatSlot
, fromFlatSlot
Expand Down Expand Up @@ -121,7 +126,7 @@ import Data.Text
import Data.Text.Class
( TextDecodingError (..), fromText, toText )
import Data.Time
( UTCTime )
( Day (ModifiedJulianDay), UTCTime, toModifiedJulianDay, utctDay )
import Data.Time.Utils
( utcTimePred, utcTimeSucc )
import Data.Word
Expand All @@ -145,6 +150,7 @@ import Test.QuickCheck
, NonNegative (..)
, NonZero (..)
, Property
, Small (..)
, arbitraryBoundedEnum
, arbitraryPrintableChar
, arbitrarySizedBoundedIntegral
Expand All @@ -171,7 +177,7 @@ import Test.QuickCheck.Monadic
import Test.Text.Roundtrip
( textRoundtrip )
import Test.Utils.Time
( genUniformTime, getUniformTime )
( genUniformTime, genUniformTimeWithinRange, getUniformTime )

import qualified Data.ByteArray as BA
import qualified Data.ByteString as BS
Expand Down Expand Up @@ -413,6 +419,187 @@ spec = do
property $ \(a :: Int) (b :: Int) ->
compare (InclusiveBound a) (InclusiveBound b) === compare a b

describe "Epoch arithmetic: predecessors and successors" $ do

let succN n = applyN n (epochSucc =<<)
let predN n = applyN n (epochPred =<<)

it "epochPred minBound == Nothing" $
epochPred minBound === Nothing

it "epochSucc maxBound == Nothing" $
epochSucc maxBound === Nothing

it "(applyN n epochSucc) . (applyN n epochPred) == id" $
withMaxSuccess 1000 $ property $
\(Small epochWord) (Small n) ->
let epoch = EpochNo epochWord
withinBounds = minBound + n <= unEpochNo epoch
expectedResult =
if withinBounds then Just epoch else Nothing
in
checkCoverage $
cover 10 withinBounds "within bounds" $
cover 10 (not withinBounds) "out of bounds" $
expectedResult === succN n (predN n $ Just epoch)

it "(applyN n epochPred) . (applyN n epochSucc) == id" $
withMaxSuccess 1000 $ property $
\(Small epochWord) (Small n) ->
let epoch = EpochNo $ maxBound - epochWord
withinBounds = maxBound - n >= unEpochNo epoch
expectedResult =
if withinBounds then Just epoch else Nothing
in
checkCoverage $
cover 10 withinBounds "within bounds" $
cover 10 (not withinBounds) "out of bounds" $
expectedResult === predN n (succN n $ Just epoch)
Copy link
Contributor

@paweljakubas paweljakubas Dec 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion]

Wouldn't be better to add properties in those two boundary cases where we apply n times epochPred and n times epochSucc but randomly picked (rather than one special case n times epochPred and later timesepochSucc`) and also require certain coverage of out-of-bounds cases?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion]

Wouldn't be better to add properties in those two boundary cases where we apply n times epochPred and n times epochSucc but randomly picked (rather than one special case n times epochPred and later timesepochSucc`) and also require certain coverage of out-of-bounds cases?

I'm not sure I understand what you mean here. Would you be able to give an example?


describe "Epoch arithmetic: epochCeiling: core properties" $ do

it "epochStartTime (epochCeiling t) >= t" $
withMaxSuccess 1000 $ property $
\(SlotParametersAndTimePoint sps time) -> do
let timeMaximum = epochStartTime sps maxBound
let withinBounds = time <= timeMaximum
checkCoverage $
cover 10 withinBounds "within bounds" $
cover 10 (not withinBounds) "out of bounds" $
case epochCeiling sps time of
Nothing -> not withinBounds
Just en -> time <= epochStartTime sps en

it "epochStartTime (epochPred (epochCeiling t)) < t" $
withMaxSuccess 1000 $ property $
\(SlotParametersAndTimePoint sps time) -> do
let timeMaximum = epochStartTime sps maxBound
let withinBounds = time <= timeMaximum
checkCoverage $
cover 10 withinBounds "within bounds" $
cover 10 (not withinBounds) "out of bounds" $
case epochCeiling sps time of
Nothing -> not withinBounds
Just e1 -> case epochPred e1 of
Nothing -> e1 == minBound
Just e2 -> time > epochStartTime sps e2

describe "Epoch arithmetic: epochFloor: core properties" $ do

it "epochStartTime (epochFloor t) <= t" $
withMaxSuccess 1000 $ property $
\(SlotParametersAndTimePoint sps time) -> do
let timeMinimum = epochStartTime sps minBound
let withinBounds = time >= timeMinimum
checkCoverage $
cover 10 withinBounds "within bounds" $
cover 10 (not withinBounds) "out of bounds" $
case epochFloor sps time of
Nothing -> not withinBounds
Just en -> time >= epochStartTime sps en

it "epochStartTime (epochSucc (epochFloor t)) > t" $
withMaxSuccess 1000 $ property $
\(SlotParametersAndTimePoint sps time) -> do
let timeMinimum = epochStartTime sps minBound
let withinBounds = time >= timeMinimum
checkCoverage $
cover 10 withinBounds "within bounds" $
cover 10 (not withinBounds) "out of bounds" $
case epochFloor sps time of
Nothing -> not withinBounds
Just e1 -> case epochSucc e1 of
Nothing -> e1 == maxBound
Just e2 -> time < epochStartTime sps e2
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved

describe "Epoch arithmetic: epochCeiling: boundary conditions" $ do

it "epochCeiling . epochStartTime == id" $
withMaxSuccess 1000 $ property $ \(sps, epoch) ->
epochCeiling sps (epochStartTime sps epoch)
=== Just epoch

it "epochCeiling . utcTimePred . epochStartTime == id" $
withMaxSuccess 1000 $ property $ \(sps, epoch) ->
epoch > minBound ==> do
let fun = epochCeiling sps
. utcTimePred
. epochStartTime sps
Just epoch === fun epoch

it "epochPred . epochCeiling . utcTimeSucc . epochStartTime == id" $
withMaxSuccess 1000 $ property $ \(sps, epoch) ->
epoch < maxBound ==> do
let fun = (epochPred =<<)
. epochCeiling sps
. utcTimeSucc
. epochStartTime sps
Just epoch === fun epoch

it "epochCeiling (epochStartTime minBound) == minBound" $
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved
withMaxSuccess 1000 $ property $ \sps ->
epochCeiling sps (epochStartTime sps minBound)
=== Just minBound

it "epochCeiling (utcTimePred (epochStartTime minBound)) == minBound" $
withMaxSuccess 1000 $ property $ \sps ->
epochCeiling sps (utcTimePred (epochStartTime sps minBound))
=== Just minBound

it "epochCeiling (epochStartTime maxBound) == maxBound" $
withMaxSuccess 1000 $ property $ \sps ->
epochCeiling sps (epochStartTime sps maxBound)
=== Just maxBound
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved

it "epochCeiling (utcTimeSucc (epochStartTime maxBound)) == Nothing" $
withMaxSuccess 1000 $ property $ \sps ->
epochCeiling sps (utcTimeSucc (epochStartTime sps maxBound))
=== Nothing

describe "Epoch arithmetic: epochFloor: boundary conditions" $ do

it "epochFloor . epochStartTime == id" $
withMaxSuccess 1000 $ property $ \(sps, epoch) ->
epochFloor sps (epochStartTime sps epoch)
=== Just epoch

it "epochFloor . utcTimeSucc . epochStartTime == id" $
withMaxSuccess 1000 $ property $ \(sps, epoch) ->
epoch < maxBound ==> do
let fun = epochFloor sps
. utcTimeSucc
. epochStartTime sps
Just epoch === fun epoch

it "epochSucc . epochFloor . utcTimePred . epochStartTime == id" $
withMaxSuccess 1000 $ property $ \(sps, epoch) ->
epoch > minBound ==> do
let fun = (epochSucc =<<)
. epochFloor sps
. utcTimePred
. epochStartTime sps
Just epoch === fun epoch

it "epochFloor (epochStartTime minBound) == minBound" $
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved
withMaxSuccess 1000 $ property $ \sps ->
epochFloor sps (epochStartTime sps minBound)
=== Just minBound

it "epochFloor (utcTimePred (epochStartTime minBound)) == Nothing" $
withMaxSuccess 1000 $ property $ \sps ->
epochFloor sps (utcTimePred (epochStartTime sps minBound))
=== Nothing

it "epochFloor (epochStartTime maxBound) == maxBound" $
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved
withMaxSuccess 1000 $ property $ \sps ->
epochFloor sps (epochStartTime sps maxBound)
=== Just maxBound

it "epochFloor (utcTimeSucc (epochStartTime maxBound)) == maxBound" $
withMaxSuccess 1000 $ property $ \sps ->
epochFloor sps (utcTimeSucc (epochStartTime sps maxBound))
=== Just maxBound

describe "Slot arithmetic" $ do

it "slotFloor (slotStartTime slotMinBound) == Just slotMinBound" $
Expand Down Expand Up @@ -1103,6 +1290,10 @@ instance Arbitrary BlockHeader where
, pure $ Hash "BLOCK03"
]

instance Arbitrary EpochNo where
arbitrary = EpochNo <$> arbitrary
shrink = genericShrink

instance Arbitrary SlotId where
shrink _ = []
arbitrary = do
Expand Down Expand Up @@ -1165,6 +1356,36 @@ instance {-# OVERLAPS #-} Arbitrary (SlotParameters, SlotId) where
(el', slot') <- shrink (el, slot)
pure (SlotParameters el' sl st, slot')

-- | Combines a 'SlotParameters' object and a single point in time.
--
-- The point in time falls into one of the following categories:
--
-- 1. occurs during the lifetime of the blockchain;
-- 2. occurs before the earliest representable slot;
-- 3. occurs after the latest representable slot.
--
data SlotParametersAndTimePoint = SlotParametersAndTimePoint
{ getSlotParameters :: SlotParameters
, getTimePoint :: UTCTime
} deriving (Eq, Show)

instance Arbitrary SlotParametersAndTimePoint where
arbitrary = do
sps <- arbitrary
let timeA = 0
let timeB = toModifiedJulianDay $ utctDay $ epochStartTime sps minBound
let timeC = toModifiedJulianDay $ utctDay $ epochStartTime sps maxBound
let timeD = timeC * 2
(lowerBound, upperBound) <- oneof $ fmap pure
[ (timeA, timeB)
, (timeB, timeC)
, (timeC, timeD)
]
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved
time <- genUniformTimeWithinRange
(ModifiedJulianDay lowerBound)
(ModifiedJulianDay upperBound)
pure $ SlotParametersAndTimePoint sps time

-- | Note, for functions which works with both an epoch length and a slot id,
-- we need to make sure that the 'slotNumber' doesn't exceed the epoch length,
-- otherwise, all computations get mixed up.
Expand Down
Loading