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

Light NetworkLayer: watchNodeTip/currentNodeTip #3169

Merged
merged 1 commit into from
Mar 16, 2022
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
2 changes: 1 addition & 1 deletion lib/shelley/src/Cardano/Wallet/Shelley/Network.hs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ withNetworkLayer
-> Cardano.NetworkId
-> NetworkParameters
-> SyncTolerance
-> ContT r IO (NetworkLayer IO (CardanoBlock StandardCrypto) )
-> ContT r IO (NetworkLayer IO (CardanoBlock StandardCrypto))
withNetworkLayer tr blockchainSrc net netParams tol =
ContT $ case blockchainSrc of
NodeSource nodeConn ver ->
Expand Down
98 changes: 65 additions & 33 deletions lib/shelley/src/Cardano/Wallet/Shelley/Network/Blockfrost.hs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- |
-- Copyright: © 2020 IOHK
Expand All @@ -12,66 +18,78 @@
module Cardano.Wallet.Shelley.Network.Blockfrost
( withNetworkLayer
, Log

-- * Blockfrost <-> Cardano translation
, blockToBlockHeader
) where

import Prelude

import qualified Blockfrost.Client as BF
import qualified Data.Text as T

import Cardano.BM.Data.Severity
( Severity (..) )
import Cardano.BM.Tracer
( Tracer )
import Cardano.BM.Tracing
( HasSeverityAnnotation (getSeverityAnnotation), traceWith )
( HasSeverityAnnotation (getSeverityAnnotation) )
import Cardano.Wallet.Logging
( BracketLog, bracketTracer )
import Cardano.Wallet.Network
( NetworkLayer (..) )
import Cardano.Wallet.Primitive.Types
( BlockHeader (..), SlotNo (SlotNo) )
import Cardano.Wallet.Primitive.Types.Hash
( Hash )
import Control.Concurrent
( threadDelay )
import Control.Monad
( forever )
import Control.Monad.Error.Class
( throwError )
( MonadError, throwError )
import Control.Monad.Trans.Except
( ExceptT (ExceptT), runExceptT )
import Data.Bifunctor
( first )
import Data.Functor.Contravariant
( (>$<) )
import Data.Quantity
( Quantity (..) )
import Data.Text.Class
( ToText (..) )
( FromText (fromText), TextDecodingError (..), ToText (..) )
import Data.Traversable
( for )
import Fmt
( pretty )
import Ouroboros.Consensus.Cardano.Block
( CardanoBlock, StandardCrypto )
import UnliftIO
( throwIO )
import UnliftIO.Async
( async, link )

import UnliftIO.Exception
( Exception )
data BlockfrostError
= ClientError BF.BlockfrostError
| NoSlotError BF.Block
| NoBlockHeight BF.Block
| InvalidBlockHash BF.BlockHash TextDecodingError
deriving (Show)

data Log
= MsgBlockfrostClientError BlockfrostError
| MsgWatcherUpdate BlockHeader BracketLog
newtype BlockfrostException = BlockfrostException BlockfrostError
deriving stock (Show)
deriving anyclass (Exception)

data Log = MsgWatcherUpdate BlockHeader BracketLog

instance ToText Log where
toText = \case
MsgBlockfrostClientError e ->
"Blockfrost client error: " <> T.pack (show e)
MsgWatcherUpdate blockHeader bracketLog ->
"Update watcher with tip: " <> pretty blockHeader <>
". Callback " <> toText bracketLog <> ". "

instance HasSeverityAnnotation Log where
getSeverityAnnotation = \case
MsgBlockfrostClientError _ -> Warning
MsgWatcherUpdate _ _ -> Info

withNetworkLayer
Expand All @@ -82,46 +100,60 @@ withNetworkLayer
withNetworkLayer tr project k = k NetworkLayer
{ chainSync = \_tr _chainFollower -> pure ()
, lightSync = Nothing
, currentNodeTip = undefined
, currentNodeTip
, currentNodeEra = undefined
, currentProtocolParameters = undefined
, currentNodeProtocolParameters = undefined
, currentSlottingParameters = undefined
, watchNodeTip = watchNodeTip
, watchNodeTip
, postTx = undefined
, stakeDistribution = undefined
, getCachedRewardAccountBalance = undefined
, fetchRewardAccountBalances = undefined
, timeInterpreter = undefined
, syncProgress = undefined
}

where
currentNodeTip :: IO BlockHeader
currentNodeTip = runExceptT fetchLatestBlockHeader >>= \case
-- TODO: use cached value while retrying
Left err -> throwIO (BlockfrostException err)
Right header -> pure header

watchNodeTip :: (BlockHeader -> IO ()) -> IO ()
watchNodeTip callback = link =<< async (pollNodeTip callback)
where
pollNodeTip :: (BlockHeader -> IO ()) -> IO ()
pollNodeTip cb = forever $ do
runExceptT fetchLatestBlockHeader >>= \case
Left err -> throwIO (BlockfrostException err)
Right header ->
bracketTracer (MsgWatcherUpdate header >$< tr) $ cb header
threadDelay 2_000_000

pollNodeTip :: (BlockHeader -> IO ()) -> IO ()
pollNodeTip callback = forever $ do
runExceptT fetchLatestBlockHeader >>= \case
Left err -> traceWith tr (MsgBlockfrostClientError err)
Right header ->
bracketTracer (MsgWatcherUpdate header >$< tr) $ callback header
threadDelay 2_000_000
fetchLatestBlockHeader :: ExceptT BlockfrostError IO BlockHeader
fetchLatestBlockHeader =
runBlockfrost BF.getLatestBlock >>= blockToBlockHeader

runBlockfrost :: BF.BlockfrostClientT IO a -> ExceptT BlockfrostError IO a
runBlockfrost =
ExceptT . (first ClientError <$>) . BF.runBlockfrostClientT project

fetchLatestBlockHeader :: ExceptT BlockfrostError IO BlockHeader
fetchLatestBlockHeader = do
[email protected]{..} <- runBlockfrost BF.getLatestBlock
slotNo <- case _blockSlot of
Just (BF.Slot s) -> pure $ SlotNo $ fromIntegral s
Nothing -> throwError $ NoSlotError block
pure BlockHeader
{ slotNo
, blockHeight = undefined -- Quantity "block" Word32
, headerHash = undefined -- !(Hash "BlockHeader")
, parentHeaderHash = undefined -- !(Maybe (Hash "BlockHeader"))
}
blockToBlockHeader ::
forall m. MonadError BlockfrostError m => BF.Block -> m BlockHeader
blockToBlockHeader [email protected]{..} = do
slotNo <- case _blockSlot of
Just s -> pure $ SlotNo $ fromIntegral $ BF.unSlot s
Nothing -> throwError $ NoSlotError block
blockHeight <- case _blockHeight of
Just height -> pure $ Quantity $ fromIntegral height
Nothing -> throwError $ NoBlockHeight block
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 I think the Nothing case indicates the genesis block, but I'm not entirely certain. I recently learned that in the Byron era, there exist Epoch Boundary Blocks (EBBs) which are weird. Could you check (or google) what value Blockfrost returns for the genesis block and these special blocks?

The Cardano mainnet genesis is:
https://explorer.cardano.org/en/block?id=5f20df933584822601f9e3f8c024eb5eb252fe8cefb24d1317dc3d432e940ebb

The block after genesis is one of those fabled EEBs:
https://explorer.cardano.org/en/block?id=89d9b5a5b8ddc8d7e5a6795e9774d97faf1efea59b2caf7eaf9f8c5b32059df4

headerHash <- parseBlockHeader _blockHash
parentHeaderHash <- for _blockPreviousBlock parseBlockHeader
pure BlockHeader { slotNo, blockHeight, headerHash, parentHeaderHash }
where
parseBlockHeader :: BF.BlockHash -> m (Hash "BlockHeader")
parseBlockHeader blockHash =
case fromText (BF.unBlockHash blockHash) of
Right hash -> pure hash
Left tde -> throwError $ InvalidBlockHash blockHash tde
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to use some helper function of type :: Either e a -> ExceptT m e' a here? 🤔

(That's the issue with explicit error handling: Large portions of the code become code that converts error types. 😅 )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used to use hoist-error library for these kind of tasks https://hackage.haskell.org/package/hoist-error-0.2.1.0
Ideally I'd introduce it to our codebase but its associated with additional effort of communicating and convincing team that its worth it and I don't feel like I am ready to kick-off such process 🤷🏻‍♂️

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough. 🤷🏻‍♂️ hoist-error may be a bit overkill, but I had the feeling that there could be a polymorphic function in base that does part of the job. E.g. Data.Bifunctor.first specializes to first :: (e -> e') -> Either e a -> Either e' a.

Copy link
Contributor

@paweljakubas paweljakubas Mar 16, 2022

Choose a reason for hiding this comment

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

@Unisay @HeinrichApfelmus well, we have also

mapLeft :: (a -> c) -> Either a b -> Either c b

from https://hackage.haskell.org/package/either-5.0.1.1/docs/Data-Either-Combinators.html