diff --git a/ouroboros-network-api/ouroboros-network-api.cabal b/ouroboros-network-api/ouroboros-network-api.cabal index f02155f54dd..5e0e31830db 100644 --- a/ouroboros-network-api/ouroboros-network-api.cabal +++ b/ouroboros-network-api/ouroboros-network-api.cabal @@ -67,6 +67,7 @@ library nothunks, serialise >=0.2 && <0.3, text >=1.2 && <2.2, + quiet, cardano-slotting, cardano-strict-containers, diff --git a/ouroboros-network-api/src/Ouroboros/Network/SizeInBytes.hs b/ouroboros-network-api/src/Ouroboros/Network/SizeInBytes.hs index 8c5309d689b..e92259d8de0 100644 --- a/ouroboros-network-api/src/Ouroboros/Network/SizeInBytes.hs +++ b/ouroboros-network-api/src/Ouroboros/Network/SizeInBytes.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DerivingVia #-} {-# LANGUAGE GeneralisedNewtypeDeriving #-} @@ -5,18 +6,25 @@ module Ouroboros.Network.SizeInBytes (SizeInBytes (..)) where import Control.DeepSeq (NFData (..)) +import Data.Monoid (Sum (..)) import Data.Word (Word32) +import GHC.Generics import Data.Measure qualified as Measure import NoThunks.Class (NoThunks (..)) +import Quiet (Quiet (..)) newtype SizeInBytes = SizeInBytes { getSizeInBytes :: Word32 } - deriving (Show, Eq, Ord) - deriving Enum via Word32 - deriving Num via Word32 - deriving Real via Word32 - deriving Integral via Word32 - deriving NoThunks via Word32 + deriving (Eq, Ord) + deriving Show via Quiet SizeInBytes + deriving Enum via Word32 + deriving Num via Word32 + deriving Real via Word32 + deriving Integral via Word32 + deriving NoThunks via Word32 + deriving Semigroup via Sum Word32 + deriving Monoid via Sum Word32 + deriving Generic deriving newtype NFData deriving Measure.Measure via Word32 deriving Measure.BoundedMeasure via Word32 diff --git a/ouroboros-network-framework/ouroboros-network-framework.cabal b/ouroboros-network-framework/ouroboros-network-framework.cabal index 4acf4ea0bfa..e21bb0ef662 100644 --- a/ouroboros-network-framework/ouroboros-network-framework.cabal +++ b/ouroboros-network-framework/ouroboros-network-framework.cabal @@ -91,6 +91,7 @@ library , si-timers , strict-stm + , nothunks ^>=0.2 , psqueues -- ^ only to derive nothunk instances diff --git a/ouroboros-network-protocols/CHANGELOG.md b/ouroboros-network-protocols/CHANGELOG.md index 678043d23ad..c10cef52158 100644 --- a/ouroboros-network-protocols/CHANGELOG.md +++ b/ouroboros-network-protocols/CHANGELOG.md @@ -25,6 +25,8 @@ * Refactored CBOR mini-protocols codecs to a more modular structure * Added `deepseq` dependency and implemented `NFData` for `testlib` types. * Added miniprotocols codec benchmarks +* Use `SizeInBytes` newtype instead of the `TxSizeInBytes` type aliase. + `TxSizeInBytes` is now deprecated. ## 0.8.0.0 -- 2024-02-21 diff --git a/ouroboros-network-protocols/ouroboros-network-protocols.cabal b/ouroboros-network-protocols/ouroboros-network-protocols.cabal index 98117370e93..811009c4e03 100644 --- a/ouroboros-network-protocols/ouroboros-network-protocols.cabal +++ b/ouroboros-network-protocols/ouroboros-network-protocols.cabal @@ -98,8 +98,10 @@ library bytestring >=0.10 && <0.13, cborg >=0.2.1 && <0.3, deepseq, + quiet, io-classes ^>=1.5.0, + nothunks, si-timers, ouroboros-network-api diff --git a/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Client.hs b/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Client.hs index c569d93be42..f7a6a1f2c80 100644 --- a/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Client.hs +++ b/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Client.hs @@ -27,8 +27,6 @@ module Ouroboros.Network.Protocol.TxSubmission2.Client , txSubmissionClientPeer ) where -import Data.Word (Word16) - import Network.TypedProtocol.Core import Ouroboros.Network.Protocol.TxSubmission2.Type @@ -56,8 +54,8 @@ data ClientStIdle txid tx m a = ClientStIdle { recvMsgRequestTxIds :: forall blocking. TokBlockingStyle blocking - -> Word16 - -> Word16 + -> NumTxIdsToAck + -> NumTxIdsToReq -> m (ClientStTxIds blocking txid tx m a), recvMsgRequestTxs :: [txid] @@ -65,7 +63,7 @@ data ClientStIdle txid tx m a = ClientStIdle { } data ClientStTxIds blocking txid tx m a where - SendMsgReplyTxIds :: BlockingReplyList blocking (txid, TxSizeInBytes) + SendMsgReplyTxIds :: BlockingReplyList blocking (txid, SizeInBytes) -> ClientStIdle txid tx m a -> ClientStTxIds blocking txid tx m a diff --git a/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Codec.hs b/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Codec.hs index 605e934ad30..7bfa2f0f806 100644 --- a/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Codec.hs +++ b/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Codec.hs @@ -102,7 +102,10 @@ encodeTxSubmission2 encodeTxId encodeTx = encode encode (ClientAgency TokInit) MsgInit = CBOR.encodeListLen 1 <> CBOR.encodeWord 6 - encode (ServerAgency TokIdle) (MsgRequestTxIds blocking ackNo reqNo) = + encode (ServerAgency TokIdle) (MsgRequestTxIds + blocking + (NumTxIdsToAck ackNo) + (NumTxIdsToReq reqNo)) = CBOR.encodeListLen 4 <> CBOR.encodeWord 0 <> CBOR.encodeBool (case blocking of @@ -115,14 +118,15 @@ encodeTxSubmission2 encodeTxId encodeTx = encode CBOR.encodeListLen 2 <> CBOR.encodeWord 1 <> CBOR.encodeListLenIndef - <> foldr (\(txid, sz) r -> CBOR.encodeListLen 2 + <> foldr (\(txid, SizeInBytes sz) r -> + CBOR.encodeListLen 2 <> encodeTxId txid <> CBOR.encodeWord32 sz <> r) CBOR.encodeBreak txids' where - txids' :: [(txid, TxSizeInBytes)] + txids' :: [(txid, SizeInBytes)] txids' = case txids of BlockingReply xs -> NonEmpty.toList xs NonBlockingReply xs -> xs @@ -166,11 +170,12 @@ decodeTxSubmission2 decodeTxId decodeTx = decode return (SomeMessage MsgInit) (ServerAgency TokIdle, 4, 0) -> do blocking <- CBOR.decodeBool - ackNo <- CBOR.decodeWord16 - reqNo <- CBOR.decodeWord16 - return $! case blocking of - True -> SomeMessage (MsgRequestTxIds TokBlocking ackNo reqNo) - False -> SomeMessage (MsgRequestTxIds TokNonBlocking ackNo reqNo) + ackNo <- NumTxIdsToAck <$> CBOR.decodeWord16 + reqNo <- NumTxIdsToReq <$> CBOR.decodeWord16 + return $! + if blocking + then SomeMessage (MsgRequestTxIds TokBlocking ackNo reqNo) + else SomeMessage (MsgRequestTxIds TokNonBlocking ackNo reqNo) (ClientAgency (TokTxIds b), 2, 1) -> do CBOR.decodeListLenIndef @@ -179,7 +184,7 @@ decodeTxSubmission2 decodeTxId decodeTx = decode (do CBOR.decodeListLenOf 2 txid <- decodeTxId sz <- CBOR.decodeWord32 - return (txid, sz)) + return (txid, SizeInBytes sz)) case (b, txids) of (TokBlocking, t:ts) -> return $ diff --git a/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Server.hs b/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Server.hs index bddb6398e2a..c29517ca208 100644 --- a/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Server.hs +++ b/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Server.hs @@ -17,13 +17,13 @@ module Ouroboros.Network.Protocol.TxSubmission2.Server TxSubmissionServerPipelined (..) , ServerStIdle (..) , Collect (..) - , TxSizeInBytes -- * Execution as a typed protocol , txSubmissionServerPeerPipelined + -- * deprecated API + , TxSizeInBytes ) where import Data.List.NonEmpty (NonEmpty) -import Data.Word (Word16) import Network.TypedProtocol.Core import Network.TypedProtocol.Pipelined @@ -44,7 +44,7 @@ data TxSubmissionServerPipelined txid tx m a where data Collect txid tx = -- | The result of 'SendMsgRequestTxIdsPipelined'. It also carries -- the number of txids originally requested. - CollectTxIds Word16 [(txid, TxSizeInBytes)] + CollectTxIds NumTxIdsToReq [(txid, SizeInBytes)] -- | The result of 'SendMsgRequestTxsPipelined'. The actual reply only -- contains the transactions sent, but this pairs them up with the @@ -58,18 +58,18 @@ data ServerStIdle (n :: N) txid tx m a where -- | -- SendMsgRequestTxIdsBlocking - :: Word16 -- ^ number of txids to acknowledge - -> Word16 -- ^ number of txids to request + :: NumTxIdsToAck -- ^ number of txids to acknowledge + -> NumTxIdsToReq -- ^ number of txids to request -> m a -- ^ Result if done - -> (NonEmpty (txid, TxSizeInBytes) + -> (NonEmpty (txid, SizeInBytes) -> m (ServerStIdle Z txid tx m a)) -> ServerStIdle Z txid tx m a -- | -- SendMsgRequestTxIdsPipelined - :: Word16 - -> Word16 + :: NumTxIdsToAck + -> NumTxIdsToReq -> m (ServerStIdle (S n) txid tx m a) -> ServerStIdle n txid tx m a diff --git a/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Type.hs b/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Type.hs index b09366148ef..220f1f2ab31 100644 --- a/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Type.hs +++ b/ouroboros-network-protocols/src/Ouroboros/Network/Protocol/TxSubmission2/Type.hs @@ -1,30 +1,56 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE EmptyCase #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE PolyKinds #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE EmptyCase #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeFamilies #-} -- | The type of the transaction submission protocol. -- -- This is used to relay transactions between nodes. -- -module Ouroboros.Network.Protocol.TxSubmission2.Type where +module Ouroboros.Network.Protocol.TxSubmission2.Type + ( TxSubmission2 (..) + , Message (..) + , ClientHasAgency (..) + , ServerHasAgency (..) + , NobodyHasAgency (..) + , TokBlockingStyle (..) + , StBlockingStyle (..) + , BlockingReplyList (..) + , NumTxIdsToAck (..) + , NumTxIdsToReq (..) + -- re-exports + , SizeInBytes (..) + -- deprecated API + , TxSizeInBytes + ) where +import Control.DeepSeq import Data.List.NonEmpty (NonEmpty) -import Data.Word (Word16, Word32) +import Data.Monoid (Sum (..)) +import Data.Word (Word16) +import GHC.Generics +import NoThunks.Class (NoThunks (..)) + +import Quiet (Quiet (..)) import Network.TypedProtocol.Core -import Control.DeepSeq +import Ouroboros.Network.SizeInBytes (SizeInBytes (..)) import Ouroboros.Network.Util.ShowProxy -- | Transactions are typically not big, but in principle in future we could -- have ones over 64k large. -- -type TxSizeInBytes = Word32 +type TxSizeInBytes = SizeInBytes +{-# DEPRECATED TxSizeInBytes "Use 'Ouroboros.Network.SizeInBytes.SizeInBytes' instead" #-} -- | The kind of the transaction-submission protocol, and the types of the -- states in the protocol state machine. @@ -96,6 +122,21 @@ data StBlockingStyle where StNonBlocking :: StBlockingStyle +newtype NumTxIdsToAck = NumTxIdsToAck { getNumTxIdsToAck :: Word16 } + deriving (Eq, Ord, NFData, Generic) + deriving newtype (Num, Enum, Real, Integral, Bounded, NoThunks) + deriving Semigroup via (Sum Word16) + deriving Monoid via (Sum Word16) + deriving Show via (Quiet NumTxIdsToAck) + +newtype NumTxIdsToReq = NumTxIdsToReq { getNumTxIdsToReq :: Word16 } + deriving (Eq, Ord, NFData, Generic) + deriving newtype (Num, Enum, Real, Integral, Bounded, NoThunks) + deriving Semigroup via (Sum Word16) + deriving Monoid via (Sum Word16) + deriving Show via (Quiet NumTxIdsToReq) + + -- | There are some constraints of the protocol that are not captured in the -- types of the messages, but are documented with the messages. Violation -- of these constraints is also a protocol error. The constraints are intended @@ -169,8 +210,8 @@ instance Protocol (TxSubmission2 txid tx) where -- MsgRequestTxIds :: TokBlockingStyle blocking - -> Word16 -- ^ Acknowledge this number of outstanding txids - -> Word16 -- ^ Request up to this number of txids. + -> NumTxIdsToAck -- ^ Acknowledge this number of outstanding txids + -> NumTxIdsToReq -- ^ Request up to this number of txids. -> Message (TxSubmission2 txid tx) StIdle (StTxIds blocking) -- | Reply with a list of transaction identifiers for available diff --git a/ouroboros-network-protocols/testlib/Ouroboros/Network/Protocol/TxSubmission2/Examples.hs b/ouroboros-network-protocols/testlib/Ouroboros/Network/Protocol/TxSubmission2/Examples.hs index fb207ff4243..2c9fab3ecc0 100644 --- a/ouroboros-network-protocols/testlib/Ouroboros/Network/Protocol/TxSubmission2/Examples.hs +++ b/ouroboros-network-protocols/testlib/Ouroboros/Network/Protocol/TxSubmission2/Examples.hs @@ -30,6 +30,7 @@ import Network.TypedProtocol.Pipelined (N, Nat (..)) import Ouroboros.Network.Protocol.TxSubmission2.Client import Ouroboros.Network.Protocol.TxSubmission2.Server +import Ouroboros.Network.Protocol.TxSubmission2.Type -- @@ -37,7 +38,7 @@ import Ouroboros.Network.Protocol.TxSubmission2.Server -- data TraceEventClient txid tx = - EventRecvMsgRequestTxIds (StrictSeq txid) (Map txid tx) [tx] Word16 Word16 + EventRecvMsgRequestTxIds (StrictSeq txid) (Map txid tx) [tx] NumTxIdsToAck NumTxIdsToReq | EventRecvMsgRequestTxs (StrictSeq txid) (Map txid tx) [tx] [txid] deriving Show @@ -57,7 +58,7 @@ txSubmissionClient (Ord txid, Show txid, Monad m) => Tracer m (TraceEventClient txid tx) -> (tx -> txid) - -> (tx -> TxSizeInBytes) + -> (tx -> SizeInBytes) -> Word16 -- ^ Maximum number of unacknowledged txids allowed -> [tx] -> TxSubmissionClient txid tx m () @@ -81,8 +82,8 @@ txSubmissionClient tracer txId txSize maxUnacked = recvMsgRequestTxIds :: forall blocking. TokBlockingStyle blocking - -> Word16 - -> Word16 + -> NumTxIdsToAck + -> NumTxIdsToReq -> m (ClientStTxIds blocking txid tx m ()) recvMsgRequestTxIds blocking ackNo reqNo = do traceWith tracer (EventRecvMsgRequestTxIds unackedSeq unackedMap @@ -92,8 +93,8 @@ txSubmissionClient tracer txId txSize maxUnacked = ++ "peer acknowledged more txids than possible" when ( fromIntegral (Seq.length unackedSeq) - - ackNo - + fromIntegral reqNo + - getNumTxIdsToAck ackNo + + getNumTxIdsToReq reqNo > maxUnacked) $ error $ "txSubmissionClientConst.recvMsgRequestTxIds: " ++ "peer requested more txids than permitted" @@ -157,8 +158,8 @@ txSubmissionClient tracer txId txSize maxUnacked = -- data TraceEventServer txid tx = - EventRequestTxIdsBlocking (ServerState txid tx) Word16 Word16 - | EventRequestTxIdsPipelined (ServerState txid tx) Word16 Word16 + EventRequestTxIdsBlocking (ServerState txid tx) NumTxIdsToAck NumTxIdsToReq + | EventRequestTxIdsPipelined (ServerState txid tx) NumTxIdsToAck NumTxIdsToReq | EventRequestTxsPipelined (ServerState txid tx) [txid] deriving instance (Show txid, Show tx) => Show (TraceEventServer txid tx) @@ -168,7 +169,7 @@ data ServerState txid tx = ServerState { -- which have not yet been replied to. We need to track this it keep -- our requests within the limit on the number of unacknowledged txids. -- - requestedTxIdsInFlight :: Word16, + requestedTxIdsInFlight :: NumTxIdsToReq, -- | Those transactions (by their identifier) that the client has told -- us about, and which we have not yet acknowledged. This is kept in @@ -182,7 +183,7 @@ data ServerState txid tx = ServerState { -- requested. This is not ordered to illustrate the fact that we can -- request txs out of order. We also remember the sizes, though this -- example does not make use of the size information. - availableTxids :: Map txid TxSizeInBytes, + availableTxids :: Map txid SizeInBytes, -- | Transactions we have successfully downloaded but have not yet added -- to the mempool or acknowledged. This is needed because we request @@ -195,7 +196,7 @@ data ServerState txid tx = ServerState { -- for more transactions. The number here have already been removed from -- 'unacknowledgedTxIds'. -- - numTxsToAcknowledge :: Word16 + numTxsToAcknowledge :: NumTxIdsToAck } deriving Show @@ -238,7 +239,7 @@ txSubmissionServer tracer txId maxUnacked maxTxIdsToRequest maxTxToRequest = -- so the only remaining thing to do is to ask for more txids. Since -- this is the only thing to do now, we make this a blocking call. | otherwise - , let numTxIdsToRequest = maxTxIdsToRequest `min` maxUnacked + , let numTxIdsToRequest = NumTxIdsToReq $ maxTxIdsToRequest `min` maxUnacked = assert (requestedTxIdsInFlight st == 0 && Seq.null (unacknowledgedTxIds st) && Map.null (availableTxids st) @@ -389,7 +390,8 @@ txSubmissionServer tracer txId maxUnacked maxTxIdsToRequest maxTxToRequest = -- This definition is justified by the fact that the -- 'numTxsToAcknowledge' are not included in the 'unacknowledgedTxIds'. numTxIdsToRequest = + NumTxIdsToReq $ (maxUnacked - fromIntegral (Seq.length (unacknowledgedTxIds st)) - - requestedTxIdsInFlight st) + - getNumTxIdsToReq (requestedTxIdsInFlight st)) `min` maxTxIdsToRequest diff --git a/ouroboros-network-protocols/testlib/Ouroboros/Network/Protocol/TxSubmission2/Test.hs b/ouroboros-network-protocols/testlib/Ouroboros/Network/Protocol/TxSubmission2/Test.hs index a5f31a8e2fe..43b7b7c413d 100644 --- a/ouroboros-network-protocols/testlib/Ouroboros/Network/Protocol/TxSubmission2/Test.hs +++ b/ouroboros-network-protocols/testlib/Ouroboros/Network/Protocol/TxSubmission2/Test.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} @@ -7,6 +8,7 @@ {-# LANGUAGE PolyKinds #-} {-# LANGUAGE QuantifiedConstraints #-} {-# LANGUAGE RankNTypes #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -Wno-orphans #-} @@ -17,6 +19,7 @@ module Ouroboros.Network.Protocol.TxSubmission2.Test , TxId (..) ) where +import Data.Bifunctor (second) import Data.ByteString.Lazy (ByteString) import Data.List (nub) import Data.List.NonEmpty qualified as NonEmpty @@ -240,6 +243,10 @@ prop_pipe_IO params = ioProperty (prop_channel createPipeConnectedChannels params) +deriving newtype instance Arbitrary NumTxIdsToAck +deriving newtype instance Arbitrary NumTxIdsToReq + + instance Arbitrary (AnyMessageAndAgency (TxSubmission2 TxId Tx)) where arbitrary = oneof [ pure $ AnyMessageAndAgency (ClientAgency TokInit) MsgInit @@ -255,11 +262,12 @@ instance Arbitrary (AnyMessageAndAgency (TxSubmission2 TxId Tx)) where , AnyMessageAndAgency (ClientAgency (TokTxIds TokBlocking)) <$> MsgReplyTxIds <$> (BlockingReply . NonEmpty.fromList + . map (second SizeInBytes) . QC.getNonEmpty <$> arbitrary) , AnyMessageAndAgency (ClientAgency (TokTxIds TokNonBlocking)) <$> - MsgReplyTxIds <$> (NonBlockingReply <$> arbitrary) + MsgReplyTxIds <$> (NonBlockingReply . map (second SizeInBytes) <$> arbitrary) , AnyMessageAndAgency (ServerAgency TokIdle) <$> MsgRequestTxs <$> arbitrary diff --git a/ouroboros-network/ouroboros-network.cabal b/ouroboros-network/ouroboros-network.cabal index 7cc92a7e8f5..0c571afb6d5 100644 --- a/ouroboros-network/ouroboros-network.cabal +++ b/ouroboros-network/ouroboros-network.cabal @@ -28,6 +28,7 @@ library -- At this experiment/prototype stage everything is exposed. -- This has to be tidied up once the design becomes clear. exposed-modules: + Control.Concurrent.Class.MonadSTM.Strict.TMergeVar Ouroboros.Network.BlockFetch Ouroboros.Network.BlockFetch.Client Ouroboros.Network.BlockFetch.ClientRegistry diff --git a/ouroboros-network/sim-tests-lib/Test/Ouroboros/Network/BlockFetch.hs b/ouroboros-network/sim-tests-lib/Test/Ouroboros/Network/BlockFetch.hs index 7a241ceecf8..e52faaf1068 100644 --- a/ouroboros-network/sim-tests-lib/Test/Ouroboros/Network/BlockFetch.hs +++ b/ouroboros-network/sim-tests-lib/Test/Ouroboros/Network/BlockFetch.hs @@ -6,7 +6,10 @@ {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} -module Test.Ouroboros.Network.BlockFetch (tests) where +module Test.Ouroboros.Network.BlockFetch + ( PeerGSVT (..) + , tests + ) where import Test.ChainGenerators (TestChainFork (..)) import Test.QuickCheck @@ -798,6 +801,8 @@ prop_terminate (TestChainFork _commonChain forkChain _forkChain) (Positive (Smal fork' = chainToAnchoredFragment forkChain +-- TODO: moved to some shared place (cannot be moved to +-- `ouroboros-network-testing` which doesn't depend on `ouroboros-network`) newtype PeerGSVT = PeerGSVT { unPeerGSVT :: PeerGSV } deriving Show diff --git a/ouroboros-network/sim-tests-lib/Test/Ouroboros/Network/TxSubmission.hs b/ouroboros-network/sim-tests-lib/Test/Ouroboros/Network/TxSubmission.hs index fc1a2ce782f..83d1c76ff09 100644 --- a/ouroboros-network/sim-tests-lib/Test/Ouroboros/Network/TxSubmission.hs +++ b/ouroboros-network/sim-tests-lib/Test/Ouroboros/Network/TxSubmission.hs @@ -73,7 +73,7 @@ tests = testGroup "TxSubmission" data Tx txid = Tx { getTxId :: txid, - getTxSize :: TxSizeInBytes, + getTxSize :: SizeInBytes, -- | If false this means that when this tx will be submitted to a remote -- mempool it will not be valid. The outbound mempool might contain -- invalid tx's in this sense. @@ -88,11 +88,21 @@ instance ShowProxy txid => ShowProxy (Tx txid) where instance Arbitrary txid => Arbitrary (Tx txid) where arbitrary = Tx <$> arbitrary - <*> arbitrary + <*> chooseEnum (0, maxTxSize) + -- note: + -- generating small tx sizes avoids overflow error when semigroup + -- instance of `SizeInBytes` is used (summing up all inflight tx + -- sizes). <*> frequency [ (3, pure True) , (1, pure False) ] + +-- maximal tx size +maxTxSize :: SizeInBytes +maxTxSize = 65536 + + newtype Mempool m txid = Mempool (TVar m (Seq (Tx txid))) @@ -109,7 +119,7 @@ newMempool = fmap Mempool . Seq.fromList readMempool :: MonadSTM m => Mempool m txid -> m [Tx txid] -readMempool (Mempool mempool) = toList <$> atomically (readTVar mempool) +readMempool (Mempool mempool) = toList <$> readTVarIO mempool getMempoolReader :: forall txid m. @@ -136,7 +146,7 @@ getMempoolReader (Mempool mempool) = mempoolHasTx = \txid -> isJust $ find (\tx -> getTxId tx == txid) seq } - f :: Int -> Tx txid -> (txid, Int, TxSizeInBytes) + f :: Int -> Tx txid -> (txid, Int, SizeInBytes) f idx Tx {getTxId, getTxSize} = (getTxId, idx, getTxSize) @@ -177,13 +187,13 @@ txSubmissionCodec2 = encodeTx Tx {getTxId, getTxSize, getTxValid} = CBOR.encodeListLen 3 <> CBOR.encodeInt getTxId - <> CBOR.encodeWord32 getTxSize + <> CBOR.encodeWord32 (getSizeInBytes getTxSize) <> CBOR.encodeBool getTxValid decodeTx = do _ <- CBOR.decodeListLen Tx <$> CBOR.decodeInt - <*> CBOR.decodeWord32 + <*> (SizeInBytes <$> CBOR.decodeWord32) <*> CBOR.decodeBool @@ -207,7 +217,7 @@ txSubmissionSimulation , txid ~ Int ) - => Word16 + => NumTxIdsToAck -> [Tx txid] -> ControlMessageSTM m -> Maybe DiffTime @@ -226,7 +236,7 @@ txSubmissionSimulation maxUnacked outboundTxs txSubmissionCodec2 (byteLimitsTxSubmission2 (fromIntegral . BSL.length)) timeLimitsTxSubmission2 - (fromMaybe id (delayChannel <$> outboundDelay) outboundChannel) + (maybe id delayChannel outboundDelay outboundChannel) (txSubmissionClientPeer (outboundPeer outboundMempool)) inboundAsync <- @@ -235,7 +245,7 @@ txSubmissionSimulation maxUnacked outboundTxs txSubmissionCodec2 (byteLimitsTxSubmission2 (fromIntegral . BSL.length)) timeLimitsTxSubmission2 - (fromMaybe id (delayChannel <$> inboundDelay) inboundChannel) + (maybe id delayChannel inboundDelay inboundChannel) (txSubmissionServerPeerPipelined (inboundPeer inboundMempool)) _ <- waitAnyCancel [ outboundAsync, inboundAsync ] @@ -289,7 +299,7 @@ prop_txSubmission (Positive maxUnacked) (NonEmpty outboundTxs) delay = * realToFrac (length outboundTxs `div` 4)) atomically (writeTVar controlMessageVar Terminate) txSubmissionSimulation - maxUnacked outboundTxs + (NumTxIdsToAck maxUnacked) outboundTxs (readTVar controlMessageVar) mbDelayTime mbDelayTime ) in diff --git a/ouroboros-network/src/Control/Concurrent/Class/MonadSTM/Strict/TMergeVar.hs b/ouroboros-network/src/Control/Concurrent/Class/MonadSTM/Strict/TMergeVar.hs new file mode 100644 index 00000000000..73af34070e0 --- /dev/null +++ b/ouroboros-network/src/Control/Concurrent/Class/MonadSTM/Strict/TMergeVar.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE BangPatterns #-} + +-- | STM TMergeVar mini-abstraction +-- +module Control.Concurrent.Class.MonadSTM.Strict.TMergeVar + ( TMergeVar + , newTMergeVar + , writeTMergeVar + , takeTMergeVar + , tryReadTMergeVar + ) where + +import Control.Concurrent.Class.MonadSTM.Strict + +-- | The 'TMergeVar' is like a 'TMVar' in that we take it, leaving it empty. +-- Unlike an ordinary 'TMVar' with a blocking \'put\' operation, it has a +-- non-blocking combining write operation: if a value is already present then +-- the values are combined using the 'Semigroup' operator. +-- +-- This is used much like a 'TMVar' as a one-place queue between threads but +-- with the property that we can \"improve\" the current value (if any). +-- +newtype TMergeVar m a = TMergeVar (StrictTMVar m a) + +newTMergeVar :: MonadSTM m => STM m (TMergeVar m a) +newTMergeVar = TMergeVar <$> newEmptyTMVar + +-- | Merge the current value with the given one and store it, return the updated +-- value. +-- +writeTMergeVar :: (MonadSTM m, Semigroup a) => TMergeVar m a -> a -> STM m a +writeTMergeVar (TMergeVar v) x = do + mx0 <- tryTakeTMVar v + case mx0 of + Nothing -> x <$ putTMVar v x + Just x0 -> x' <$ putTMVar v x' where !x' = x0 <> x + +takeTMergeVar :: MonadSTM m => TMergeVar m a -> STM m a +takeTMergeVar (TMergeVar v) = takeTMVar v + +tryReadTMergeVar :: MonadSTM m + => TMergeVar m a + -> STM m (Maybe a) +tryReadTMergeVar (TMergeVar v) = tryReadTMVar v diff --git a/ouroboros-network/src/Ouroboros/Network/BlockFetch/ClientState.hs b/ouroboros-network/src/Ouroboros/Network/BlockFetch/ClientState.hs index 2ff01c54618..40b8661c61d 100644 --- a/ouroboros-network/src/Ouroboros/Network/BlockFetch/ClientState.hs +++ b/ouroboros-network/src/Ouroboros/Network/BlockFetch/ClientState.hs @@ -41,6 +41,7 @@ import Data.Set (Set) import Data.Set qualified as Set import Control.Concurrent.Class.MonadSTM.Strict +import Control.Concurrent.Class.MonadSTM.Strict.TMergeVar import Control.Exception (assert) import Control.Monad (when) import Control.Monad.Class.MonadTime.SI @@ -750,38 +751,3 @@ takeTFetchRequestVar :: MonadSTM m PeerFetchInFlightLimits) takeTFetchRequestVar v = (\(r,g,l) -> (r, getLast g, getLast l)) <$> takeTMergeVar v - --- --- STM TMergeVar mini-abstraction --- - --- | The 'TMergeVar' is like a 'TMVar' in that we take it, leaving it empty. --- Unlike an ordinary 'TMVar' with a blocking \'put\' operation, it has a --- non-blocking combining write operation: if a value is already present then --- the values are combined using the 'Semigroup' operator. --- --- This is used much like a 'TMVar' as a one-place queue between threads but --- with the property that we can \"improve\" the current value (if any). --- -newtype TMergeVar m a = TMergeVar (StrictTMVar m a) - -newTMergeVar :: MonadSTM m => STM m (TMergeVar m a) -newTMergeVar = TMergeVar <$> newEmptyTMVar - --- | Merge the current value with the given one and store it, return the updated --- value. --- -writeTMergeVar :: (MonadSTM m, Semigroup a) => TMergeVar m a -> a -> STM m a -writeTMergeVar (TMergeVar v) x = do - mx0 <- tryTakeTMVar v - case mx0 of - Nothing -> x <$ putTMVar v x - Just x0 -> x' <$ putTMVar v x' where !x' = x0 <> x - -takeTMergeVar :: MonadSTM m => TMergeVar m a -> STM m a -takeTMergeVar (TMergeVar v) = takeTMVar v - -tryReadTMergeVar :: MonadSTM m - => TMergeVar m a - -> STM m (Maybe a) -tryReadTMergeVar (TMergeVar v) = tryReadTMVar v diff --git a/ouroboros-network/src/Ouroboros/Network/TxSubmission/Inbound.hs b/ouroboros-network/src/Ouroboros/Network/TxSubmission/Inbound.hs index 4f2aabddf66..cf216548a02 100644 --- a/ouroboros-network/src/Ouroboros/Network/TxSubmission/Inbound.hs +++ b/ouroboros-network/src/Ouroboros/Network/TxSubmission/Inbound.hs @@ -40,6 +40,7 @@ import Network.TypedProtocol.Pipelined (N, Nat (..), natToInt) import Ouroboros.Network.NodeToNode.Version (NodeToNodeVersion) import Ouroboros.Network.Protocol.TxSubmission2.Server +import Ouroboros.Network.Protocol.TxSubmission2.Type import Ouroboros.Network.TxSubmission.Mempool.Reader (MempoolSnapshot (..), TxSubmissionMempoolReader (..)) @@ -118,7 +119,7 @@ data ServerState txid tx = ServerState { -- are a subset of the 'unacknowledgedTxIds' that we have not yet -- requested. This is not ordered to illustrate the fact that we can -- request txs out of order. We also remember the size. - availableTxids :: !(Map txid TxSizeInBytes), + availableTxids :: !(Map txid SizeInBytes), -- | Transactions we have successfully downloaded but have not yet added -- to the mempool or acknowledged. This needed because we can request @@ -177,17 +178,17 @@ txSubmissionInbound , MonadThrow m ) => Tracer m (TraceTxSubmissionInbound txid tx) - -> Word16 -- ^ Maximum number of unacknowledged txids allowed + -> NumTxIdsToAck -- ^ Maximum number of unacknowledged txids allowed -> TxSubmissionMempoolReader txid tx idx m -> TxSubmissionMempoolWriter txid tx idx m -> NodeToNodeVersion -> TxSubmissionServerPipelined txid tx m () -txSubmissionInbound tracer maxUnacked mpReader mpWriter _version = +txSubmissionInbound tracer (NumTxIdsToAck maxUnacked) mpReader mpWriter _version = TxSubmissionServerPipelined $ continueWithStateM (serverIdle Zero) initialServerState where -- TODO #1656: replace these fixed limits by policies based on - -- TxSizeInBytes and delta-Q and the bandwidth/delay product. + -- SizeInBytes and delta-Q and the bandwidth/delay product. -- These numbers are for demo purposes only, the throughput will be low. maxTxIdsToRequest = 3 :: Word16 maxTxToRequest = 2 :: Word16 @@ -224,15 +225,15 @@ txSubmissionInbound tracer maxUnacked mpReader mpWriter _version = && Map.null (bufferedTxs st)) $ pure $ SendMsgRequestTxIdsBlocking - (numTxsToAcknowledge st) - numTxIdsToRequest + (NumTxIdsToAck (numTxsToAcknowledge st)) + (NumTxIdsToReq numTxIdsToRequest) -- Our result if the client terminates the protocol (traceWith tracer TraceTxInboundTerminated) ( collectAndContinueWithState (handleReply Zero) st { numTxsToAcknowledge = 0, requestedTxIdsInFlight = numTxIdsToRequest } - . CollectTxIds numTxIdsToRequest + . CollectTxIds (NumTxIdsToReq numTxIdsToRequest) . NonEmpty.toList) Succ n' -> if canRequestMoreTxs st @@ -270,7 +271,7 @@ txSubmissionInbound tracer maxUnacked mpReader mpWriter _version = Nat n -> StatefulCollect (ServerState txid tx) n txid tx m handleReply n = StatefulCollect $ \st collect -> case collect of - CollectTxIds reqNo txids -> do + CollectTxIds (NumTxIdsToReq reqNo) txids -> do -- Check they didn't send more than we asked for. We don't need to -- check for a minimum: the blocking case checks for non-zero -- elsewhere, and for the non-blocking case it is quite normal for @@ -342,12 +343,13 @@ txSubmissionInbound tracer maxUnacked mpReader mpWriter _version = bufferedTxs2 = Foldable.foldl' (flip Map.delete) bufferedTxs1 acknowledgedTxIds - -- If we are acknowleding transactions that are still in unacknowledgedTxIds' - -- we need to re-add them so that we also can acknowledge them again later. - -- This will happen incase of duplicate txids within the same window. + -- If we are acknowledging transactions that are still in + -- unacknowledgedTxIds' we need to re-add them so that we also can + -- acknowledge them again later. This will happen in case of + -- duplicate txids within the same window. live = filter (`elem` unacknowledgedTxIds') $ toList acknowledgedTxIds bufferedTxs3 = forceElemsToWHNF $ bufferedTxs2 <> - (Map.fromList (zip live (repeat Nothing))) + Map.fromList (zip live (repeat Nothing)) let !collected = length txs traceWith tracer $ @@ -378,7 +380,7 @@ txSubmissionInbound tracer maxUnacked mpReader mpWriter _version = -- acknowledgeTxIds :: ServerState txid tx -> StrictSeq txid - -> Map txid TxSizeInBytes + -> Map txid SizeInBytes -> MempoolSnapshot txid tx idx -> ServerState txid tx acknowledgeTxIds st txidsSeq _ _ | Seq.null txidsSeq = st @@ -446,7 +448,7 @@ txSubmissionInbound tracer maxUnacked mpReader mpWriter _version = -- We will also uses the size of txs in bytes as our limit for -- upper and lower watermarks for pipelining. We'll also use the -- amount in flight and delta-Q to estimate when we're in danger of - -- becomming idle, and need to request stalled txs. + -- becoming idle, and need to request stalled txs. -- let (txsToRequest, availableTxids') = Map.splitAt (fromIntegral maxTxToRequest) (availableTxids st) @@ -472,8 +474,8 @@ txSubmissionInbound tracer maxUnacked mpReader mpWriter _version = if numTxIdsToRequest > 0 then pure $ SendMsgRequestTxIdsPipelined - (numTxsToAcknowledge st) - numTxIdsToRequest + (NumTxIdsToAck (numTxsToAcknowledge st)) + (NumTxIdsToReq numTxIdsToRequest) (continueWithStateM (serverIdle (Succ n)) st { requestedTxIdsInFlight = requestedTxIdsInFlight st + numTxIdsToRequest, diff --git a/ouroboros-network/src/Ouroboros/Network/TxSubmission/Mempool/Reader.hs b/ouroboros-network/src/Ouroboros/Network/TxSubmission/Mempool/Reader.hs index 079cf2a68ef..771bbbfe6b1 100644 --- a/ouroboros-network/src/Ouroboros/Network/TxSubmission/Mempool/Reader.hs +++ b/ouroboros-network/src/Ouroboros/Network/TxSubmission/Mempool/Reader.hs @@ -6,8 +6,7 @@ module Ouroboros.Network.TxSubmission.Mempool.Reader ) where import Control.Monad.Class.MonadSTM (MonadSTM, STM) - -import Ouroboros.Network.Protocol.TxSubmission2.Client (TxSizeInBytes) +import Ouroboros.Network.SizeInBytes (SizeInBytes) -- | The consensus layer functionality that the inbound and outbound side of -- the tx submission logic requires. @@ -53,7 +52,7 @@ mapTxSubmissionMempoolReader f rdr = -- data MempoolSnapshot txid tx idx = MempoolSnapshot { - mempoolTxIdsAfter :: idx -> [(txid, idx, TxSizeInBytes)], + mempoolTxIdsAfter :: idx -> [(txid, idx, SizeInBytes)], mempoolLookupTx :: idx -> Maybe tx, mempoolHasTx :: txid -> Bool } diff --git a/ouroboros-network/src/Ouroboros/Network/TxSubmission/Outbound.hs b/ouroboros-network/src/Ouroboros/Network/TxSubmission/Outbound.hs index 41e96e6da3a..d5cac825788 100644 --- a/ouroboros-network/src/Ouroboros/Network/TxSubmission/Outbound.hs +++ b/ouroboros-network/src/Ouroboros/Network/TxSubmission/Outbound.hs @@ -12,10 +12,9 @@ module Ouroboros.Network.TxSubmission.Outbound import Data.Foldable (find) import Data.List.NonEmpty qualified as NonEmpty -import Data.Maybe (catMaybes, isNothing) +import Data.Maybe (catMaybes, isNothing, mapMaybe) import Data.Sequence.Strict (StrictSeq) import Data.Sequence.Strict qualified as Seq -import Data.Word (Word16) import Control.Exception (assert) import Control.Monad (unless, when) @@ -27,6 +26,7 @@ import Ouroboros.Network.ControlMessage (ControlMessage, ControlMessageSTM, timeoutWithControlMessage) import Ouroboros.Network.NodeToNode.Version (NodeToNodeVersion) import Ouroboros.Network.Protocol.TxSubmission2.Client +import Ouroboros.Network.Protocol.TxSubmission2.Type import Ouroboros.Network.TxSubmission.Mempool.Reader (MempoolSnapshot (..), TxSubmissionMempoolReader (..)) @@ -44,7 +44,7 @@ data TraceTxSubmissionOutbound txid tx data TxSubmissionProtocolError = ProtocolErrorAckedTooManyTxids | ProtocolErrorRequestedNothing - | ProtocolErrorRequestedTooManyTxids Word16 Word16 + | ProtocolErrorRequestedTooManyTxids NumTxIdsToReq NumTxIdsToAck | ProtocolErrorRequestBlocking | ProtocolErrorRequestNonBlocking | ProtocolErrorRequestedUnavailableTx @@ -78,7 +78,7 @@ txSubmissionOutbound :: forall txid tx idx m. (Ord txid, Ord idx, MonadSTM m, MonadThrow m) => Tracer m (TraceTxSubmissionOutbound txid tx) - -> Word16 -- ^ Maximum number of unacknowledged txids allowed + -> NumTxIdsToAck -- ^ Maximum number of unacknowledged txids allowed -> TxSubmissionMempoolReader txid tx idx m -> NodeToNodeVersion -> ControlMessageSTM m @@ -92,18 +92,18 @@ txSubmissionOutbound tracer maxUnacked TxSubmissionMempoolReader{..} _version co where recvMsgRequestTxIds :: forall blocking. TokBlockingStyle blocking - -> Word16 - -> Word16 + -> NumTxIdsToAck + -> NumTxIdsToReq -> m (ClientStTxIds blocking txid tx m ()) recvMsgRequestTxIds blocking ackNo reqNo = do - when (ackNo > fromIntegral (Seq.length unackedSeq)) $ + when (getNumTxIdsToAck ackNo > fromIntegral (Seq.length unackedSeq)) $ throwIO ProtocolErrorAckedTooManyTxids when ( fromIntegral (Seq.length unackedSeq) - - ackNo - + reqNo - > maxUnacked) $ + - getNumTxIdsToAck ackNo + + getNumTxIdsToReq reqNo + > getNumTxIdsToAck maxUnacked) $ throwIO (ProtocolErrorRequestedTooManyTxids reqNo maxUnacked) -- Update our tracking state to remove the number of txids that the @@ -119,7 +119,7 @@ txSubmissionOutbound tracer maxUnacked TxSubmissionMempoolReader{..} _version co !lastIdx' | null txs = lastIdx | otherwise = idx where (_, idx, _) = last txs - txs' :: [(txid, TxSizeInBytes)] + txs' :: [(txid, SizeInBytes)] txs' = [ (txid, size) | (txid, _, size) <- txs ] client' = client unackedSeq'' lastIdx' in (txs', client') @@ -183,7 +183,7 @@ txSubmissionOutbound tracer maxUnacked TxSubmissionMempoolReader{..} _version co -- The 'mempoolLookupTx' will return nothing if the transaction is no -- longer in the mempool. This is good. Neither the sending nor -- receiving side wants to forward txs that are no longer of interest. - let txs = catMaybes (map mempoolLookupTx txidxs') + let txs = mapMaybe mempoolLookupTx txidxs' client' = client unackedSeq lastIdx -- Trace the transactions to be sent in the response. diff --git a/scripts/ci/check-stylish.sh b/scripts/ci/check-stylish.sh index 4710e56b0b9..4375c32df8f 100755 --- a/scripts/ci/check-stylish.sh +++ b/scripts/ci/check-stylish.sh @@ -2,13 +2,70 @@ set -euo pipefail +function usage { + echo "Usage $(basename "$0") [-ch]" + echo "Check files with 'stylish-haskell'; by default check all files." + echo + echo " -u only check files uncommitted" + echo " -c only check files committed in HEAD" + echo " -h this help message" + exit +} + export LC_ALL=C.UTF-8 + +STYLISH_HASKELL_ARGS="-c .stylish-haskell-network.yaml -i" + +optstring=":uch" +while getopts ${optstring} arg; do + case ${arg} in + h) + usage; + exit 0 + ;; + c) + PATHS=$(git show --pretty='' --name-only HEAD) + for path in $PATHS; do + if [ "${path##*.}" == "hs" ]; then + if grep -qE '^#' $path; then + echo "$path contains CPP. Skipping." + else + echo $path + stylish-haskell $STYLISH_HASKELL_ARGS $path + fi + fi + done + exit 0 + ;; + u) + PATHS=$(git diff --name-only HEAD) + for path in $PATHS; do + if [ "${path##*.}" == "hs" ]; then + if grep -qE '^#' $path; then + echo "$path contains CPP. Skipping." + else + echo $path + stylish-haskell $STYLISH_HASKELL_ARGS $path + fi + fi + done + exit 0 + ;; + ?) + echo "Invalid argument ${arg}" + exit 1 + ;; + esac +done + # TODO CPP pragmas in export lists are not supported by stylish-haskell -fd . './quickcheck-monoids' -e hs --ignore-file ./scripts/ci/check-stylish-ignore -X stylish-haskell -c .stylish-haskell-network.yaml -i -fd . './network-mux' -e hs --ignore-file ./scripts/ci/check-stylish-ignore -X stylish-haskell -c .stylish-haskell-network.yaml -i -fd . './ouroboros-network-api' -e hs --ignore-file ./scripts/ci/check-stylish-ignore -X stylish-haskell -c .stylish-haskell-network.yaml -i -fd . './ouroboros-network-framework' -e hs --ignore-file ./scripts/ci/check-stylish-ignore -X stylish-haskell -c .stylish-haskell-network.yaml -i -fd . './ouroboros-network-mock' -e hs --ignore-file ./scripts/ci/check-stylish-ignore -X stylish-haskell -c .stylish-haskell-network.yaml -i -fd . './ouroboros-network-protocols' -e hs --ignore-file ./scripts/ci/check-stylish-ignore -X stylish-haskell -c .stylish-haskell-network.yaml -i -fd . './ouroboros-network' -e hs --ignore-file ./scripts/ci/check-stylish-ignore -X stylish-haskell -c .stylish-haskell-network.yaml -i -fd . './cardano-client' -e hs --ignore-file ./scripts/ci/check-stylish-ignore -X stylish-haskell -c .stylish-haskell-network.yaml -i +FD_OPTS="-e hs --ignore-file ./scripts/ci/check-stylish-ignore -X stylish-haskell $STYLISH_HASKELL_ARGS" + +fd . './quickcheck-monoids' $FD_OPTS +fd . './network-mux' $FD_OPTS +fd . './ouroboros-network-api' $FD_OPTS +fd . './ouroboros-network-framework' $FD_OPTS +fd . './ouroboros-network-mock' $FD_OPTS +fd . './ouroboros-network-protocols' $FD_OPTS +fd . './ouroboros-network' $FD_OPTS +fd . './cardano-client' $FD_OPTS