diff --git a/cardano-wallet.cabal b/cardano-wallet.cabal index e0f8c7ccc08..585e709230b 100644 --- a/cardano-wallet.cabal +++ b/cardano-wallet.cabal @@ -58,11 +58,9 @@ library hs-source-dirs: src exposed-modules: - Cardano.ChainProducer - Cardano.ChainProducer.RustHttpBridge - Cardano.ChainProducer.RustHttpBridge.Api - Cardano.ChainProducer.RustHttpBridge.Client - Cardano.ChainProducer.RustHttpBridge.NetworkLayer + Cardano.NetworkLayer + Cardano.NetworkLayer.HttpBridge + Cardano.NetworkLayer.HttpBridge.Api Cardano.Wallet Cardano.Wallet.AddressDerivation Cardano.Wallet.AddressDiscovery @@ -71,7 +69,6 @@ library Cardano.Wallet.BlockSyncer Cardano.Wallet.Mnemonic Cardano.Wallet.Primitive - Cardano.Wallet.Slotting Servant.Extra.ContentTypes other-modules: Paths_cardano_wallet @@ -152,8 +149,6 @@ test-suite unit main-is: Main.hs other-modules: - Cardano.ChainProducer.RustHttpBridge.MockNetworkLayer - Cardano.ChainProducer.RustHttpBridgeSpec Cardano.Wallet.AddressDerivationSpec Cardano.Wallet.AddressDiscoverySpec Cardano.Wallet.Binary.PackfileSpec @@ -161,5 +156,7 @@ test-suite unit Cardano.Wallet.BlockSyncerSpec Cardano.Wallet.MnemonicSpec Cardano.Wallet.PrimitiveSpec - Cardano.Wallet.SlottingSpec Cardano.WalletSpec + Cardano.NetworkLayerSpec + Cardano.NetworkLayer.HttpBridgeSpec + Cardano.NetworkLayer.HttpBridge.ApiSpec diff --git a/src/Cardano/ChainProducer.hs b/src/Cardano/ChainProducer.hs deleted file mode 100644 index 33e2ef9c6a4..00000000000 --- a/src/Cardano/ChainProducer.hs +++ /dev/null @@ -1,38 +0,0 @@ --- | --- Copyright: © 2018-2019 IOHK --- License: MIT - -module Cardano.ChainProducer - ( MonadChainProducer (..) - , ErrGetNextBlocks (..) - ) where - -import Prelude - -import Cardano.Wallet.Primitive - ( Block ) -import Cardano.Wallet.Slotting - ( SlotId ) -import Control.Exception - ( Exception ) -import Control.Monad.Except - ( ExceptT ) -import Numeric.Natural - ( Natural ) - -class MonadChainProducer m where - -- | Get some blocks from the chain producer. - -- - -- This may retrieve less than the requested number of blocks. - -- It might return no blocks at all. - nextBlocks - :: Natural -- ^ Number of blocks to retrieve - -> SlotId -- ^ Starting point - -> ExceptT ErrGetNextBlocks m [Block] - --- | The things that can go wrong when retrieving blocks. -newtype ErrGetNextBlocks - = GetNextBlocksError String - deriving (Show, Eq) - -instance Exception ErrGetNextBlocks diff --git a/src/Cardano/ChainProducer/RustHttpBridge.hs b/src/Cardano/ChainProducer/RustHttpBridge.hs deleted file mode 100644 index 28ae8dc2f12..00000000000 --- a/src/Cardano/ChainProducer/RustHttpBridge.hs +++ /dev/null @@ -1,199 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} - --- | --- Copyright: © 2018-2019 IOHK --- License: MIT - -module Cardano.ChainProducer.RustHttpBridge - ( RustBackend - , runRustBackend - ) where - -import Prelude - -import Cardano.ChainProducer - ( ErrGetNextBlocks (..), MonadChainProducer (..) ) -import Cardano.ChainProducer.RustHttpBridge.NetworkLayer - ( NetworkLayer (..), NetworkLayerError ) -import Cardano.Wallet.Primitive - ( Block (..), BlockHeader (..), Hash (..) ) -import Cardano.Wallet.Slotting - ( EpochIndex - , LocalSlotIndex (..) - , SlotId (..) - , addSlots - , slotNext - , slotsPerEpoch - ) -import Control.Monad.Except - ( ExceptT (..), mapExceptT, runExceptT ) -import Control.Monad.IO.Class - ( MonadIO, liftIO ) -import Control.Monad.Reader - ( MonadReader, ReaderT (..), ask, lift ) -import Data.Bifunctor - ( first ) -import Data.Maybe - ( fromMaybe ) -import Numeric.Natural - ( Natural ) - -newtype RustBackend a = RustBackend - { runRB :: ReaderT (NetworkLayer IO) IO a - } deriving - ( Monad - , Applicative - , Functor - , MonadReader (NetworkLayer IO) - , MonadIO - ) - -runRustBackend :: NetworkLayer IO -> RustBackend a -> IO a -runRustBackend network action = runReaderT (runRB action) network - -getNetwork :: RustBackend (NetworkLayer IO) -getNetwork = ask - -instance MonadChainProducer RustBackend where - nextBlocks = rbNextBlocks - --- Note: This will be quite inefficient for at least two reasons. --- 1. If the number of blocks requested is small, it will fetch the same epoch --- pack file repeatedly. --- 2. Fetching the tip block and working backwards is not ideal. --- We will keep it for now, and it can be improved later. -rbNextBlocks - :: Natural -- ^ Number of blocks to retrieve - -> SlotId -- ^ Starting point - -> ExceptT ErrGetNextBlocks RustBackend [Block] -rbNextBlocks numBlocks start = do - net <- lift getNetwork - (tipHash, tip) <- fmap headerSlot <$> runNetworkLayer (getNetworkTip net) - epochBlocks <- blocksFromPacks net tip - lastBlocks <- unstableBlocks net tipHash tip epochBlocks - pure (epochBlocks ++ lastBlocks) - where - end = addSlots numBlocks start - - -- Grab blocks from epoch pack files - blocksFromPacks network tip = do - let epochs = epochRange numBlocks start tip - epochBlocks <- runNetworkLayer (getEpochs network epochs) - pure $ filter (blockIsBetween start end) (concat epochBlocks) - - -- The next slot after the last block. - slotAfter [] = Nothing - slotAfter bs = Just . slotNext . headerSlot . header . last $ bs - - -- Grab the remaining blocks which aren't packed in epoch files, - -- starting from the tip. - unstableBlocks network tipHash tip epochBlocks = do - let start' = fromMaybe start (slotAfter epochBlocks) - - lastBlocks <- if end > start' && start' <= tip - then runNetworkLayer $ fetchBlocksFromTip network start' tipHash - else pure [] - - pure $ filter (blockIsBefore end) lastBlocks - --- | Fetch epoch blocks until one fails. -getEpochs - :: Monad m - => NetworkLayer m - -> [EpochIndex] - -> ExceptT NetworkLayerError m [[Block]] -getEpochs network = mapUntilError (getEpoch network) - --- Fetch blocks which are not in epoch pack files. -fetchBlocksFromTip - :: Monad m - => NetworkLayer m - -> SlotId - -> Hash "BlockHeader" - -> ExceptT NetworkLayerError m [Block] -fetchBlocksFromTip network start tipHash = - reverse <$> workBackwards tipHash - where - workBackwards headerHash = do - block <- getBlock network headerHash - if blockIsAfter start block then do - blocks <- workBackwards $ prevBlockHash $ header block - pure (block:blocks) - else - pure [block] - -runNetworkLayer - :: ExceptT NetworkLayerError IO a - -> ExceptT ErrGetNextBlocks RustBackend a -runNetworkLayer = - mapExceptT (fmap handle . liftIO) - where - handle = first (GetNextBlocksError . show) - --- * Utility functions for monadic loops - --- | Apply an action to each element of a list, until an action fails, or there --- are no more elements. This is like mapM, except that it always succeeds and --- the resulting list might be smaller than the given list. -mapUntilError - :: Monad m - => (a -> ExceptT e m b) - -- ^ Action to run - -> [a] - -- ^ Elements - -> ExceptT e m [b] - -- ^ Results -mapUntilError action (x:xs) = ExceptT $ runExceptT (action x) >>= \case - Left _ -> pure $ Right [] - Right r -> runExceptT $ do - rs <- mapUntilError action xs - pure (r:rs) -mapUntilError _ [] = pure [] - --- * Slotting calculation utilities - --- | Gets the slot from a block header. -headerSlot :: BlockHeader -> SlotId -headerSlot bh = SlotId - (epochIndex (bh :: BlockHeader)) (slotNumber (bh :: BlockHeader)) - --- | Calculates which epochs to fetch, given a number of slots, and the start --- point. It takes into account the latest block available, and that the most --- recent epoch is never available in a pack file. -epochRange - :: Natural - -- ^ Number of slots - -> SlotId - -- ^ Start point - -> SlotId - -- ^ Latest block available - -> [EpochIndex] -epochRange - numBlocks - (SlotId startEpoch (LocalSlotIndex startSlot)) (SlotId tipEpoch _) - = [startEpoch .. min (tipEpoch - 1) (startEpoch + fromIntegral numEpochs)] - where - numEpochs = (numBlocks + fromIntegral startSlot) `div` slotsPerEpoch - --- | Predicate returns true iff the block is from the given slot or a later one. -blockIsSameOrAfter :: SlotId -> Block -> Bool -blockIsSameOrAfter s = (>= s) . headerSlot . header - --- | Predicate returns true iff the block is after then given slot -blockIsAfter :: SlotId -> Block -> Bool -blockIsAfter s = (> s) . headerSlot . header - --- | Predicate returns true iff the block is before the given slot. -blockIsBefore :: SlotId -> Block -> Bool -blockIsBefore s = (< s) . headerSlot . header - --- | @blockIsBetween start end@ Returns true if the block is in within the --- interval @[start, end)@. -blockIsBetween :: SlotId -> SlotId -> Block -> Bool -blockIsBetween start end b = blockIsSameOrAfter start b && blockIsBefore end b diff --git a/src/Cardano/ChainProducer/RustHttpBridge/Client.hs b/src/Cardano/ChainProducer/RustHttpBridge/Client.hs deleted file mode 100644 index bea30bf6f76..00000000000 --- a/src/Cardano/ChainProducer/RustHttpBridge/Client.hs +++ /dev/null @@ -1,88 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE RankNTypes #-} - --- | --- Copyright: © 2018-2019 IOHK --- License: MIT --- --- An API client for the Cardano HTTP Bridge. -module Cardano.ChainProducer.RustHttpBridge.Client - ( mkNetworkLayer - ) where - -import Prelude - -import Cardano.ChainProducer.RustHttpBridge.Api - ( Block, BlockHeader, EpochIndex, NetworkName, api ) -import Cardano.ChainProducer.RustHttpBridge.NetworkLayer - ( NetworkLayer (..), NetworkLayerError (..) ) -import Control.Monad.Except - ( ExceptT (..), throwError ) -import Crypto.Hash - ( HashAlgorithm, digestFromByteString ) -import Crypto.Hash.Algorithms - ( Blake2b_256 ) -import Data.Bifunctor - ( first ) -import Data.ByteArray - ( convert ) -import Network.HTTP.Client - ( Manager ) -import Servant.API - ( (:<|>) (..) ) -import Servant.Client - ( BaseUrl, ClientM, client, mkClientEnv, runClientM ) -import Servant.Extra.ContentTypes - ( Hash (..), WithHash (..) ) - -import qualified Cardano.ChainProducer.RustHttpBridge.Api as Api -import qualified Cardano.Wallet.Primitive as Primitive - --- | Retrieve a block identified by the unique hash of its header. -getBlockByHash :: NetworkName -> Hash Blake2b_256 BlockHeader -> ClientM Block - --- | Retrieve all the blocks for the epoch identified by the given integer ID. -getEpochById :: NetworkName -> EpochIndex -> ClientM [Block] - --- | Retrieve the header of the latest known block. -getTipBlockHeader :: NetworkName -> ClientM (WithHash Blake2b_256 BlockHeader) - -getBlockByHash - :<|> getEpochById - :<|> getTipBlockHeader - = client api - --- | Construct a new network layer -mkNetworkLayer :: Manager -> BaseUrl -> NetworkName -> NetworkLayer IO -mkNetworkLayer mgr baseUrl network = NetworkLayer - { getBlock = \hash -> do - hash' <- hashToApi' hash - run (Api.getBlock <$> getBlockByHash network hash') - , getEpoch = \ep -> run (map Api.getBlock <$> - getEpochById network (Api.EpochIndex ep)) - , getNetworkTip = run (blockHeaderHash <$> getTipBlockHeader network) - } - where - run query = ExceptT $ (first convertError) <$> runClientM query env - env = mkClientEnv mgr baseUrl - convertError = NetworkLayerError . show - -blockHeaderHash - :: WithHash algorithm BlockHeader - -> (Primitive.Hash "BlockHeader", Primitive.BlockHeader) -blockHeaderHash (WithHash h (Api.BlockHeader bh)) = - (Primitive.Hash (convert h), bh) - -hashToApi :: HashAlgorithm a => Primitive.Hash h -> Maybe (Hash a b) -hashToApi (Primitive.Hash h) = Hash <$> digestFromByteString h - --- | Converts a Hash to the Digest type that the Api module requires. -hashToApi' - :: (Monad m, HashAlgorithm algorithm) - => Primitive.Hash a - -> ExceptT NetworkLayerError m (Hash algorithm b) -hashToApi' h = case hashToApi h of - Just h' -> pure h' - Nothing -> throwError - $ NetworkLayerError "hashToApi: Digest was of the wrong length" diff --git a/src/Cardano/ChainProducer/RustHttpBridge/NetworkLayer.hs b/src/Cardano/ChainProducer/RustHttpBridge/NetworkLayer.hs deleted file mode 100644 index e66458a557b..00000000000 --- a/src/Cardano/ChainProducer/RustHttpBridge/NetworkLayer.hs +++ /dev/null @@ -1,39 +0,0 @@ -{-# LANGUAGE DataKinds #-} - --- | --- Copyright: © 2018-2019 IOHK --- License: MIT --- --- Representation of a network layer - -module Cardano.ChainProducer.RustHttpBridge.NetworkLayer - ( NetworkLayer (..) - , NetworkLayerError(..) - ) where - -import Prelude - -import Cardano.Wallet.Primitive - ( Block (..), BlockHeader (..), Hash (..) ) -import Cardano.Wallet.Slotting - ( EpochIndex ) -import Control.Exception - ( Exception (..) ) -import Control.Monad.Except - ( ExceptT ) - --- | Endpoints of the cardano-http-bridge API. -data NetworkLayer m = NetworkLayer - { getBlock - :: Hash "BlockHeader" -> ExceptT NetworkLayerError m Block - , getEpoch - :: EpochIndex -> ExceptT NetworkLayerError m [Block] - , getNetworkTip - :: ExceptT NetworkLayerError m (Hash "BlockHeader", BlockHeader) - } - -newtype NetworkLayerError - = NetworkLayerError String - deriving (Show, Eq) - -instance Exception NetworkLayerError diff --git a/src/Cardano/NetworkLayer.hs b/src/Cardano/NetworkLayer.hs new file mode 100644 index 00000000000..997e6a7c240 --- /dev/null +++ b/src/Cardano/NetworkLayer.hs @@ -0,0 +1,16 @@ +{-# LANGUAGE DataKinds #-} + +module Cardano.NetworkLayer where + +import Cardano.Wallet.Primitive + ( Block, BlockHeader (..), Hash (..), SlotId ) +import Control.Monad.Except + ( ExceptT ) +import Data.Word + ( Word64 ) + + +data NetworkLayer m e0 e1 = NetworkLayer + { nextBlocks :: Word64 -> SlotId -> ExceptT e0 m [Block] + , networkTip :: ExceptT e1 m (Hash "BlockHeader", BlockHeader) + } diff --git a/src/Cardano/NetworkLayer/HttpBridge.hs b/src/Cardano/NetworkLayer/HttpBridge.hs new file mode 100644 index 00000000000..f08e2a65a1c --- /dev/null +++ b/src/Cardano/NetworkLayer/HttpBridge.hs @@ -0,0 +1,267 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} + +-- | +-- Copyright: © 2018-2019 IOHK +-- License: MIT +-- +-- This module contains the necessary logic to talk to implement the network +-- layer using the cardano-http-bridge as a chain producer. + +module Cardano.NetworkLayer.HttpBridge where + +import Prelude + +import Cardano.NetworkLayer.HttpBridge.Api + ( ApiT (..), EpochIndex (..), NetworkName, api ) +import Cardano.Wallet.Primitive + ( Block (..) + , BlockHeader (..) + , Hash (..) + , Hash (..) + , SlotId (..) + , blockIsAfter + , blockIsBefore + , blockIsBetween + , slotIncr + , slotsPerEpoch + ) +import Control.Exception + ( Exception ) +import Control.Monad.Except + ( ExceptT (..), mapExceptT, runExceptT, throwError ) +import Control.Monad.IO.Class + ( MonadIO, liftIO ) +import Control.Monad.Reader + ( MonadReader, ReaderT (..), ask, lift ) +import Crypto.Hash + ( HashAlgorithm, digestFromByteString ) +import Crypto.Hash.Algorithms + ( Blake2b_256 ) +import Data.Bifunctor + ( first ) +import Data.ByteArray + ( convert ) +import Data.Maybe + ( fromMaybe ) +import Data.Word + ( Word64 ) +import Network.HTTP.Client + ( Manager ) +import Servant.API + ( (:<|>) (..) ) +import Servant.Client + ( BaseUrl, ClientM, client, mkClientEnv, runClientM ) +import Servant.Extra.ContentTypes + ( WithHash (..) ) + +import qualified Servant.Extra.ContentTypes as Api + + +newtype RustBackend a = RustBackend + { runRB :: ReaderT (HttpBridge IO) IO a + } deriving + ( Monad + , Applicative + , Functor + , MonadReader (HttpBridge IO) + , MonadIO + ) + +runRustBackend :: HttpBridge IO -> RustBackend a -> IO a +runRustBackend network action = runReaderT (runRB action) network + +getNetwork :: RustBackend (HttpBridge IO) +getNetwork = ask + +-- | The things that can go wrong when retrieving blocks. +newtype ErrGetNextBlocks + = GetNextBlocksError String + deriving (Show, Eq) + +instance Exception ErrGetNextBlocks + +-- Note: This will be quite inefficient for at least two reasons. +-- 1. If the number of blocks requested is small, it will fetch the same epoch +-- pack file repeatedly. +-- 2. Fetching the tip block and working backwards is not ideal. +-- We will keep it for now, and it can be improved later. +nextBlocks + :: Word64 -- ^ Number of blocks to retrieve + -> SlotId -- ^ Starting point + -> ExceptT ErrGetNextBlocks RustBackend [Block] +nextBlocks numBlocks start = do + net <- lift getNetwork + (tipHash, tip) <- fmap slotId <$> runHttpBridge (getNetworkTip net) + epochBlocks <- blocksFromPacks net tip + lastBlocks <- unstableBlocks net tipHash tip epochBlocks + pure (epochBlocks ++ lastBlocks) + where + end = slotIncr numBlocks start + + -- Grab blocks from epoch pack files + blocksFromPacks network tip = do + let epochs = epochRange numBlocks start tip + epochBlocks <- runHttpBridge (getEpochs network epochs) + pure $ filter (blockIsBetween start end) (concat epochBlocks) + + -- The next slot after the last block. + slotAfter [] = Nothing + slotAfter bs = Just . succ . slotId . header . last $ bs + + -- Grab the remaining blocks which aren't packed in epoch files, + -- starting from the tip. + unstableBlocks network tipHash tip epochBlocks = do + let start' = fromMaybe start (slotAfter epochBlocks) + + lastBlocks <- if end > start' && start' <= tip + then runHttpBridge $ fetchBlocksFromTip network start' tipHash + else pure [] + + pure $ filter (blockIsBefore end) lastBlocks + +-- | Calculates which epochs to fetch, given a number of slots, and the start +-- point. It takes into account the latest block available, and that the most +-- recent epoch is never available in a pack file. +epochRange + :: Word64 + -- ^ Number of slots + -> SlotId + -- ^ Start point + -> SlotId + -- ^ Latest block available + -> [Word64] +epochRange numBlocks (SlotId startEpoch startSlot) (SlotId tipEpoch _) = + [startEpoch .. min (tipEpoch - 1) (startEpoch + fromIntegral numEpochs)] + where + numEpochs = (numBlocks + fromIntegral startSlot) `div` slotsPerEpoch + +-- | Fetch epoch blocks until one fails. +getEpochs + :: Monad m + => HttpBridge m + -> [Word64] + -> ExceptT HttpBridgeError m [[Block]] +getEpochs network = mapUntilError (getEpoch network) + +-- Fetch blocks which are not in epoch pack files. +fetchBlocksFromTip + :: Monad m + => HttpBridge m + -> SlotId + -> Hash "BlockHeader" + -> ExceptT HttpBridgeError m [Block] +fetchBlocksFromTip network start tipHash = + reverse <$> workBackwards tipHash + where + workBackwards headerHash = do + block <- getBlock network headerHash + if blockIsAfter start block then do + blocks <- workBackwards $ prevBlockHash $ header block + pure (block:blocks) + else + pure [block] + +runHttpBridge + :: ExceptT HttpBridgeError IO a + -> ExceptT ErrGetNextBlocks RustBackend a +runHttpBridge = + mapExceptT (fmap handle . liftIO) + where + handle = first (GetNextBlocksError . show) + + +-- * Utility functions for monadic loops + +-- | Apply an action to each element of a list, until an action fails, or there +-- are no more elements. This is like mapM, except that it always succeeds and +-- the resulting list might be smaller than the given list. +mapUntilError + :: Monad m + => (a -> ExceptT e m b) + -- ^ Action to run + -> [a] + -- ^ Elements + -> ExceptT e m [b] + -- ^ Results +mapUntilError action (x:xs) = ExceptT $ runExceptT (action x) >>= \case + Left _ -> pure $ Right [] + Right r -> runExceptT $ do + rs <- mapUntilError action xs + pure (r:rs) +mapUntilError _ [] = pure [] + + +{------------------------------------------------------------------------------- + HTTP-Bridge Client +-------------------------------------------------------------------------------} + +-- | Endpoints of the cardano-http-bridge API. +data HttpBridge m = HttpBridge + { getBlock + :: Hash "BlockHeader" -> ExceptT HttpBridgeError m Block + , getEpoch + :: Word64 -> ExceptT HttpBridgeError m [Block] + , getNetworkTip + :: ExceptT HttpBridgeError m (Hash "BlockHeader", BlockHeader) + } + +newtype HttpBridgeError + = HttpBridgeError String + deriving (Show, Eq) + +instance Exception HttpBridgeError + +-- | Retrieve a block identified by the unique hash of its header. +getBlockByHash :: NetworkName -> Api.Hash Blake2b_256 (ApiT BlockHeader) -> ClientM (ApiT Block) + +-- | Retrieve all the blocks for the epoch identified by the given integer ID. +getEpochById :: NetworkName -> EpochIndex -> ClientM [ApiT Block] + +-- | Retrieve the header of the latest known block. +getTipBlockHeader :: NetworkName -> ClientM (WithHash Blake2b_256 (ApiT BlockHeader)) + +getBlockByHash + :<|> getEpochById + :<|> getTipBlockHeader + = client api + +-- | Construct a new network layer +mkHttpBridge :: Manager -> BaseUrl -> NetworkName -> HttpBridge IO +mkHttpBridge mgr baseUrl network = HttpBridge + { getBlock = \hash -> do + hash' <- hashToApi' hash + run (getApiT <$> getBlockByHash network hash') + , getEpoch = \ep -> run (map getApiT <$> + getEpochById network (EpochIndex ep)) + , getNetworkTip = run (blockHeaderHash <$> getTipBlockHeader network) + } + where + run :: ClientM a -> ExceptT HttpBridgeError IO a + run query = ExceptT $ (first convertError) <$> runClientM query env + env = mkClientEnv mgr baseUrl + convertError = HttpBridgeError . show + +blockHeaderHash + :: WithHash algorithm (ApiT BlockHeader) + -> (Hash "BlockHeader", BlockHeader) +blockHeaderHash (WithHash h (ApiT bh)) = + (Hash (convert h), bh) + +hashToApi :: HashAlgorithm a => Hash h -> Maybe (Api.Hash a b) +hashToApi (Hash h) = Api.Hash <$> digestFromByteString h + +-- | Converts a Hash to the Digest type that the Api module requires. +hashToApi' + :: (Monad m, HashAlgorithm algorithm) + => Hash a + -> ExceptT HttpBridgeError m (Api.Hash algorithm b) +hashToApi' h = case hashToApi h of + Just h' -> pure h' + Nothing -> throwError + $ HttpBridgeError "hashToApi: Digest was of the wrong length" diff --git a/src/Cardano/ChainProducer/RustHttpBridge/Api.hs b/src/Cardano/NetworkLayer/HttpBridge/Api.hs similarity index 66% rename from src/Cardano/ChainProducer/RustHttpBridge/Api.hs rename to src/Cardano/NetworkLayer/HttpBridge/Api.hs index 551d9697454..b70103b17d8 100644 --- a/src/Cardano/ChainProducer/RustHttpBridge/Api.hs +++ b/src/Cardano/NetworkLayer/HttpBridge/Api.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeOperators #-} -- | @@ -6,31 +7,33 @@ -- License: MIT -- -- An API specification for the Cardano HTTP Bridge. -module Cardano.ChainProducer.RustHttpBridge.Api +module Cardano.NetworkLayer.HttpBridge.Api ( Api , api - , Block (..) - , BlockHeader (..) + , ApiT(..) , EpochIndex (..) , NetworkName (..) ) where +import Prelude + import Cardano.Wallet.Binary ( decodeBlock, decodeBlockHeader ) +import Cardano.Wallet.Primitive + ( Block, BlockHeader ) import Crypto.Hash.Algorithms ( Blake2b_256 ) import Data.Proxy ( Proxy (..) ) import Data.Text ( Text ) -import Prelude +import Data.Word + ( Word64 ) import Servant.API ( (:<|>), (:>), Capture, Get, ToHttpApiData (..) ) import Servant.Extra.ContentTypes ( CBOR, ComputeHash, FromCBOR (..), Hash, Packed, WithHash ) -import qualified Cardano.Wallet.Primitive as Primitive -import qualified Cardano.Wallet.Slotting as Slotting api :: Proxy Api api = Proxy @@ -44,45 +47,37 @@ type Api type GetBlockByHash = Capture "networkName" NetworkName :> "block" - :> Capture "blockHeaderHash" (Hash Blake2b_256 BlockHeader) - :> Get '[CBOR] Block + :> Capture "blockHeaderHash" (Hash Blake2b_256 (ApiT BlockHeader)) + :> Get '[CBOR] (ApiT Block) -- | Retrieve all the blocks for the epoch identified by the given integer ID. type GetEpochById = Capture "networkName" NetworkName :> "epoch" :> Capture "epochId" EpochIndex - :> Get '[Packed CBOR] [Block] + :> Get '[Packed CBOR] [ApiT Block] -- | Retrieve the header of the latest known block. type GetTipBlockHeader = Capture "networkName" NetworkName :> "tip" - :> Get '[ComputeHash Blake2b_256 CBOR] (WithHash Blake2b_256 BlockHeader) - --- | Represents a block. -newtype Block = Block - { getBlock :: Primitive.Block - } deriving Eq + :> Get '[ComputeHash Blake2b_256 CBOR] (WithHash Blake2b_256 (ApiT BlockHeader)) -instance FromCBOR Block where - fromCBOR = Block <$> decodeBlock +newtype ApiT a = ApiT { getApiT :: a } deriving (Show) --- | Represents a block header. -newtype BlockHeader = BlockHeader - { getBlockHeader :: Primitive.BlockHeader - } deriving Eq +instance FromCBOR (ApiT Block) where + fromCBOR = ApiT <$> decodeBlock -instance FromCBOR BlockHeader where - fromCBOR = BlockHeader <$> decodeBlockHeader +instance FromCBOR (ApiT BlockHeader) where + fromCBOR = ApiT <$> decodeBlockHeader -- | Represents a unique epoch. newtype EpochIndex = EpochIndex - { getEpochIndex :: Slotting.EpochIndex + { getEpochIndex :: Word64 } deriving (Eq, Show) instance ToHttpApiData (EpochIndex) where - toUrlPiece = toUrlPiece . Slotting.getEpochIndex . getEpochIndex + toUrlPiece = toUrlPiece . getEpochIndex -- | Represents the name of a Cardano network. newtype NetworkName = NetworkName diff --git a/src/Cardano/Wallet/Binary.hs b/src/Cardano/Wallet/Binary.hs index cbe1320a0ee..7e686c956d0 100644 --- a/src/Cardano/Wallet/Binary.hs +++ b/src/Cardano/Wallet/Binary.hs @@ -46,12 +46,11 @@ import Cardano.Wallet.Primitive , BlockHeader (..) , Coin (..) , Hash (..) + , SlotId (..) , Tx (..) , TxIn (..) , TxOut (..) ) -import Cardano.Wallet.Slotting - ( EpochIndex (..), LocalSlotIndex (..) ) import Control.Monad ( void ) import Crypto.Hash @@ -201,7 +200,7 @@ decodeGenesisBlockHeader = do -- number of `0`. In practices, when parsing a full epoch, we can discard -- the genesis block entirely and we won't bother about modelling this -- extra complexity at the type-level. That's a bit dodgy though. - return $ BlockHeader (EpochIndex epoch) (LocalSlotIndex 0) previous + return $ BlockHeader (SlotId epoch 0) previous decodeGenesisConsensusData :: CBOR.Decoder s Word64 decodeGenesisConsensusData = do @@ -256,8 +255,7 @@ decodeMainBlockHeader = do _ <- decodeMainProof (epoch, slot) <- decodeMainConsensusData _ <- decodeMainExtraData - return $ BlockHeader (EpochIndex epoch) - (LocalSlotIndex slot) previous + return $ BlockHeader (SlotId epoch slot) previous decodeMainConsensusData :: CBOR.Decoder s (Word64, Word16) decodeMainConsensusData = do diff --git a/src/Cardano/Wallet/Primitive.hs b/src/Cardano/Wallet/Primitive.hs index 0450da9e971..3ae3ce3c60e 100644 --- a/src/Cardano/Wallet/Primitive.hs +++ b/src/Cardano/Wallet/Primitive.hs @@ -5,6 +5,7 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} @@ -48,7 +49,17 @@ module Cardano.Wallet.Primitive , restrictedTo , Dom(..) - -- * Generic + -- * Slotting + , SlotId (..) + , isValidSlotId + , slotsPerEpoch + , slotDiff + , slotIncr + , blockIsAfter + , blockIsBefore + , blockIsBetween + + -- * Polymorphic , Hash (..) , ShowFmt (..) , invariant @@ -56,8 +67,6 @@ module Cardano.Wallet.Primitive import Prelude -import Cardano.Wallet.Slotting - ( EpochIndex, LocalSlotIndex ) import Control.DeepSeq ( NFData (..) ) import Data.ByteArray.Encoding @@ -71,7 +80,7 @@ import Data.Map.Strict import Data.Set ( Set ) import Data.Word - ( Word32, Word64 ) + ( Word16, Word32, Word64 ) import Fmt ( Buildable (..) , blockListF @@ -103,16 +112,15 @@ data Block = Block instance NFData Block data BlockHeader = BlockHeader - { epochIndex - :: !EpochIndex - , slotNumber - :: !LocalSlotIndex + { slotId + :: SlotId , prevBlockHash :: !(Hash "BlockHeader") } deriving (Show, Eq, Ord, Generic) instance NFData BlockHeader + -- * Tx data Tx = Tx @@ -283,6 +291,68 @@ restrictedTo (UTxO utxo) outs = UTxO $ Map.filter (`Set.member` outs) utxo +-- * Slotting + +-- | Hard-coded for the time being +slotsPerEpoch :: Word64 +slotsPerEpoch = 21600 + +-- | A slot identifier is the combination of an epoch and slot. +data SlotId = SlotId + { epochIndex :: !Word64 + , slotNumber :: !Word16 + } deriving stock (Show, Eq, Ord, Generic) + +instance NFData SlotId + +instance Enum SlotId where + toEnum i + | i < 0 = error "SlotId.toEnum: bad argument" + | otherwise = slotIncr (fromIntegral i) (SlotId 0 0) + fromEnum (SlotId e s) + | n > fromIntegral (maxBound @Int) = + error "SlotId.fromEnum: arithmetic overflow" + | otherwise = fromIntegral n + where + n :: Word64 + n = fromIntegral e * fromIntegral slotsPerEpoch + fromIntegral s + +-- | Add a number of slots to an (Epoch, LocalSlotIndex) pair, where the number +-- of slots can be greater than one epoch. +slotIncr :: Word64 -> SlotId -> SlotId +slotIncr n slot = SlotId e s + where + e = fromIntegral (fromIntegral n' `div` slotsPerEpoch) + s = fromIntegral (fromIntegral n' `mod` slotsPerEpoch) + n' = n + fromIntegral (fromEnum slot) + +-- | @slotDiff a b@ is the number of slots by which @a@ is greater than @b@. +slotDiff :: SlotId -> SlotId -> Integer +slotDiff s1 s2 = fromIntegral (fromEnum s1 - fromEnum s2) + +-- | Whether the epoch index and slot number are in range. +isValidSlotId :: SlotId -> Bool +isValidSlotId (SlotId e s) = + e >= 0 && s >= 0 && s < fromIntegral slotsPerEpoch + +-- | Predicate returns true iff the block is from the given slot or a later one. +blockIsSameOrAfter :: SlotId -> Block -> Bool +blockIsSameOrAfter s = (>= s) . slotId . header + +-- | Predicate returns true iff the block is after then given slot +blockIsAfter :: SlotId -> Block -> Bool +blockIsAfter s = (> s) . slotId . header + +-- | Predicate returns true iff the block is before the given slot. +blockIsBefore :: SlotId -> Block -> Bool +blockIsBefore s = (< s) . slotId . header + +-- | @blockIsBetween start end@ Returns true if the block is in within the +-- interval @[start, end)@. +blockIsBetween :: SlotId -> SlotId -> Block -> Bool +blockIsBetween start end b = blockIsSameOrAfter start b && blockIsBefore end b + + -- * Polymorphic class Dom a where diff --git a/src/Cardano/Wallet/Slotting.hs b/src/Cardano/Wallet/Slotting.hs deleted file mode 100644 index 4ad9097c8cf..00000000000 --- a/src/Cardano/Wallet/Slotting.hs +++ /dev/null @@ -1,92 +0,0 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - --- | --- Copyright: © 2018-2019 IOHK --- License: MIT - -module Cardano.Wallet.Slotting - ( SlotId (..) - , EpochIndex (..) - , LocalSlotIndex (..) - , slotsPerEpoch - , addSlots - , slotDiff - , slotNext - , slotPrev - , isValidSlotId - ) where - -import Prelude - -import Control.DeepSeq - ( NFData (..) ) -import Data.Word - ( Word16, Word64 ) -import GHC.Generics - ( Generic ) -import Numeric.Natural - ( Natural ) - --- | Denotes a chain epoch, which contains a certain number of slots. -newtype EpochIndex = EpochIndex - { getEpochIndex :: Word64 } - deriving stock (Show, Eq, Ord, Generic) - deriving newtype (Num, Enum, NFData) - --- | Denotes the slot number within an epoch. -newtype LocalSlotIndex = LocalSlotIndex - { getLocalSlotIndex :: Word16 } - deriving stock (Show, Eq, Ord, Generic) - deriving newtype (Num, Enum, NFData) - --- | A slot identifier is the combination of an epoch and slot. -data SlotId = SlotId - { epochIndex :: !EpochIndex - , slotNumber :: !LocalSlotIndex - } deriving stock (Show, Eq, Ord, Generic) - --- | Hard-coded for the time being -slotsPerEpoch :: Natural -slotsPerEpoch = 21600 - -instance Bounded LocalSlotIndex where - minBound = LocalSlotIndex 0 - maxBound = LocalSlotIndex (fromIntegral slotsPerEpoch - 1) - --- | Add a number of slots to an (Epoch, LocalSlotIndex) pair, where the number --- of slots can be greater than one epoch. -addSlots :: Natural -> SlotId -> SlotId -addSlots n (SlotId (EpochIndex e) (LocalSlotIndex sl)) - = SlotId (EpochIndex (e + fromIntegral e')) - (LocalSlotIndex (fromIntegral sl')) - where - e' = n' `div` slotsPerEpoch - sl' = n' `mod` slotsPerEpoch - n' = fromIntegral sl + n - --- | @slotDiff a b@ is the number of slots by which @a@ is greater than @b@. -slotDiff :: SlotId -> SlotId -> Integer -slotDiff s1 s2 = flatten s1 - flatten s2 - where flatten = fromIntegral . flattenSlotId - --- | Convert SlotId into number of slots since genesis. -flattenSlotId :: SlotId -> Natural -flattenSlotId (SlotId (EpochIndex e) (LocalSlotIndex sl)) - = fromIntegral e * slotsPerEpoch + fromIntegral sl - --- | The slot after. -slotNext :: SlotId -> SlotId -slotNext = addSlots 1 - --- | The slot before, if there is one. -slotPrev :: SlotId -> Maybe SlotId -slotPrev (SlotId 0 0) = Nothing -slotPrev (SlotId ep 0) = Just $ SlotId (ep - 1) (fromIntegral slotsPerEpoch - 1) -slotPrev (SlotId ep sl) = Just $ SlotId ep (sl - 1) - --- | Whether the epoch index and slot number are in range. -isValidSlotId :: SlotId -> Bool -isValidSlotId (SlotId ep (LocalSlotIndex sl)) = ep >= 0 && sl >= 0 - && sl < fromIntegral slotsPerEpoch diff --git a/test/unit/Cardano/ChainProducer/RustHttpBridge/MockNetworkLayer.hs b/test/unit/Cardano/ChainProducer/RustHttpBridge/MockNetworkLayer.hs deleted file mode 100644 index 8813f0ebac1..00000000000 --- a/test/unit/Cardano/ChainProducer/RustHttpBridge/MockNetworkLayer.hs +++ /dev/null @@ -1,75 +0,0 @@ -module Cardano.ChainProducer.RustHttpBridge.MockNetworkLayer - ( mockNetworkLayer - ) where - -import Prelude - -import Cardano.ChainProducer.RustHttpBridge.NetworkLayer - ( NetworkLayer (..), NetworkLayerError (..) ) -import Cardano.Wallet.Primitive - ( Block (..), BlockHeader (..), Hash (..) ) -import Cardano.Wallet.Slotting - ( EpochIndex (..) - , LocalSlotIndex (..) - , SlotId (..) - , slotPrev - , slotsPerEpoch - ) -import Control.Monad.Catch - ( MonadThrow (..) ) -import Control.Monad.Except - ( throwError ) - -import qualified Data.ByteString.Char8 as B8 - - --- | Embed an epoch index and slot number into a hash. -mockHash :: SlotId -> Hash a -mockHash (SlotId (EpochIndex ep) (LocalSlotIndex sl)) - = Hash $ B8.pack ("Hash " <> show ep <> "." <> show sl) - --- | Extract the epoch index and slot number from a hash. -unMockHash :: Hash a -> SlotId -unMockHash (Hash h) = parse . map B8.unpack . B8.split '.' . B8.drop 5 $ h - where - parse [ep, sl] = - SlotId (EpochIndex (read ep)) (LocalSlotIndex (read sl)) - parse _ = error $ "Could not read mock hash: " ++ B8.unpack h - --- | Create a block header from its hash, assuming that the hash was created --- with 'mockHash'. -mockHeaderFromHash :: Hash a -> BlockHeader -mockHeaderFromHash h = BlockHeader ep sl prevHash - where - slotId@(SlotId ep sl) = unMockHash h - prevHash = maybe (Hash "?") mockHash (slotPrev slotId) - --- | Generate an entire epoch's worth of mock blocks. There are no transactions --- generated. -mockEpoch :: EpochIndex -> [Block] -mockEpoch ep = [ Block (mockHeaderFromHash (mockHash sl)) mempty - | sl <- [ SlotId ep i | i <- epochs ] ] - where epochs = [ 0 .. fromIntegral (slotsPerEpoch - 1) ] - --- | A network layer which returns mock blocks. -mockNetworkLayer - :: MonadThrow m - => EpochIndex -- ^ make getEpoch fail for epochs after this - -> SlotId -- ^ the tip block - -> NetworkLayer m -mockNetworkLayer firstUnstableEpoch tip = NetworkLayer - { getBlock = \hash -> do - -- putStrLn $ "mock getBlock " ++ show hash - pure $ Block (mockHeaderFromHash hash) mempty - , getEpoch = \ep -> do - -- putStrLn $ "mock getEpoch " ++ show ep - if ep < firstUnstableEpoch - then pure $ mockEpoch ep - else throwError $ NetworkLayerError - $ "mock epoch " ++ show ep ++ " > firstUnstableEpoch " - ++ show firstUnstableEpoch - , getNetworkTip = do - -- putStrLn $ "mock getNetworkTip" - let hash = mockHash tip - pure (hash, mockHeaderFromHash hash) - } diff --git a/test/unit/Cardano/ChainProducer/RustHttpBridgeSpec.hs b/test/unit/Cardano/ChainProducer/RustHttpBridgeSpec.hs deleted file mode 100644 index 5c94a1355d9..00000000000 --- a/test/unit/Cardano/ChainProducer/RustHttpBridgeSpec.hs +++ /dev/null @@ -1,65 +0,0 @@ -module Cardano.ChainProducer.RustHttpBridgeSpec (spec) where - -import Prelude - -import Cardano.ChainProducer - ( nextBlocks ) -import Cardano.ChainProducer.RustHttpBridge - ( RustBackend, runRustBackend ) -import Cardano.ChainProducer.RustHttpBridge.MockNetworkLayer - ( mockNetworkLayer ) -import Cardano.ChainProducer.RustHttpBridge.NetworkLayer - ( NetworkLayer ) -import Cardano.Wallet.Primitive - ( BlockHeader (..), header ) -import Cardano.Wallet.Slotting - ( SlotId (SlotId) ) -import Control.Exception - ( Exception, throwIO ) -import Control.Monad - ( (<=<) ) -import Control.Monad.Except - ( ExceptT, runExceptT ) -import Control.Monad.IO.Class - ( MonadIO, liftIO ) -import Test.Hspec - ( Spec, SpecWith, beforeAll, describe, it, shouldBe, shouldSatisfy ) - -spec :: Spec -spec = do - describe "Getting next blocks with a mock backend" $ do - beforeAll (pure $ mockNetworkLayer 105 (SlotId 106 1492)) $ do - getNextBlocksSpec - -getNextBlocksSpec :: SpecWith (NetworkLayer IO) -getNextBlocksSpec = do - it "should get something from the latest epoch" $ \network -> do - blocks <- runBackend network $ nextBlocks 1000 (SlotId 106 1000) - -- the number of blocks between slots 1000 and 1492 inclusive - length blocks `shouldBe` 493 - let hdrs = map header blocks - map slotNumber hdrs `shouldBe` [1000 .. 1492] - map epochIndex hdrs `shouldSatisfy` all (== 106) - - it "should get something from an unstable epoch" $ \network -> do - blocks <- runBackend network $ nextBlocks 1000 (SlotId 105 17000) - length blocks `shouldBe` 1000 - - it "should get from old epochs" $ \network -> do - blocks <- runBackend network $ nextBlocks 1000 (SlotId 104 10000) - length blocks `shouldBe` 1000 - map (epochIndex . header) blocks `shouldSatisfy` all (== 104) - - it "should produce no blocks if start slot is after tip" $ \network -> do - blocks <- runBackend network $ nextBlocks 1000 (SlotId 107 0) - blocks `shouldBe` [] - - it "should work for zero blocks" $ \network -> do - blocks <- runBackend network $ nextBlocks 0 (SlotId 106 1000) - blocks `shouldBe` [] - -unsafeRunExceptT :: (Exception e, MonadIO m) => ExceptT e m a -> m a -unsafeRunExceptT = either (liftIO . throwIO) pure <=< runExceptT - -runBackend :: Exception e => NetworkLayer IO -> ExceptT e RustBackend a -> IO a -runBackend network = runRustBackend network . unsafeRunExceptT diff --git a/test/unit/Cardano/NetworkLayer/HttpBridge/ApiSpec.hs b/test/unit/Cardano/NetworkLayer/HttpBridge/ApiSpec.hs new file mode 100644 index 00000000000..030b0906cb3 --- /dev/null +++ b/test/unit/Cardano/NetworkLayer/HttpBridge/ApiSpec.hs @@ -0,0 +1,12 @@ +module Cardano.NetworkLayer.HttpBridge.ApiSpec + ( spec + ) where + +import Cardano.NetworkLayer.HttpBridge.Api + () +import Prelude +import Test.Hspec + ( Spec ) + +spec :: Spec +spec = return () diff --git a/test/unit/Cardano/NetworkLayer/HttpBridgeSpec.hs b/test/unit/Cardano/NetworkLayer/HttpBridgeSpec.hs new file mode 100644 index 00000000000..64dd81bc496 --- /dev/null +++ b/test/unit/Cardano/NetworkLayer/HttpBridgeSpec.hs @@ -0,0 +1,135 @@ +module Cardano.NetworkLayer.HttpBridgeSpec + ( spec + ) where + +import Prelude + +import Cardano.NetworkLayer.HttpBridge + ( HttpBridge (..) + , HttpBridgeError (..) + , RustBackend + , nextBlocks + , runRustBackend + ) +import Cardano.Wallet.Primitive + ( Block (..), BlockHeader (..), Hash (..), SlotId (..), slotsPerEpoch ) +import Control.Exception + ( Exception, throwIO ) +import Control.Monad + ( (<=<) ) +import Control.Monad.Catch + ( MonadThrow (..) ) +import Control.Monad.Except + ( ExceptT, runExceptT, throwError ) +import Control.Monad.IO.Class + ( MonadIO, liftIO ) +import Data.Word + ( Word64 ) +import Test.Hspec + ( Spec, SpecWith, beforeAll, describe, it, shouldBe, shouldSatisfy ) + +import qualified Data.ByteString.Char8 as B8 + + +spec :: Spec +spec = do + describe "Getting next blocks with a mock backend" $ do + beforeAll (pure $ mockHttpBridge 105 (SlotId 106 1492)) $ do + getNextBlocksSpec + +getNextBlocksSpec :: SpecWith (HttpBridge IO) +getNextBlocksSpec = do + it "should get something from the latest epoch" $ \network -> do + blocks <- runBackend network $ nextBlocks 1000 (SlotId 106 1000) + -- the number of blocks between slots 1000 and 1492 inclusive + length blocks `shouldBe` 493 + let hdrs = map (slotId . header) blocks + map slotNumber hdrs `shouldBe` [1000 .. 1492] + map epochIndex hdrs `shouldSatisfy` all (== 106) + + it "should get something from an unstable epoch" $ \network -> do + blocks <- runBackend network $ nextBlocks 1000 (SlotId 105 17000) + length blocks `shouldBe` 1000 + + it "should get from old epochs" $ \network -> do + blocks <- runBackend network $ nextBlocks 1000 (SlotId 104 10000) + length blocks `shouldBe` 1000 + map (epochIndex . slotId . header) blocks `shouldSatisfy` all (== 104) + + it "should produce no blocks if start slot is after tip" $ \network -> do + blocks <- runBackend network $ nextBlocks 1000 (SlotId 107 0) + blocks `shouldBe` [] + + it "should work for zero blocks" $ \network -> do + blocks <- runBackend network $ nextBlocks 0 (SlotId 106 1000) + blocks `shouldBe` [] + +unsafeRunExceptT :: (Exception e, MonadIO m) => ExceptT e m a -> m a +unsafeRunExceptT = either (liftIO . throwIO) pure <=< runExceptT + +runBackend :: Exception e => HttpBridge IO -> ExceptT e RustBackend a -> IO a +runBackend network = runRustBackend network . unsafeRunExceptT + + +{------------------------------------------------------------------------------- + Mock HTTP Bridge +-------------------------------------------------------------------------------} + +-- | Embed an epoch index and slot number into a hash. +mockHash :: SlotId -> Hash a +mockHash (SlotId ep sl) = + Hash $ B8.pack ("Hash " <> show ep <> "." <> show sl) + +-- | Extract the epoch index and slot number from a hash. +unMockHash :: Hash a -> SlotId +unMockHash (Hash h) = parse . map B8.unpack . B8.split '.' . B8.drop 5 $ h + where + parse [ep, sl] = SlotId (read ep) (read sl) + parse _ = error $ "Could not read mock hash: " ++ B8.unpack h + +-- | Create a block header from its hash, assuming that the hash was created +-- with 'mockHash'. +mockHeaderFromHash :: Hash a -> BlockHeader +mockHeaderFromHash h = BlockHeader slot prevHash + where + slot = unMockHash h + prevHash = + if slot == SlotId 0 0 then + Hash "genesis" + else + mockHash (pred slot) + +-- | Generate an entire epoch's worth of mock blocks. There are no transactions +-- generated. +mockEpoch :: Word64 -> [Block] +mockEpoch ep = + [ Block (mockHeaderFromHash (mockHash sl)) mempty + | sl <- [ SlotId ep i | i <- epochs ] + ] + where + epochs = [ 0 .. fromIntegral (slotsPerEpoch - 1) ] + +-- | A network layer which returns mock blocks. +mockHttpBridge + :: MonadThrow m + => Word64 -- ^ make getEpoch fail for epochs after this + -> SlotId -- ^ the tip block + -> HttpBridge m +mockHttpBridge firstUnstableEpoch tip = HttpBridge + { getBlock = \hash -> do + -- putStrLn $ "mock getBlock " ++ show hash + pure $ Block (mockHeaderFromHash hash) mempty + , getEpoch = \ep -> do + -- putStrLn $ "mock getEpoch " ++ show ep + if ep < firstUnstableEpoch then + pure $ mockEpoch ep + else + throwError $ HttpBridgeError $ + "mock epoch " ++ show ep ++ " > firstUnstableEpoch " ++ + show firstUnstableEpoch + + , getNetworkTip = do + -- putStrLn $ "mock getNetworkTip" + let hash = mockHash tip + pure (hash, mockHeaderFromHash hash) + } diff --git a/test/unit/Cardano/NetworkLayerSpec.hs b/test/unit/Cardano/NetworkLayerSpec.hs new file mode 100644 index 00000000000..16af9aec9ae --- /dev/null +++ b/test/unit/Cardano/NetworkLayerSpec.hs @@ -0,0 +1,14 @@ +module Cardano.NetworkLayerSpec + ( spec + ) where + +import Prelude + +import Cardano.NetworkLayer + () +import Test.Hspec + ( Spec ) + + +spec :: Spec +spec = return () diff --git a/test/unit/Cardano/Wallet/Binary/PackfileSpec.hs b/test/unit/Cardano/Wallet/Binary/PackfileSpec.hs index 30a880d1b8c..cf2cdd507c7 100644 --- a/test/unit/Cardano/Wallet/Binary/PackfileSpec.hs +++ b/test/unit/Cardano/Wallet/Binary/PackfileSpec.hs @@ -9,7 +9,7 @@ import Cardano.Wallet.Binary.Packfile import Cardano.Wallet.BinarySpec ( unsafeDeserialiseFromBytes ) import Cardano.Wallet.Primitive - ( Block (..), BlockHeader (..) ) + ( Block (..), BlockHeader (..), SlotId (..) ) import Data.Either ( fromRight, isRight ) import Test.Hspec @@ -85,7 +85,8 @@ spec = do it "should decode correct blocks" $ do bs <- L8.readFile testPackfile - let (ebb:first:second:_) = map header $ unsafeDeserialiseEpoch bs + let (ebb:first:second:_) = + map (slotId . header) $ unsafeDeserialiseEpoch bs epochIndex ebb `shouldBe` 104 epochIndex first `shouldBe` 104 slotNumber ebb `shouldBe` 0 -- epoch genesis block diff --git a/test/unit/Cardano/Wallet/BinarySpec.hs b/test/unit/Cardano/Wallet/BinarySpec.hs index f4f7b6174ce..63ba20b44e7 100644 --- a/test/unit/Cardano/Wallet/BinarySpec.hs +++ b/test/unit/Cardano/Wallet/BinarySpec.hs @@ -17,6 +17,7 @@ import Cardano.Wallet.Primitive , BlockHeader (..) , Coin (..) , Hash (..) + , SlotId (..) , Tx (..) , TxIn (..) , TxOut (..) @@ -111,8 +112,7 @@ spec = do -- A mainnet block header blockHeader1 :: BlockHeader blockHeader1 = BlockHeader - { epochIndex = 105 - , slotNumber = 9520 + { slotId = SlotId 105 9520 , prevBlockHash = hash16 "9f3c67b575bf2c5638291949694849d6ce5d29efa1f2eb3ed0beb6dac262e9e0" } @@ -120,8 +120,7 @@ blockHeader1 = BlockHeader block1 :: Block block1 = Block { header = BlockHeader - { epochIndex = 105 - , slotNumber = 9519 + { slotId = SlotId 105 9519 , prevBlockHash = prevBlockHash0 } , transactions = mempty @@ -134,8 +133,7 @@ block1 = Block block2 :: Block block2 = Block { header = BlockHeader - { epochIndex = 105 - , slotNumber = 9876 + { slotId = SlotId 105 9876 , prevBlockHash = prevBlockHash0 } , transactions = Set.fromList @@ -164,8 +162,7 @@ block2 = Block block3 :: Block block3 = Block { header = BlockHeader - { epochIndex = 30 - , slotNumber = 9278 + { slotId = SlotId 30 9278 , prevBlockHash = prevBlockHash0 } , transactions = Set.fromList @@ -197,8 +194,7 @@ block3 = Block block4 :: Block block4 = Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 18 + { slotId = SlotId 14 18 , prevBlockHash = prevBlockHash0 } , transactions = Set.fromList diff --git a/test/unit/Cardano/Wallet/BlockSyncerSpec.hs b/test/unit/Cardano/Wallet/BlockSyncerSpec.hs index 116dbf022d6..422defd5239 100644 --- a/test/unit/Cardano/Wallet/BlockSyncerSpec.hs +++ b/test/unit/Cardano/Wallet/BlockSyncerSpec.hs @@ -14,9 +14,7 @@ import Prelude import Cardano.Wallet.BlockSyncer ( BlockHeadersConsumed (..), tickingFunction ) import Cardano.Wallet.Primitive - ( Block (..), BlockHeader (..), Hash (..) ) -import Cardano.Wallet.Slotting - ( EpochIndex (..), LocalSlotIndex (..) ) + ( Block (..), BlockHeader (..), Hash (..), SlotId (..) ) import Control.Concurrent ( forkIO, killThread ) import Control.Concurrent.MVar @@ -38,8 +36,7 @@ import Test.QuickCheck import Test.QuickCheck.Monadic ( monadicIO ) -import qualified Codec.CBOR.Encoding as CBOR -import qualified Codec.CBOR.Write as CBOR +import qualified Data.ByteString.Char8 as B8 import qualified Data.List as L @@ -120,7 +117,7 @@ instance Arbitrary Blocks where -- No Shrinking arbitrary = do n <- fromIntegral . (`mod` 42) <$> arbitrary @Word8 - let h0 = BlockHeader 1 0 (Hash "initial block") + let h0 = BlockHeader (SlotId 1 0) (Hash "initial block") let blocks = map snd $ take n $ iterate next ( blockHeaderHash h0 , Block h0 mempty @@ -130,22 +127,14 @@ instance Arbitrary Blocks where next :: (Hash "BlockHeader", Block) -> (Hash "BlockHeader", Block) next (prev, b) = let - epoch = epochIndex (header b) - slot = slotNumber (header b) + 1 - h = BlockHeader epoch slot prev + slot = slotId (header b) + h = BlockHeader (succ slot) prev in (blockHeaderHash h, Block h mempty) blockHeaderHash :: BlockHeader -> Hash "BlockHeader" - blockHeaderHash = - Hash . CBOR.toStrictByteString . encodeBlockHeader - where - encodeBlockHeader (BlockHeader epoch slot prev) = mempty - <> CBOR.encodeListLen 3 - <> CBOR.encodeWord64 (getEpochIndex epoch) - <> CBOR.encodeWord16 (getLocalSlotIndex slot) - <> CBOR.encodeBytes (getHash prev) - + blockHeaderHash (BlockHeader (SlotId e s) _) = + Hash (B8.pack (show e <> show s)) -- | Construct arbitrary groups of elements from a given list. -- diff --git a/test/unit/Cardano/Wallet/PrimitiveSpec.hs b/test/unit/Cardano/Wallet/PrimitiveSpec.hs index 2449c6c9ee8..da266acb1b7 100644 --- a/test/unit/Cardano/Wallet/PrimitiveSpec.hs +++ b/test/unit/Cardano/Wallet/PrimitiveSpec.hs @@ -15,6 +15,7 @@ import Cardano.Wallet.Primitive , Coin (..) , Dom (..) , Hash (..) + , SlotId (..) , Tx (..) , TxIn (..) , TxOut (..) @@ -23,14 +24,18 @@ import Cardano.Wallet.Primitive , excluding , isSubsetOf , isValidCoin + , isValidSlotId , restrictedBy , restrictedTo + , slotDiff + , slotIncr + , slotsPerEpoch , updatePending ) -import Cardano.Wallet.SlottingSpec - () import Data.Set ( Set, (\\) ) +import Data.Word + ( Word64 ) import Test.Hspec ( Spec, describe, it ) import Test.QuickCheck @@ -39,11 +44,13 @@ import Test.QuickCheck , checkCoverage , choose , cover + , expectFailure , oneof , property , scale , vectorOf , (===) + , (==>) ) import qualified Data.Map.Strict as Map @@ -54,6 +61,7 @@ spec :: Spec spec = do describe "Generators are valid" $ do it "Arbitrary Coin" $ property isValidCoin + it "Arbitrary SlotId" $ property isValidSlotId describe "Lemma 2.1 - Properties of UTxO operations" $ do it "2.1.1) ins⊲ u ⊆ u" @@ -85,6 +93,24 @@ spec = do it "3.3) updatePending b pending ⊆ pending" (checkCoverage prop_3_2) + describe "Basic slot arithmetic" $ do + let maxSlot = toEnum maxBound :: SlotId + it "succ . pred = id" + (property propNextSlotPrevSlot) + it "succ always increments the SlotId" + (property propNextIncrements) + it ("succ on max slot (" <> show maxSlot <>") throws at runtime") + (expectFailure prop_succSlotMaxIntFails) + it "pred decrements the SlotId" + (property propPrevDecrements) + it "pred on initial slot throws at runtime" + (expectFailure prop_predSlot0Fails) + it "slotDiff results in correct difference" + (property propAddSlotsDiff) + it "slotIncr 0 == id" + (property propAddSlotsId) + + {------------------------------------------------------------------------------- Wallet Specification - Lemma 2.1 - Properties of UTxO operations @@ -212,6 +238,36 @@ prop_3_2 (b, pending) = prop = updatePending b pending `Set.isSubsetOf` pending +{------------------------------------------------------------------------------- + Basic Slot Arithmetic +-------------------------------------------------------------------------------} + +propNextSlotPrevSlot :: SlotId -> Property +propNextSlotPrevSlot sl = pred (succ sl) === sl + +propNextIncrements :: SlotId -> Property +propNextIncrements sl = slotDiff (succ sl) sl === 1 + +propPrevDecrements :: SlotId -> Property +propPrevDecrements sl = + sl > SlotId 0 0 ==> slotDiff sl (pred sl) === 1 + +prop_predSlot0Fails :: Property +prop_predSlot0Fails = + property $ pred (SlotId 0 0) `seq` () + +prop_succSlotMaxIntFails :: Property +prop_succSlotMaxIntFails = + property $ succ (toEnum (maxBound :: Int) :: SlotId) `seq` () + +propAddSlotsDiff :: (NumberOfSlots, SlotId) -> Property +propAddSlotsDiff (NumberOfSlots n, sl) = + slotDiff (slotIncr n sl) sl === fromIntegral n + +propAddSlotsId :: SlotId -> Property +propAddSlotsId sl = slotIncr 0 sl === sl + + {------------------------------------------------------------------------------- Arbitrary Instances @@ -275,13 +331,24 @@ instance Arbitrary BlockHeader where -- No Shrinking arbitrary = BlockHeader <$> arbitrary - <*> arbitrary <*> oneof [ pure $ Hash "BLOCK01" , pure $ Hash "BLOCK02" , pure $ Hash "BLOCK03" ] +instance Arbitrary SlotId where + shrink _ = [] + arbitrary = toEnum <$> choose (0, maxBound `div` 2) + +newtype NumberOfSlots = NumberOfSlots Word64 deriving Show + +instance Arbitrary NumberOfSlots where + shrink (NumberOfSlots 0) = [] + shrink (NumberOfSlots n) = NumberOfSlots <$> [0, n `div` 2, n - 1] + arbitrary = NumberOfSlots . fromIntegral + <$> choose (0 :: Int, 4 * (fromIntegral slotsPerEpoch)) + instance Arbitrary Block where shrink (Block h txs) = Block h <$> shrink txs arbitrary = do diff --git a/test/unit/Cardano/Wallet/SlottingSpec.hs b/test/unit/Cardano/Wallet/SlottingSpec.hs deleted file mode 100644 index 555ad947296..00000000000 --- a/test/unit/Cardano/Wallet/SlottingSpec.hs +++ /dev/null @@ -1,84 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE FlexibleInstances #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} - -module Cardano.Wallet.SlottingSpec - ( spec - ) where - -import Prelude - -import Cardano.Wallet.Slotting - ( EpochIndex (..) - , LocalSlotIndex (..) - , SlotId (..) - , addSlots - , isValidSlotId - , slotDiff - , slotNext - , slotPrev - , slotsPerEpoch - ) -import Numeric.Natural - ( Natural ) -import Test.Hspec - ( Spec, describe, it ) -import Test.QuickCheck - ( Arbitrary (..), Property, choose, property, (===), (==>) ) - -spec :: Spec -spec = do - describe "Generators are valid" $ do - it "Arbitrary SlotId" $ property isValidSlotId - - describe "Basic slot arithmetic" $ do - it "slotNext . slotPrev = id" - (property propNextSlotPrevSlot) - - it "slotNext always increments the SlotId" - (property propNextIncrements) - - it "slotPrev decrements the SlotId" - (property propPrevDecrements) - - it "addSlots results in correct difference" - (property propAddSlotsDiff) - - it "addSlots 0 == id" - (property propAddSlotsId) - -propNextSlotPrevSlot :: SlotId -> Property -propNextSlotPrevSlot sl = slotPrev (slotNext sl) === Just sl - -propNextIncrements :: SlotId -> Property -propNextIncrements sl = slotDiff (slotNext sl) sl === 1 - -propPrevDecrements :: SlotId -> Property -propPrevDecrements sl = - sl > SlotId 0 0 ==> (slotDiff sl <$> slotPrev sl) === Just 1 - -propAddSlotsDiff :: (Natural, SlotId) -> Property -propAddSlotsDiff (n, sl) = - slotDiff (addSlots n sl) sl === fromIntegral n - -propAddSlotsId :: SlotId -> Property -propAddSlotsId sl = addSlots 0 sl === sl - - -{------------------------------------------------------------------------------- - Arbitrary Instances --------------------------------------------------------------------------------} - -instance Arbitrary SlotId where - arbitrary = SlotId <$> arbitrary <*> arbitrary - -instance Arbitrary EpochIndex where - arbitrary = EpochIndex <$> arbitrary - -instance Arbitrary LocalSlotIndex where - arbitrary = - LocalSlotIndex <$> choose (0, fromIntegral slotsPerEpoch - 1) - -instance Arbitrary Natural where - arbitrary = fromIntegral - <$> choose (0 :: Int, 4 * (fromIntegral slotsPerEpoch)) diff --git a/test/unit/Cardano/WalletSpec.hs b/test/unit/Cardano/WalletSpec.hs index 36ea87e2e47..e1f6eef1efd 100644 --- a/test/unit/Cardano/WalletSpec.hs +++ b/test/unit/Cardano/WalletSpec.hs @@ -27,6 +27,7 @@ import Cardano.Wallet.Primitive , Hash (..) , IsOurs (..) , ShowFmt (..) + , SlotId (..) , Tx (..) , TxIn (..) , TxOut (..) @@ -227,16 +228,14 @@ blockchain :: [Block] blockchain = [ Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 0 + { slotId = SlotId 14 0 , prevBlockHash = Hash "39d89a1e837e968ba35370be47cdfcbfd193cd992fdeed557b77c49b77ee59cf" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 1 + { slotId = SlotId 14 1 , prevBlockHash = Hash "2d04732b41d07e45a2b87c05888f956805f94b108f59e1ff3177860a17c292db" } , transactions = Set.fromList @@ -262,8 +261,7 @@ blockchain = } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 2 + { slotId = SlotId 14 2 , prevBlockHash = Hash "e95a6e7da3cd61e923e30b1998b135d40958419e4157a9f05d2f0f194e4d7bba" } , transactions = Set.fromList @@ -289,8 +287,7 @@ blockchain = } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 3 + { slotId = SlotId 14 3 , prevBlockHash = Hash "b5d970285a2f8534e94119cd631888c20b3a4ec0707a821f6df5c96650fe01dd" } , transactions = Set.fromList @@ -316,16 +313,14 @@ blockchain = } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 4 + { slotId = SlotId 14 4 , prevBlockHash = Hash "cb96ff923728a67e52dfad54df01fc5a20c7aaf386226a0564a1185af9798cb1" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 5 + { slotId = SlotId 14 5 , prevBlockHash = Hash "63040af5ed7eb2948e2c09a43f946c91d5dd2efaa168bbc5c4f3e989cfc337e6" } , transactions = Set.fromList @@ -355,40 +350,35 @@ blockchain = } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 6 + { slotId = SlotId 14 6 , prevBlockHash = Hash "1a32e01995225c7cd514e0fe5087f19a6fd597a6071ad4ad1fbf5b20de39670b" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 7 + { slotId = SlotId 14 7 , prevBlockHash = Hash "7855c0f101b6761b234058e7e9fd19fbed9fee90a202cca899da1f6cbf29518d" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 8 + { slotId = SlotId 14 8 , prevBlockHash = Hash "9007e0513b9fea848034a7203b380cdbbba685073bcfb7d8bb795130d92e7be8" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 9 + { slotId = SlotId 14 9 , prevBlockHash = Hash "0af8082504f59eb1b7114981b7dee9009064638420382211118730b45ad385ae" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 10 + { slotId = SlotId 14 10 , prevBlockHash = Hash "adc8c71d2c85cee39fbb34cdec6deca2a4d8ce6493d6d28f542d891d5504fc38" } , transactions = Set.fromList @@ -432,8 +422,7 @@ blockchain = } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 11 + { slotId = SlotId 14 11 , prevBlockHash = Hash "4fdff9f1d751dba5a48bc2a14d6dfb21709882a13dad495b856bf76d5adf4bd1" } , transactions = Set.fromList @@ -477,56 +466,49 @@ blockchain = } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 12 + { slotId = SlotId 14 12 , prevBlockHash = Hash "96a31a7cdb410aeb5756ddb43ee2ddb4c682f6308db38310ab54bf38b89d6b0d" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 13 + { slotId = SlotId 14 13 , prevBlockHash = Hash "47c08c0a11f66aeab915e5cd19362e8da50dc2523e629b230b73ec7b6cdbeef8" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 14 + { slotId = SlotId 14 14 , prevBlockHash = Hash "d6d7e79e2a25f53e6fb771eebd1be05274861004dc62c03bf94df03ff7b87198" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 15 + { slotId = SlotId 14 15 , prevBlockHash = Hash "647e62b29ebcb0ecfa0b4deb4152913d1a669611d646072d2f5898835b88d938" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 16 + { slotId = SlotId 14 16 , prevBlockHash = Hash "02f38ce50c9499f2526dd9c5f9e8899e65c0c40344e14ff01dc6c31137978efb" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 17 + { slotId = SlotId 14 17 , prevBlockHash = Hash "528492ded729ca77a72b1d85654742db85dfd3b68e6c4117ce3c253e3e86616d" } , transactions = Set.fromList [] } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 18 + { slotId = SlotId 14 18 , prevBlockHash = Hash "f4283844eb78ca6f6333b007f5a735d71499d6ce7cc816846a033a36784bd299" } , transactions = Set.fromList @@ -570,8 +552,7 @@ blockchain = } , Block { header = BlockHeader - { epochIndex = 14 - , slotNumber = 19 + { slotId = SlotId 14 19 , prevBlockHash = Hash "dffc3506d381361468376227e1c9323a2ffc76011103e3225124f08e6969a73b" } , transactions = Set.fromList