From 70e999e9010f7fb679ea414b1624f02b3183aace Mon Sep 17 00:00:00 2001 From: Nicolas Frisby Date: Mon, 27 Mar 2023 11:12:16 -0700 Subject: [PATCH 01/15] Implement the Peer Simulator for Genesis tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolas “Niols” Jeannerod Co-authored-by: Torsten Schmits Co-authored-by: Facundo Domínguez --- .../ouroboros-consensus-diffusion.cabal | 27 + .../test/consensus-test/Main.hs | 2 + .../Test/Consensus/BlockTree.hs | 207 +++++++ .../Test/Consensus/Genesis/Setup.hs | 53 ++ .../Consensus/Genesis/Setup/Classifiers.hs | 57 ++ .../Test/Consensus/Genesis/Setup/GenChains.hs | 144 +++++ .../Test/Consensus/Genesis/Tests.hs | 9 + .../Genesis/Tests/LongRangeAttack.hs | 52 ++ .../Network/AnchoredFragment/Extras.hs | 13 + .../Consensus/Network/Driver/Limits/Extras.hs | 106 ++++ .../Consensus/PeerSimulator/BlockFetch.hs | 141 +++++ .../Test/Consensus/PeerSimulator/Config.hs | 49 ++ .../Test/Consensus/PeerSimulator/Handlers.hs | 136 +++++ .../Test/Consensus/PeerSimulator/Resources.hs | 168 ++++++ .../Test/Consensus/PeerSimulator/Run.hs | 311 ++++++++++ .../PeerSimulator/ScheduledChainSyncServer.hs | 214 +++++++ .../Test/Consensus/PeerSimulator/Trace.hs | 71 +++ .../Test/Consensus/PointSchedule.hs | 565 ++++++++++++++++++ ouroboros-consensus/ouroboros-consensus.cabal | 2 + .../Consensus/ChainGenerator/Adversarial.hs | 18 + .../Consensus/ChainGenerator/Honest.hs | 12 +- .../Consensus/ChainGenerator/Params.hs | 17 +- .../Test/QuickCheck/Extras.hs | 16 + .../Test/Util/TestBlock.hs | 2 +- .../ChainGenerator/Tests/Adversarial.hs | 49 +- .../Consensus/ChainGenerator/Tests/Honest.hs | 41 +- 26 files changed, 2417 insertions(+), 65 deletions(-) create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/BlockTree.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/Classifiers.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/GenChains.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/Driver/Limits/Extras.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/BlockFetch.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Config.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Resources.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/ScheduledChainSyncServer.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs create mode 100644 ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PointSchedule.hs create mode 100644 ouroboros-consensus/src/unstable-consensus-testlib/Test/QuickCheck/Extras.hs diff --git a/ouroboros-consensus-diffusion/ouroboros-consensus-diffusion.cabal b/ouroboros-consensus-diffusion/ouroboros-consensus-diffusion.cabal index ea5e0265b0..d36f0f0a88 100644 --- a/ouroboros-consensus-diffusion/ouroboros-consensus-diffusion.cabal +++ b/ouroboros-consensus-diffusion/ouroboros-consensus-diffusion.cabal @@ -204,36 +204,63 @@ test-suite consensus-test hs-source-dirs: test/consensus-test main-is: Main.hs other-modules: + Test.Consensus.BlockTree + Test.Consensus.Genesis.Setup + Test.Consensus.Genesis.Setup.Classifiers + Test.Consensus.Genesis.Setup.GenChains + Test.Consensus.Genesis.Tests + Test.Consensus.Genesis.Tests.LongRangeAttack Test.Consensus.HardFork.Combinator Test.Consensus.HardFork.Combinator.A Test.Consensus.HardFork.Combinator.B + Test.Consensus.Network.AnchoredFragment.Extras + Test.Consensus.Network.Driver.Limits.Extras Test.Consensus.Node + Test.Consensus.PeerSimulator.BlockFetch + Test.Consensus.PeerSimulator.Config + Test.Consensus.PeerSimulator.Handlers + Test.Consensus.PeerSimulator.Resources + Test.Consensus.PeerSimulator.Run + Test.Consensus.PeerSimulator.ScheduledChainSyncServer + Test.Consensus.PeerSimulator.Trace + Test.Consensus.PointSchedule build-depends: , base , binary , bytestring + , cardano-crypto-class , cardano-slotting , containers + , contra-tracer , directory , fs-api ^>=0.2 , fs-sim ^>=0.2 + , hashable + , io-classes , io-sim , mtl , nothunks , ouroboros-consensus-diffusion , ouroboros-consensus:{ouroboros-consensus, unstable-consensus-testlib} + , ouroboros-network , ouroboros-network-api + , ouroboros-network-framework , ouroboros-network-mock + , ouroboros-network-protocols , QuickCheck , quiet , serialise , si-timers , sop-extras , strict-sop-core + , strict-stm , tasty , tasty-hunit , tasty-quickcheck , temporary , time + , typed-protocols + , typed-protocols-examples , unstable-diffusion-testlib + , vector diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Main.hs b/ouroboros-consensus-diffusion/test/consensus-test/Main.hs index c5b2a4db04..c4b2443ff6 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Main.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Main.hs @@ -1,5 +1,6 @@ module Main (main) where +import qualified Test.Consensus.Genesis.Tests (tests) import qualified Test.Consensus.HardFork.Combinator (tests) import qualified Test.Consensus.Node (tests) import Test.Tasty @@ -18,4 +19,5 @@ tests = Test.Consensus.HardFork.Combinator.tests ] ] + , Test.Consensus.Genesis.Tests.tests ] diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/BlockTree.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/BlockTree.hs new file mode 100644 index 0000000000..7e1513a9c2 --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/BlockTree.hs @@ -0,0 +1,207 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} + +-- REVIEW: There is a `BlockTree` in `Test.Utils.TestBlock`. It relies on +-- different mechanisms but maybe we should rely on that instead to avoid +-- duplication. + +module Test.Consensus.BlockTree ( + BlockTree (..) + , BlockTreeBranch (..) + , PathAnchoredAtSource (..) + , addBranch + , addBranch' + , allFragments + , findFragment + , findPath + , mkTrunk + , prettyPrint + ) where + +import Cardano.Slotting.Block (BlockNo) +import Cardano.Slotting.Slot (SlotNo (unSlotNo)) +import Data.Foldable (asum) +import Data.Function ((&)) +import Data.Functor ((<&>)) +import Data.Maybe (fromJust, fromMaybe) +import qualified Data.Vector as Vector +import Ouroboros.Consensus.Block.Abstract (blockNo, blockSlot, + unBlockNo) +import qualified Ouroboros.Network.AnchoredFragment as AF +import Text.Printf (printf) + +-- | Represent a branch of a block tree by a prefix and a suffix. The full +-- fragment (the prefix and suffix catenated) is provided for practicality. +-- +-- INVARIANT: the head of @btbPrefix@ is the anchor of @btbSuffix@. +-- +-- INVARIANT: @btbFull == fromJust $ AF.join btbPrefix btbSuffix@ +data BlockTreeBranch blk = BlockTreeBranch { + btbPrefix :: AF.AnchoredFragment blk, + btbSuffix :: AF.AnchoredFragment blk, + btbFull :: AF.AnchoredFragment blk + } + deriving (Show) + +-- | Represent a block tree with a main trunk and branches leaving from the +-- trunk in question. All the branches are represented by their prefix to and +-- suffix from the intersection point. +-- +-- INVARIANT: The branches' prefixes share the same anchor as the trunk and are +-- fully contained in the trunk. +-- +-- INVARIANT: The branches' suffixes are anchored in the trunk and do not +-- contain any blocks in common with the trunk. +-- +-- INVARIANT: The branches' suffixes do not contain any block in common with one +-- another. +data BlockTree blk = BlockTree { + btTrunk :: AF.AnchoredFragment blk, + btBranches :: [BlockTreeBranch blk] + } + deriving (Show) + +-- | Make a block tree made of only a trunk. +mkTrunk :: AF.AnchoredFragment blk -> BlockTree blk +mkTrunk btTrunk = BlockTree { btTrunk, btBranches = [] } + +-- | Add a branch to an existing block tree. +-- +-- PRECONDITION: The given fragment intersects with the trunk or its anchor. +-- +-- FIXME: we should enforce that the branch's prefix shares the same anchor as +-- the trunk. +-- +-- FIXME: we should enforce that the new branch' suffix does not contain any +-- block in common with an existingbranch. +addBranch :: AF.HasHeader blk => AF.AnchoredFragment blk -> BlockTree blk -> Maybe (BlockTree blk) +addBranch branch BlockTree{..} = do + (_, btbPrefix, _, btbSuffix) <- AF.intersect btTrunk branch + -- NOTE: We could use the monadic bind for @Maybe@ here but we would rather + -- catch bugs quicker. + let btbFull = fromJust $ AF.join btbPrefix btbSuffix + pure $ BlockTree { btTrunk, btBranches = BlockTreeBranch { .. } : btBranches } + +-- | Same as @addBranch@ but assumes that the precondition holds. +addBranch' :: AF.HasHeader blk => AF.AnchoredFragment blk -> BlockTree blk -> BlockTree blk +addBranch' branch blockTree = + fromMaybe (error "addBranch': precondition does not hold") $ addBranch branch blockTree + +-- | Return all the full fragments from the root of the tree. +allFragments :: BlockTree blk -> [AF.AnchoredFragment blk] +allFragments BlockTree{..} = btTrunk : map btbFull btBranches + +-- | Look for a point in the block tree and return a fragment going from the +-- root of the tree to the point in question. +findFragment :: AF.HasHeader blk => AF.Point blk -> BlockTree blk -> Maybe (AF.AnchoredFragment blk) +findFragment point blockTree = + allFragments blockTree + & map (\fragment -> AF.splitAfterPoint fragment point) + & asum + <&> fst + +-- | See 'findPath'. +newtype PathAnchoredAtSource = PathAnchoredAtSource Bool + +-- | @findPath source target blockTree@ finds a path from the @source@ point to +-- the @target@ point in the @blockTree@ and returns it as an anchored fragment +-- It returns @Nothing@ when either of @source@ are @target@ are not in the +-- 'BlockTree'. There are two interesting properties on this fragment: +-- +-- 1. Whether the returned fragment is anchored at the @source@. +-- 2. Whether the returned fragment is empty. +-- +-- Together, those two properties form four interesting cases: +-- +-- a. If the fragment is anchored at the @source@ and is empty, then @source +-- == target@. +-- +-- b. If the fragment is anchored at the @source@ and is not empty, then +-- @source@ is an ancestor of @target@ and the fragment contains all the +-- blocks between them, @target@ included. +-- +-- c. If the fragment is not anchored at the @source@ and is empty, then +-- @target@ is an ancestor of @source@. +-- +-- d. If the fragment is not anchored at the @source@ and is not empty, then +-- it is anchored at the youngest common ancestor of both @source@ and +-- @target@ and contains all the blocks between that ancestor and @target@. +findPath :: + AF.HasHeader blk => + AF.Point blk -> + AF.Point blk -> + BlockTree blk -> + Maybe (PathAnchoredAtSource, AF.AnchoredFragment blk) +findPath source target blockTree = do + sourceFragment <- findFragment source blockTree + targetFragment <- findFragment target blockTree + (_, _, _, targetSuffix) <- AF.intersect sourceFragment targetFragment + pure ( + PathAnchoredAtSource (AF.anchorPoint targetSuffix == source), + targetSuffix + ) + +-- | Pretty prints a block tree for human readability. For instance: +-- +-- slots: 0 1 2 3 4 5 6 7 8 9 +-- trunk: 0─────1──2──3──4─────5──6──7 +-- ╰─────3──4─────5 +-- +-- Returns a list of strings intended to be catenated with a newline. +prettyPrint :: AF.HasHeader blk => BlockTree blk -> [String] +prettyPrint blockTree = do + let honestFragment = btTrunk blockTree + let advFragment = btbSuffix $ head $ btBranches blockTree + + let (oSlotNo, oBlockNo) = slotAndBlockNoFromAnchor $ AF.anchor honestFragment + let (hSlotNo, _) = slotAndBlockNoFromAnchor $ AF.headAnchor honestFragment + + let (aoSlotNo, _) = slotAndBlockNoFromAnchor $ AF.anchor advFragment + let (ahSlotNo, _) = slotAndBlockNoFromAnchor $ AF.headAnchor advFragment + + let firstSlotNo = min oSlotNo aoSlotNo + let lastSlotNo = max hSlotNo ahSlotNo + + -- FIXME: only handles two fragments at this point. not very hard to make it + -- handle all of them. Some work needed to make it handle all of them _in a + -- clean way_. + + [ "Block tree:" + , + + [firstSlotNo .. lastSlotNo] + & map (printf "%2d" . unSlotNo) + & unwords + & (" slots: " ++) + , + + honestFragment + & AF.toOldestFirst + & map (\block -> (fromIntegral (unSlotNo (blockSlot block) - 1), Just (unBlockNo (blockNo block)))) + & Vector.toList . (Vector.replicate (fromIntegral (unSlotNo hSlotNo - unSlotNo oSlotNo)) Nothing Vector.//) + & map (maybe " " (printf "%2d")) + & unwords + & map (\c -> if c == ' ' then '─' else c) + & ("─" ++) + & (printf "%2d" (unBlockNo oBlockNo) ++) + & (" trunk: " ++) + , + + advFragment + & AF.toOldestFirst + & map (\block -> (fromIntegral (unSlotNo (blockSlot block) - unSlotNo aoSlotNo - 1), Just (unBlockNo (blockNo block)))) + & Vector.toList . (Vector.replicate (fromIntegral (unSlotNo ahSlotNo - unSlotNo aoSlotNo)) Nothing Vector.//) + & map (maybe " " (printf "%2d")) + & unwords + & map (\c -> if c == ' ' then '─' else c) + & (" ╰─" ++) + & (replicate (3 * fromIntegral (unSlotNo (aoSlotNo - oSlotNo))) ' ' ++) + & (" " ++) + ] + + where + slotAndBlockNoFromAnchor :: AF.Anchor b -> (SlotNo, BlockNo) + slotAndBlockNoFromAnchor = \case + AF.AnchorGenesis -> (0, 0) + AF.Anchor slotNo _ blockNo' -> (slotNo, blockNo') diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup.hs new file mode 100644 index 0000000000..b06fa9e5d1 --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup.hs @@ -0,0 +1,53 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE NamedFieldPuns #-} + +module Test.Consensus.Genesis.Setup + ( module Test.Consensus.Genesis.Setup, + module Test.Consensus.Genesis.Setup.GenChains + ) +where + +import Control.Monad.Class.MonadTime (MonadTime) +import Control.Monad.Class.MonadTimer.SI (MonadTimer) +import Control.Tracer (traceWith) +import Ouroboros.Consensus.Util.Condense +import Ouroboros.Consensus.Util.IOLike +import qualified Test.Consensus.BlockTree as BT +import Test.Consensus.PointSchedule +import Test.Consensus.PeerSimulator.Run +import Test.QuickCheck +import Test.Util.Orphans.IOLike () +import Test.Util.Tracer (recordingTracerTVar) +import Test.Consensus.Genesis.Setup.GenChains + +runTest :: + (IOLike m, MonadTime m, MonadTimer m) => + GenesisTest -> + PointSchedule -> + (TestFragH -> Property) -> + m Property +runTest genesisTest@GenesisTest {gtBlockTree, gtHonestAsc} schedule makeProperty = do + (tracer, getTrace) <- recordingTracerTVar + -- let tracer = debugTracer + + traceWith tracer $ "Honest active slot coefficient: " ++ show gtHonestAsc + + mapM_ (traceWith tracer) $ BT.prettyPrint gtBlockTree + + result <- runPointSchedule schedulerConfig genesisTest schedule tracer + trace <- unlines <$> getTrace + + let + prop = case result of + Left exn -> + counterexample ("exception: " <> show exn) False + Right fragment -> + counterexample ("result: " <> condense fragment) (makeProperty fragment) + + pure $ counterexample trace prop + where + schedulerConfig = SchedulerConfig {enableTimeouts = False} diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/Classifiers.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/Classifiers.hs new file mode 100644 index 0000000000..a5657c26d7 --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/Classifiers.hs @@ -0,0 +1,57 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} + +module Test.Consensus.Genesis.Setup.Classifiers ( + Classifiers (..) + , classifiers + ) where + +import Ouroboros.Consensus.Block.Abstract (SlotNo (SlotNo), + withOrigin) +import Ouroboros.Consensus.Config +import Ouroboros.Network.AnchoredFragment (anchor, anchorToSlotNo, + headSlot) +import qualified Ouroboros.Network.AnchoredFragment as AF +import Test.Consensus.BlockTree (BlockTree (..), BlockTreeBranch (..)) +import Test.Consensus.Network.AnchoredFragment.Extras (slotLength) +import Test.Consensus.PointSchedule +import Test.Util.Orphans.IOLike () + +-- | Interesting categories to classify test inputs +data Classifiers = + Classifiers { + -- | There are more than k blocks in the alternative chain after the intersection + existsSelectableAdversary :: Bool, + -- | There are at least scg slots after the intesection on both the honest + -- and the alternative chain + -- + -- Knowing if there is a Genesis window after the intersection is important because + -- otherwise the Genesis node has no chance to advance the immutable tip past + -- the Limit on Eagerness. + -- + genesisWindowAfterIntersection :: Bool + } + +classifiers :: GenesisTest -> Classifiers +classifiers GenesisTest {gtBlockTree, gtSecurityParam = SecurityParam k, gtGenesisWindow = GenesisWindow scg} = + Classifiers {existsSelectableAdversary, genesisWindowAfterIntersection} + where + genesisWindowAfterIntersection = + any fragmentHasGenesis branches + + fragmentHasGenesis btb = + let + frag = btbSuffix btb + SlotNo intersection = withOrigin 0 id (anchorToSlotNo (anchor frag)) + in isSelectable btb && slotLength frag > fromIntegral scg && goodTipSlot - intersection > scg + + existsSelectableAdversary = + any isSelectable branches + + isSelectable BlockTreeBranch{..} = AF.length btbSuffix > fromIntegral k + + SlotNo goodTipSlot = withOrigin 0 id (headSlot goodChain) + + branches = btBranches gtBlockTree + + goodChain = btTrunk gtBlockTree diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/GenChains.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/GenChains.hs new file mode 100644 index 0000000000..e0dc5eb02f --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/GenChains.hs @@ -0,0 +1,144 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Test.Consensus.Genesis.Setup.GenChains ( + GenesisTest (..) + , genChains + ) where + +import Control.Monad (replicateM) +import qualified Control.Monad.Except as Exn +import Data.List (foldl') +import Data.Proxy (Proxy (Proxy)) +import qualified Data.Vector.Unboxed as Vector +import Data.Word (Word8) +import Ouroboros.Consensus.Block.Abstract hiding (Header) +import Ouroboros.Consensus.Protocol.Abstract + (SecurityParam (SecurityParam)) +import Ouroboros.Network.AnchoredFragment (AnchoredFragment) +import qualified Ouroboros.Network.AnchoredFragment as AF +import qualified Test.Consensus.BlockTree as BT +import Test.Consensus.PointSchedule +import qualified Test.Ouroboros.Consensus.ChainGenerator.Adversarial as A +import Test.Ouroboros.Consensus.ChainGenerator.Adversarial + (genPrefixBlockCount) +import Test.Ouroboros.Consensus.ChainGenerator.Counting + (Count (Count), getVector) +import qualified Test.Ouroboros.Consensus.ChainGenerator.Honest as H +import Test.Ouroboros.Consensus.ChainGenerator.Honest + (ChainSchema (ChainSchema), HonestRecipe (..)) +import Test.Ouroboros.Consensus.ChainGenerator.Params +import qualified Test.Ouroboros.Consensus.ChainGenerator.Slot as S +import Test.Ouroboros.Consensus.ChainGenerator.Slot (S) +import qualified Test.QuickCheck as QC +import Test.QuickCheck.Extras (unsafeMapSuchThatJust) +import Test.QuickCheck.Random (QCGen) +import Test.Util.Orphans.IOLike () +import Test.Util.TestBlock hiding (blockTree) + +-- | Random generator for an honest chain recipe and schema. +genHonestChainSchema :: QC.Gen (Asc, H.HonestRecipe, H.SomeHonestChainSchema) +genHonestChainSchema = do + asc <- genAsc + honestRecipe <- H.genHonestRecipe + + H.SomeCheckedHonestRecipe Proxy Proxy honestRecipe' <- + case Exn.runExcept $ H.checkHonestRecipe honestRecipe of + Left exn -> error $ "impossible! " <> show (honestRecipe, exn) + Right honestRecipe' -> pure honestRecipe' + (seed :: QCGen) <- QC.arbitrary + let schema = H.uniformTheHonestChain (Just asc) honestRecipe' seed + + pure (asc, honestRecipe, H.SomeHonestChainSchema Proxy Proxy schema) + +-- | Random generator for one alternative chain schema forking off a given +-- honest chain schema. The alternative chain schema is returned as the pair of +-- a slot number on the honest chain schema and a list of active slots. +-- +-- REVIEW: Use 'SlotNo' instead of 'Int'? +genAlternativeChainSchema :: (H.HonestRecipe, H.ChainSchema base hon) -> QC.Gen (Int, [S]) +genAlternativeChainSchema (testRecipeH, arHonest) = + unsafeMapSuchThatJust $ do + let H.HonestRecipe kcp scg delta _len = testRecipeH + + (seedPrefix :: QCGen) <- QC.arbitrary + let arPrefix = genPrefixBlockCount seedPrefix arHonest + + let testRecipeA = A.AdversarialRecipe { + A.arPrefix, + A.arParams = (kcp, scg, delta), + A.arHonest + } + + alternativeAsc <- ascFromBits <$> QC.choose (1 :: Word8, maxBound - 1) + + case Exn.runExcept $ A.checkAdversarialRecipe testRecipeA of + Left e -> case e of + A.NoSuchAdversarialBlock -> pure Nothing + A.NoSuchCompetitor -> error $ "impossible! " <> show e + A.NoSuchIntersection -> error $ "impossible! " <> show e + + Right (A.SomeCheckedAdversarialRecipe _ testRecipeA'') -> do + let Count prefixCount = arPrefix + (seed :: QCGen) <- QC.arbitrary + let H.ChainSchema _ v = A.uniformAdversarialChain (Just alternativeAsc) testRecipeA'' seed + pure $ Just (prefixCount, Vector.toList (getVector v)) + +-- | Random generator for a block tree. The block tree contains one trunk (the +-- “honest” chain) and as many branches as given as a parameter (the +-- “alternative” chains or “bad” chains). For instance, one such tree could be +-- graphically represented as: +-- +-- slots: 1 2 3 4 5 6 7 8 9 +-- trunk: O─────1──2──3──4─────5──6──7 +-- │ ╰─────6 +-- ╰─────3──4─────5 +genChains :: Word -> QC.Gen GenesisTest +genChains numForks = do + (asc, honestRecipe, someHonestChainSchema) <- genHonestChainSchema + + H.SomeHonestChainSchema _ _ honestChainSchema <- pure someHonestChainSchema + let ChainSchema _ vH = honestChainSchema + goodChain = mkTestFragment goodBlocks + -- blocks for the good chain in reversed order + goodBlocks = mkTestBlocks True [] slotsH + slotsH = Vector.toList (getVector vH) + HonestRecipe (Kcp kcp) (Scg scg) _delta _len = honestRecipe + + alternativeChainSchemas <- replicateM (fromIntegral numForks) (genAlternativeChainSchema (honestRecipe, honestChainSchema)) + pure $ GenesisTest { + gtHonestAsc = asc, + gtSecurityParam = SecurityParam (fromIntegral kcp), + gtGenesisWindow = GenesisWindow (fromIntegral scg), + gtBlockTree = foldl' (flip BT.addBranch') (BT.mkTrunk goodChain) $ map (genAdversarialFragment goodBlocks) alternativeChainSchemas + } + + where + genAdversarialFragment :: [TestBlock] -> (Int, [S]) -> TestFrag + genAdversarialFragment goodBlocks (prefixCount, slotsA) + = + mkTestFragment (mkTestBlocks False prefix slotsA) + where + -- blocks in the common prefix in reversed order + prefix = drop (length goodBlocks - prefixCount) goodBlocks + + mkTestFragment :: [TestBlock] -> AnchoredFragment TestBlock + mkTestFragment = + AF.fromNewestFirst AF.AnchorGenesis + + mkTestBlocks :: Bool -> [TestBlock] -> [S] -> [TestBlock] + mkTestBlocks honest pre active = + fst (foldl' folder ([], 0) active) + where + folder (chain, inc) s | S.test S.notInverted s = (issue inc chain, 0) + | otherwise = (chain, inc + 1) + issue inc (h : t) = incSlot inc (successorBlock h) : h : t + issue inc [] | [] <- pre = [ incSlot inc (firstBlock (if honest then 0 else 1)) ] + | h : t <- pre = incSlot inc (forkBlock (successorBlock h)) : h : t + + incSlot :: SlotNo -> TestBlock -> TestBlock + incSlot n b = b { tbSlot = tbSlot b + n } diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests.hs new file mode 100644 index 0000000000..4b20d3f057 --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests.hs @@ -0,0 +1,9 @@ +module Test.Consensus.Genesis.Tests (tests) where + +import qualified Test.Consensus.Genesis.Tests.LongRangeAttack as LongRangeAttack +import Test.Tasty + +tests :: TestTree +tests = testGroup "Genesis tests" + [ LongRangeAttack.tests + ] diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs new file mode 100644 index 0000000000..26c4dac744 --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs @@ -0,0 +1,52 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} + +module Test.Consensus.Genesis.Tests.LongRangeAttack (tests) where + +import Control.Monad.IOSim (runSimOrThrow) +import Ouroboros.Consensus.Block.Abstract (HeaderHash) +import Ouroboros.Network.AnchoredFragment (headAnchor) +import qualified Ouroboros.Network.AnchoredFragment as AF +import Test.Consensus.Genesis.Setup +import Test.Consensus.Genesis.Setup.Classifiers +import Test.Consensus.PointSchedule +import qualified Test.QuickCheck as QC +import Test.QuickCheck +import Test.QuickCheck.Extras (unsafeMapSuchThatJust) +import Test.Tasty +import Test.Tasty.QuickCheck +import Test.Util.Orphans.IOLike () +import Test.Util.TestBlock (TestBlock, unTestHash) + +tests :: TestTree +tests = testProperty "long range attack" prop_longRangeAttack + +genChainsAndSchedule :: Word -> ScheduleType -> QC.Gen (GenesisTest, PointSchedule) +genChainsAndSchedule numAdversaries scheduleType = + unsafeMapSuchThatJust do + gt <- genChains numAdversaries + pure $ ((gt,) <$> genSchedule scheduleType (gtBlockTree gt)) + +prop_longRangeAttack :: QC.Gen QC.Property +prop_longRangeAttack = do + (genesisTest, schedule) <- genChainsAndSchedule 1 FastAdversary + let Classifiers {..} = classifiers genesisTest + + pure $ withMaxSuccess 10 $ runSimOrThrow $ + runTest genesisTest schedule $ \fragment -> + classify genesisWindowAfterIntersection "Full genesis window after intersection" + $ existsSelectableAdversary ==> not $ isHonestTestFragH fragment + -- TODO + -- $ not existsSelectableAdversary ==> immutableTipBeforeFork fragment + where + isHonestTestFragH :: TestFragH -> Bool + isHonestTestFragH frag = case headAnchor frag of + AF.AnchorGenesis -> True + AF.Anchor _ hash _ -> isHonestTestHeaderHash hash + + isHonestTestHeaderHash :: HeaderHash TestBlock -> Bool + isHonestTestHeaderHash = all (0 ==) . unTestHash diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs new file mode 100644 index 0000000000..4a4ecd3ce7 --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs @@ -0,0 +1,13 @@ +module Test.Consensus.Network.AnchoredFragment.Extras (slotLength) where + +import Cardano.Slotting.Slot (SlotNo (unSlotNo), withOrigin) +import Ouroboros.Network.AnchoredFragment (AnchoredFragment, + HasHeader, anchor, anchorToSlotNo, headAnchor) + +-- | Number of slots on which the fragment spans. This is different from the +-- 'length' which is the number of blocks in the fragment. +slotLength :: HasHeader blk => AnchoredFragment blk -> Int +slotLength fragment = + fromIntegral $ unSlotNo $ + withOrigin 0 id (anchorToSlotNo $ headAnchor fragment) + - withOrigin 0 id (anchorToSlotNo $ anchor fragment) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/Driver/Limits/Extras.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/Driver/Limits/Extras.hs new file mode 100644 index 0000000000..9b6641c1ce --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/Driver/Limits/Extras.hs @@ -0,0 +1,106 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE QuantifiedConstraints #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} + +module Test.Consensus.Network.Driver.Limits.Extras ( + chainSyncNoSizeLimits + , chainSyncNoTimeouts + , chainSyncTimeouts + , runConnectedPeersPipelinedWithLimits + ) where + +import Cardano.Slotting.Time (SlotLength, getSlotLength) +import Control.Monad.Class.MonadTimer.SI (MonadTimer) +import Control.Tracer (Contravariant (contramap), Tracer) +import Data.Time.Clock (secondsToDiffTime) +import qualified Network.TypedProtocol as TP +import qualified Network.TypedProtocol.Codec as TP +import Ouroboros.Consensus.Util (ShowProxy) +import Ouroboros.Consensus.Util.IOLike (DiffTime, + MonadAsync (concurrently), MonadFork, MonadMask, + MonadSTM (STM), MonadThrow) +import Ouroboros.Network.Channel (Channel) +import Ouroboros.Network.Driver (TraceSendRecv, runPeer) +import Ouroboros.Network.Driver.Limits (runPipelinedPeerWithLimits) +import Ouroboros.Network.Driver.Simple (Role (Client, Server)) +import Ouroboros.Network.Protocol.ChainSync.Codec + (ChainSyncTimeout (..), byteLimitsChainSync) +import Ouroboros.Network.Protocol.ChainSync.Type (ChainSync, + ClientHasAgency, ServerHasAgency) +import Ouroboros.Network.Protocol.Limits (ProtocolSizeLimits (..), + ProtocolTimeLimits (..), shortWait) +import Test.Ouroboros.Consensus.ChainGenerator.Params (Asc, ascVal) +import Test.Util.Orphans.IOLike () + +-- | Same as 'runConnectedPeersPipelined' except the client peer is ran not with +-- 'runPipelinedPeer' but with 'runPipelinedPeerWithLimits'. +runConnectedPeersPipelinedWithLimits :: + ( MonadAsync m + , MonadFork m + , MonadMask m + , MonadThrow (STM m) + , MonadTimer m + , Show failure + , forall (st' :: ps). Show (ClientHasAgency st') + , forall (st' :: ps). Show (ServerHasAgency st') + , ShowProxy ps + ) => + m (Channel m bytes, Channel m bytes) -> + Tracer m (Role, TraceSendRecv ps) -> + TP.Codec ps failure m bytes -> + ProtocolSizeLimits ps bytes -> + ProtocolTimeLimits ps -> + TP.PeerPipelined ps pr st m a -> + TP.Peer ps (TP.FlipAgency pr) st m b -> + m (a, b) +runConnectedPeersPipelinedWithLimits createChannels tracer codec sizeLimits timeLimits client server = + createChannels >>= \(clientChannel, serverChannel) -> + (fst <$> runPipelinedPeerWithLimits tracerClient codec sizeLimits timeLimits clientChannel client) + `concurrently` + (fst <$> runPeer tracerServer codec serverChannel server) + where + tracerClient = contramap ((,) Client) tracer + tracerServer = contramap ((,) Server) tracer + +chainSyncNoSizeLimits :: ProtocolSizeLimits (ChainSync header point tip) bytes +chainSyncNoSizeLimits = byteLimitsChainSync (const 0) + +chainSyncTimeouts :: + SlotLength -> + Asc -> + ChainSyncTimeout +chainSyncTimeouts t f = + ChainSyncTimeout{ + canAwaitTimeout + , intersectTimeout + , mustReplyTimeout + } + where + canAwaitTimeout :: Maybe DiffTime + canAwaitTimeout = shortWait -- REVIEW: what is this exactly? + + intersectTimeout :: Maybe DiffTime + intersectTimeout = shortWait -- REVIEW: what is this exactly? + + -- | The following timeout is derived from the average length of a streak of + -- empty slots. If the probability of the election of a leader is @f@ and + -- @Y@ is a probability, then a streak of empty slots will be shorter than + -- @log (1 - Y) / log (1 - f)@ with probability @Y@. Main net nodes pick a + -- random value for @Y@ between 99.9% and 99.999%. For our use case, we + -- choose the tightest bound of 99.9%. + mustReplyTimeout :: Maybe DiffTime + mustReplyTimeout = Just $ secondsToDiffTime $ round $ + realToFrac (getSlotLength t) + * log (1 - 0.999) / log (1 - ascVal f) + +chainSyncNoTimeouts :: ChainSyncTimeout +chainSyncNoTimeouts = + ChainSyncTimeout { + canAwaitTimeout = Nothing + , intersectTimeout = Nothing + , mustReplyTimeout = Nothing + } diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/BlockFetch.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/BlockFetch.hs new file mode 100644 index 0000000000..45a9095225 --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/BlockFetch.hs @@ -0,0 +1,141 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# OPTIONS_GHC -Wno-missing-export-lists #-} + +-- | Functions that call to the BlockFetch API to start clients and servers +module Test.Consensus.PeerSimulator.BlockFetch ( + runBlockFetchClient + , startBlockFetchLogic + , startKeepAliveThread + ) where + +import Control.Monad (void) +import Control.Monad.Class.MonadTime +import Control.Tracer (nullTracer) +import Data.Hashable (Hashable) +import Data.Map.Strict (Map) +import Network.TypedProtocol.Channel (createConnectedChannels) +import Network.TypedProtocol.Driver.Simple + (runConnectedPeersPipelined) +import Ouroboros.Consensus.Block (HasHeader) +import Ouroboros.Consensus.Block.Abstract (Header, Point (..)) +import qualified Ouroboros.Consensus.MiniProtocol.BlockFetch.ClientInterface as BlockFetchClientInterface +import Ouroboros.Consensus.Node.ProtocolInfo + (NumCoreNodes (NumCoreNodes)) +import Ouroboros.Consensus.Storage.ChainDB.API +import Ouroboros.Consensus.Util.IOLike (IOLike, STM, atomically, + retry) +import Ouroboros.Consensus.Util.ResourceRegistry +import Ouroboros.Network.AnchoredFragment (AnchoredFragment) +import qualified Ouroboros.Network.AnchoredFragment as AF +import Ouroboros.Network.BlockFetch (BlockFetchConfiguration (..), + FetchClientRegistry, FetchMode (..), blockFetchLogic, + bracketFetchClient, bracketKeepAliveClient) +import Ouroboros.Network.BlockFetch.Client (blockFetchClient) +import Ouroboros.Network.ControlMessage (ControlMessageSTM) +import Ouroboros.Network.NodeToNode.Version (NodeToNodeVersion, + isPipeliningEnabled) +import Ouroboros.Network.Protocol.BlockFetch.Codec (codecBlockFetchId) +import Ouroboros.Network.Protocol.BlockFetch.Server + (BlockFetchBlockSender (SendMsgNoBlocks, SendMsgStartBatch), + BlockFetchSendBlocks (SendMsgBatchDone, SendMsgBlock), + BlockFetchServer (..), blockFetchServerPeer) +import Ouroboros.Network.Protocol.BlockFetch.Type (ChainRange (..)) +import Test.Util.Orphans.IOLike () +import Test.Util.TestBlock (BlockConfig (TestBlockConfig), TestBlock) +import Test.Util.Time (dawnOfTime) + + +startBlockFetchLogic + :: forall m peer. + (Hashable peer, Ord peer, IOLike m) + => ResourceRegistry m + -> ChainDB m TestBlock + -> FetchClientRegistry peer (Header TestBlock) TestBlock m + -> STM m (Map peer (AnchoredFragment (Header TestBlock))) + -> m () +startBlockFetchLogic registry chainDb fetchClientRegistry getCandidates = do + let slotForgeTime :: BlockFetchClientInterface.SlotForgeTimeOracle m TestBlock + slotForgeTime _ = pure dawnOfTime + + blockFetchConsensusInterface = + BlockFetchClientInterface.mkBlockFetchConsensusInterface + (TestBlockConfig $ NumCoreNodes 0) -- Only needed when minting blocks + (BlockFetchClientInterface.defaultChainDbView chainDb) + getCandidates + (\_hdr -> 1000) + slotForgeTime + (pure FetchModeBulkSync) + + blockFetchCfg = BlockFetchConfiguration + { bfcMaxConcurrencyBulkSync = 2 + , bfcMaxConcurrencyDeadline = 2 + , bfcMaxRequestsInflight = 4 + , bfcDecisionLoopInterval = 0 + , bfcSalt = 0 + } + + void $ forkLinkedThread registry "BlockFetchLogic" $ + blockFetchLogic + nullTracer + nullTracer + blockFetchConsensusInterface + fetchClientRegistry + blockFetchCfg + +startKeepAliveThread + :: forall m peer. + (Ord peer, IOLike m) + => ResourceRegistry m + -> FetchClientRegistry peer (Header TestBlock) TestBlock m + -> peer + -> m () +startKeepAliveThread registry fetchClientRegistry peerId = + void $ forkLinkedThread registry "KeepAlive" $ + bracketKeepAliveClient fetchClientRegistry peerId $ \_ -> + atomically retry + +runBlockFetchClient + :: (Ord peer, IOLike m, MonadTime m) + => peer + -> FetchClientRegistry peer (Header TestBlock) TestBlock m + -> ControlMessageSTM m + -> m (AnchoredFragment TestBlock) + -> m () +runBlockFetchClient peerId fetchClientRegistry controlMsgSTM getCurrentServerChain = + bracketFetchClient fetchClientRegistry ntnVersion isPipeliningEnabled peerId $ \clientCtx -> do + let bfClient = blockFetchClient ntnVersion controlMsgSTM nullTracer clientCtx + bfServer = blockFetchServerPeer $ mockBlockFetchServer getCurrentServerChain + + fst <$> runConnectedPeersPipelined + createConnectedChannels + nullTracer + codecBlockFetchId + bfClient + bfServer + where + ntnVersion :: NodeToNodeVersion + ntnVersion = maxBound + +mockBlockFetchServer :: + forall m blk. + (Monad m, HasHeader blk) + => m (AnchoredFragment blk) + -> BlockFetchServer blk (Point blk) m () +mockBlockFetchServer getCurrentChain = idle + where + idle :: BlockFetchServer blk (Point blk) m () + idle = flip BlockFetchServer () $ \(ChainRange from to) -> do + curChain <- getCurrentChain + pure $ case AF.sliceRange curChain from to of + Nothing -> SendMsgNoBlocks (pure idle) + Just slice -> SendMsgStartBatch $ sendBlocks (AF.toOldestFirst slice) + + sendBlocks :: [blk] -> m (BlockFetchSendBlocks blk (Point blk) m ()) + sendBlocks = pure . \case + [] -> SendMsgBatchDone (pure idle) + blk : blks -> SendMsgBlock blk (sendBlocks blks) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Config.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Config.hs new file mode 100644 index 0000000000..2719fcf88f --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Config.hs @@ -0,0 +1,49 @@ +{-# LANGUAGE TypeFamilies #-} + +module Test.Consensus.PeerSimulator.Config (defaultCfg) where + +import Cardano.Crypto.DSIGN (SignKeyDSIGN (..), VerKeyDSIGN (..)) +import Cardano.Slotting.Time (SlotLength, slotLengthFromSec) +import qualified Data.Map.Strict as Map +import Ouroboros.Consensus.Config (SecurityParam, TopLevelConfig (..)) +import qualified Ouroboros.Consensus.HardFork.History.EraParams as HardFork +import Ouroboros.Consensus.Node.ProtocolInfo + (NumCoreNodes (NumCoreNodes)) +import Ouroboros.Consensus.NodeId (CoreNodeId (CoreNodeId), + NodeId (CoreId)) +import Ouroboros.Consensus.Protocol.BFT + (BftParams (BftParams, bftNumNodes, bftSecurityParam), + ConsensusConfig (BftConfig, bftParams, bftSignKey, bftVerKeys)) +import Test.Util.Orphans.IOLike () +import Test.Util.TestBlock (BlockConfig (TestBlockConfig), + CodecConfig (TestBlockCodecConfig), + StorageConfig (TestBlockStorageConfig), TestBlock) + +-- REVIEW: this has not been deliberately chosen +defaultCfg :: SecurityParam -> TopLevelConfig TestBlock +defaultCfg secParam = TopLevelConfig { + topLevelConfigProtocol = BftConfig { + bftParams = BftParams { + bftSecurityParam = secParam + , bftNumNodes = NumCoreNodes 2 + } + , bftSignKey = SignKeyMockDSIGN 0 + , bftVerKeys = Map.fromList [ + (CoreId (CoreNodeId 0), VerKeyMockDSIGN 0) + , (CoreId (CoreNodeId 1), VerKeyMockDSIGN 1) + ] + } + , topLevelConfigLedger = eraParams + , topLevelConfigBlock = TestBlockConfig numCoreNodes + , topLevelConfigCodec = TestBlockCodecConfig + , topLevelConfigStorage = TestBlockStorageConfig + } + where + -- REVIEW: Make it 1s or a parameter? + slotLength :: SlotLength + slotLength = slotLengthFromSec 20 + + eraParams :: HardFork.EraParams + eraParams = HardFork.defaultEraParams secParam slotLength + + numCoreNodes = NumCoreNodes 2 diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs new file mode 100644 index 0000000000..52f0ba022e --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs @@ -0,0 +1,136 @@ +{-# LANGUAGE LambdaCase #-} + +-- | Business logic of the SyncChain protocol handlers that operates +-- on the 'AdvertisedPoints' of a point schedule. +-- +-- These are separated from the scheduling related mechanics of the +-- ChainSync server mock that the peer simulator uses, in +-- "Test.Consensus.PeerSimulator.ScheduledChainSyncServer". +module Test.Consensus.PeerSimulator.Handlers ( + handlerFindIntersection + , handlerRequestNext + ) where + +import Control.Monad.Trans (lift) +import Control.Monad.Writer.Strict (MonadWriter (tell), + WriterT (runWriterT)) +import Data.Coerce (coerce) +import Data.Maybe (fromJust) +import Data.Monoid (First (..)) +import Ouroboros.Consensus.Block.Abstract (Point (..), getHeader) +import Ouroboros.Consensus.Util.Condense (Condense (..)) +import Ouroboros.Consensus.Util.IOLike (IOLike, STM, StrictTVar, + readTVar, writeTVar) +import Ouroboros.Network.AnchoredFragment (AnchoredFragment) +import qualified Ouroboros.Network.AnchoredFragment as AF +import Ouroboros.Network.Block (blockPoint, getTipPoint) +import qualified Test.Consensus.BlockTree as BT +import Test.Consensus.BlockTree (BlockTree) +import Test.Consensus.PeerSimulator.ScheduledChainSyncServer + (FindIntersect (..), + RequestNext (AwaitReply, RollBackward, RollForward)) +import Test.Consensus.PointSchedule (AdvertisedPoints (header, tip), + HeaderPoint (HeaderPoint), TipPoint (TipPoint)) +import Test.Util.Orphans.IOLike () +import Test.Util.TestBlock (TestBlock) + +-- | Find the first fragment contained in the first arg that starts at one of the given points. +intersectWith :: AnchoredFragment TestBlock -> [Point TestBlock] -> Maybe (Point TestBlock) +intersectWith fullFrag pts = + AF.anchorPoint . snd <$> getFirst (foldMap (First . AF.splitAfterPoint fullFrag) pts) + +-- | Handle a @MsgFindIntersect@ message. +-- +-- Extracts the fragment up to the current advertised tip from the block tree, +-- then searches for any of the client's points in it. +handlerFindIntersection :: + IOLike m => + StrictTVar m (Point TestBlock) -> + BlockTree TestBlock -> + AdvertisedPoints -> + [Point TestBlock] -> + STM m (FindIntersect, [String]) +handlerFindIntersection currentIntersection blockTree points clientPoints = do + let TipPoint tip' = tip points + tipPoint = getTipPoint tip' + fragment = fromJust $ BT.findFragment tipPoint blockTree + case intersectWith fragment clientPoints of + Nothing -> + pure (IntersectNotFound tip', []) + Just intersection -> do + writeTVar currentIntersection intersection + pure (IntersectFound intersection tip', []) + where + +-- | Handle a @MsgRequestNext@ message. +-- +-- Finds the potential path from the current intersection to the advertised header point for this turn, +-- which can have four distinct configurations for the anchor point and the path: +-- +-- - Anchor == intersection == HP +-- - HP after intersection == HP +-- - HP before intersection (special case for the point scheduler architecture) +-- - Anchor != intersection +handlerRequestNext :: + IOLike m => + StrictTVar m (Point TestBlock) -> + BlockTree TestBlock -> + AdvertisedPoints -> + STM m (Maybe RequestNext, [String]) +handlerRequestNext currentIntersection blockTree points = + runWriterT $ do + intersection <- lift $ readTVar currentIntersection + trace $ " last intersection is " ++ condense intersection + maybe noPathError analysePath (BT.findPath intersection headerPoint blockTree) + where + noPathError = error "serveHeader: intersection and and headerPoint should always be in the block tree" + + analysePath = \case + -- If the anchor is the intersection (the source of the path-finding) but + -- the fragment is empty, then the intersection is exactly our header + -- point and there is nothing to do. If additionally the header point is + -- also the tip point (because we served our whole chain, or we are + -- stalling as an adversarial behaviour), then we ask the client to wait; + -- otherwise we just do nothing. + (BT.PathAnchoredAtSource True, AF.Empty _) | getTipPoint tip' == headerPoint -> do + trace " chain has been fully served" + pure (Just AwaitReply) + (BT.PathAnchoredAtSource True, AF.Empty _) -> do + trace " intersection is exactly our header point" + pure Nothing + -- If the anchor is the intersection and the fragment is non-empty, then + -- we have something to serve. + (BT.PathAnchoredAtSource True, fragmentAhead@(next AF.:< _)) -> do + trace " intersection is before our header point" + trace $ " fragment ahead: " ++ condense fragmentAhead + lift $ writeTVar currentIntersection $ blockPoint next + pure $ Just (RollForward (getHeader next) (coerce (tip points))) + -- If the anchor is not the intersection but the fragment is empty, then + -- the intersection is further than the tip that we can serve. + (BT.PathAnchoredAtSource False, AF.Empty _) -> do + trace " intersection is further than our header point" + -- REVIEW: The following is a hack that allows the honest peer to not + -- get disconnected when it falls behind. Why does a peer doing that not + -- get disconnected from? + -- + -- We decided to hold off on making this work with timeouts, so we'll return + -- Nothing here for now. + -- The consequence of this is that a slow peer will just block until it reaches + -- the fork intersection in its schedule. + -- pure (Just AwaitReply) + pure Nothing + -- If the anchor is not the intersection and the fragment is non-empty, + -- then we require a rollback + (BT.PathAnchoredAtSource False, fragment) -> do + trace $ " we will require a rollback to" ++ condense (AF.anchorPoint fragment) + trace $ " fragment: " ++ condense fragment + let + point = AF.anchorPoint fragment + lift $ writeTVar currentIntersection point + pure $ Just (RollBackward point tip') + + HeaderPoint header' = header points + headerPoint = AF.castPoint $ blockPoint header' + TipPoint tip' = tip points + + trace = tell . pure diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Resources.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Resources.hs new file mode 100644 index 0000000000..6a7fdeee53 --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Resources.hs @@ -0,0 +1,168 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} + +-- | Data types and resource allocating constructors for the concurrency +-- primitives used by ChainSync and BlockFetch in the handlers that implement +-- the block tree analysis specific to our peer simulator. +module Test.Consensus.PeerSimulator.Resources ( + ChainSyncResources (..) + , PeerResources (..) + , SharedResources (..) + , makeChainSyncResources + , makePeerResources + , makePeersResources + ) where + +import Control.Concurrent.Class.MonadSTM.Strict (newEmptyTMVarIO, + takeTMVar) +import Control.Tracer (Tracer) +import Data.Foldable (toList) +import Data.List.NonEmpty (NonEmpty) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map +import Data.Traversable (for) +import Ouroboros.Consensus.Block (WithOrigin (Origin)) +import Ouroboros.Consensus.Block.Abstract (Header, Point (..)) +import Ouroboros.Consensus.Util.Condense (Condense (..)) +import Ouroboros.Consensus.Util.IOLike (IOLike, MonadSTM (STM), + StrictTMVar, StrictTVar, readTVar, uncheckedNewTVarM, + writeTVar) +import qualified Ouroboros.Network.AnchoredFragment as AF +import Ouroboros.Network.Block (Tip (..)) +import Ouroboros.Network.Protocol.ChainSync.Server + (ChainSyncServer (..)) +import Test.Consensus.BlockTree (BlockTree) +import Test.Consensus.PeerSimulator.Handlers +import Test.Consensus.PeerSimulator.ScheduledChainSyncServer +import Test.Consensus.PointSchedule +import Test.Util.Orphans.IOLike () +import Test.Util.TestBlock (TestBlock) + +-- | Resources used by both ChainSync and BlockFetch for a single peer. +data SharedResources m = + SharedResources { + -- | The name of the peer. + srPeerId :: PeerId, + + -- | The block tree in which the test is taking place. In combination to + -- 'csssCurrentIntersection' and the current point schedule tick, it allows + -- to define which blocks to serve to the client. + srBlockTree :: BlockTree TestBlock, + + -- | The currently active schedule point. + -- + -- This is 'Maybe' because we cannot wait for the initial state otherwise. + srCurrentState :: StrictTVar m (Maybe AdvertisedPoints), + + -- | The candidate fragment for a peer is shared by ChainSync, BlockFetch and the ChainDB. + srCandidateFragment :: StrictTVar m TestFragH, + + srTracer :: Tracer m String + } + +-- | The data used by the point scheduler to interact with the mocked protocol handler in +-- "Test.Consensus.PeerSimulator.ScheduledChainSyncServer". +data ChainSyncResources m = + ChainSyncResources { + -- | A mailbox of node states that is updated by the scheduler in the peer's active tick, + -- waking up the chain sync server. + csrNextState :: StrictTMVar m NodeState, + + -- | The current known intersection with the chain of the client. + csrCurrentIntersection :: StrictTVar m (Point TestBlock), + + -- | The final server passed to typed-protocols. + csrServer :: ChainSyncServer (Header TestBlock) (Point TestBlock) (Tip TestBlock) m () + } + +-- | The totality of resources used by a single peer in ChainSync and BlockFetch. +data PeerResources m = + PeerResources { + -- | Resources used by ChainSync and BlockFetch. + prShared :: SharedResources m, + + -- | Resources used by ChainSync only. + prChainSync :: ChainSyncResources m + } + +-- | Create 'ChainSyncServerHandlers' for our default implementation using 'AdvertisedPoints'. +makeChainSyncServerHandlers :: + IOLike m => + StrictTVar m (Point TestBlock) -> + BlockTree TestBlock -> + ChainSyncServerHandlers m AdvertisedPoints +makeChainSyncServerHandlers currentIntersection blockTree = + ChainSyncServerHandlers { + csshFindIntersection = handlerFindIntersection currentIntersection blockTree, + csshRequestNext = handlerRequestNext currentIntersection blockTree + } + +-- | Transaction that blocks until the next turn of the current peer, when the +-- scheduler puts the new state into the TMVar, and updates the TVar that is +-- read by all of the ChainSync and BlockFetch handlers. +-- +-- The ChainSync protocol handler mock is agnostic of our state type, +-- 'NodeState', so we convert it to 'Maybe'. +waitForNextState :: + IOLike m => + StrictTMVar m NodeState -> + StrictTVar m (Maybe AdvertisedPoints) -> + STM m (Maybe AdvertisedPoints) +waitForNextState nextState currentState = + takeTMVar nextState >>= \ newState -> do + let + a = case newState of + NodeOffline -> Nothing + NodeOnline tick -> Just tick + writeTVar currentState a + pure a + +-- | Create all the resources used exclusively by the ChainSync handlers, and +-- the ChainSync protocol server that uses the handlers to interface with the +-- typed-protocols engine. +makeChainSyncResources :: + IOLike m => + SharedResources m -> + m (ChainSyncResources m) +makeChainSyncResources SharedResources {srPeerId, srTracer, srBlockTree, srCurrentState} = do + csrNextState <- newEmptyTMVarIO + csrCurrentIntersection <- uncheckedNewTVarM $ AF.Point Origin + let + wait = waitForNextState csrNextState srCurrentState + handlers = makeChainSyncServerHandlers csrCurrentIntersection srBlockTree + csrServer = runScheduledChainSyncServer (condense srPeerId) wait (readTVar srCurrentState) srTracer handlers + pure ChainSyncResources {csrServer, csrNextState, csrCurrentIntersection} + +-- | Create all concurrency resources and the ChainSync protocol server used +-- for a single peer. +-- +-- A peer performs BlockFetch and ChainSync using a state of +-- type 'AdvertisedPoints' that is updated by a separate scheduler, waking up +-- the protocol handlers to process messages until the conditions of the new +-- state are satisfied. +makePeerResources :: + IOLike m => + Tracer m String -> + BlockTree TestBlock -> + PeerId -> + m (PeerResources m) +makePeerResources srTracer srBlockTree srPeerId = do + srCandidateFragment <- uncheckedNewTVarM $ AF.Empty AF.AnchorGenesis + srCurrentState <- uncheckedNewTVarM Nothing + let prShared = SharedResources {srTracer, srBlockTree, srPeerId, srCandidateFragment, srCurrentState} + prChainSync <- makeChainSyncResources prShared + pure PeerResources {prChainSync, prShared} + +-- | Create resources for all given peers operating on the given block tree. +makePeersResources :: + IOLike m => + Tracer m String -> + BlockTree TestBlock -> + NonEmpty PeerId -> + m (Map PeerId (PeerResources m)) +makePeersResources tracer blockTree peers = do + resources <- for peers $ \ peerId -> do + peerResources <- makePeerResources tracer blockTree peerId + pure (peerId, peerResources) + pure (Map.fromList $ toList resources) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs new file mode 100644 index 0000000000..890dc93610 --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs @@ -0,0 +1,311 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} + +module Test.Consensus.PeerSimulator.Run ( + ChainSyncException (..) + , SchedulerConfig (..) + , runPointSchedule + ) where + +import Control.Monad.Class.MonadAsync + (AsyncCancelled (AsyncCancelled)) +import Control.Monad.Class.MonadTime (MonadTime) +import Control.Monad.Class.MonadTimer.SI (MonadTimer) +import Control.Tracer (Tracer, nullTracer, traceWith) +import Data.Foldable (for_) +import Data.Functor (void) +import Data.List.NonEmpty (NonEmpty, nonEmpty) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map +import Data.Traversable (for) +import Ouroboros.Consensus.Config (TopLevelConfig (..)) +import qualified Ouroboros.Consensus.HardFork.History.EraParams as HardFork +import Ouroboros.Consensus.MiniProtocol.ChainSync.Client (ChainDbView, + Consensus, chainSyncClient, defaultChainDbView) +import Ouroboros.Consensus.Storage.ChainDB.API +import qualified Ouroboros.Consensus.Storage.ChainDB.API as ChainDB +import Ouroboros.Consensus.Storage.ChainDB.Impl + (ChainDbArgs (cdbTracer)) +import qualified Ouroboros.Consensus.Storage.ChainDB.Impl as ChainDB.Impl +import Ouroboros.Consensus.Util.Condense (Condense (..)) +import Ouroboros.Consensus.Util.IOLike (Exception (fromException), + IOLike, MonadCatch (try), MonadDelay (threadDelay), + MonadSTM (atomically, retry), MonadThrow (throwIO), + SomeException, StrictTVar, readTVar, readTVarIO, + tryPutTMVar, uncheckedNewTVarM, writeTVar) +import Ouroboros.Consensus.Util.ResourceRegistry +import Ouroboros.Network.Block (blockPoint) +import Ouroboros.Network.BlockFetch (FetchClientRegistry, + bracketSyncWithFetchClient, newFetchClientRegistry) +import Ouroboros.Network.Channel (createConnectedChannels) +import Ouroboros.Network.ControlMessage (ControlMessage (..), + ControlMessageSTM) +import Ouroboros.Network.Driver.Limits +import Ouroboros.Network.Protocol.ChainSync.ClientPipelined + (ChainSyncClientPipelined, chainSyncClientPeerPipelined) +import Ouroboros.Network.Protocol.ChainSync.Codec +import Ouroboros.Network.Protocol.ChainSync.PipelineDecision + (pipelineDecisionLowHighMark) +import Ouroboros.Network.Protocol.ChainSync.Server + (chainSyncServerPeer) +import qualified Test.Consensus.BlockTree as BT +import Test.Consensus.Genesis.Setup.GenChains (GenesisTest) +import Test.Consensus.Network.Driver.Limits.Extras +import qualified Test.Consensus.PeerSimulator.BlockFetch as PeerSimulator.BlockFetch +import Test.Consensus.PeerSimulator.Config +import Test.Consensus.PeerSimulator.Resources +import Test.Consensus.PeerSimulator.Trace +import qualified Test.Consensus.PointSchedule as PointSchedule +import Test.Consensus.PointSchedule (GenesisTest (GenesisTest), + Peer (Peer), PeerId, PointSchedule (PointSchedule), + TestFragH, Tick (Tick), pointSchedulePeers) +import Test.Ouroboros.Consensus.ChainGenerator.Params (Asc) +import Test.Util.ChainDB +import Test.Util.Orphans.IOLike () +import Test.Util.TestBlock (Header (..), TestBlock, testInitExtLedger) + +-- | Behavior config for the scheduler. +data SchedulerConfig = + SchedulerConfig { + -- | Whether to use timouts for the ChainSync protocol. + -- These apply when the client sends a MsgRequestNext and the server doesn't reply. + -- Because the point schedule cannot yet handle the case where a slow peer has a + -- header point that's behind the latest header that another peer has sent, we need + -- to be able to disable them. + enableTimeouts :: Bool + } + deriving (Show) + +basicChainSyncClient :: + IOLike m => + Tracer m String -> + TopLevelConfig TestBlock -> + ChainDbView m TestBlock -> + StrictTVar m TestFragH -> + Consensus ChainSyncClientPipelined TestBlock m +basicChainSyncClient tracer cfg chainDbView varCandidate = + chainSyncClient + (pipelineDecisionLowHighMark 10 20) + (mkChainSyncClientTracer tracer) + cfg + chainDbView + maxBound + (return Continue) + nullTracer + varCandidate + +-- | A record to associate an exception thrown by the ChainSync +-- thread with the peer that it was running for. +data ChainSyncException = ChainSyncException + { csePeerId :: PeerId + , cseException :: SomeException + } + deriving Show + +-- | Run a ChainSync protocol for one peer, consisting of a server and client. +-- +-- The connection uses timeouts based on the ASC. +-- +-- The client is synchronized with BlockFetch using the supplied 'FetchClientRegistry'. +-- +-- Execution is started asynchronously, returning an action that kills the thread, +-- to allow extraction of a potential exception. +startChainSyncConnectionThread :: + (IOLike m, MonadTimer m) => + ResourceRegistry m -> + Tracer m String -> + TopLevelConfig TestBlock -> + Asc -> + ChainDbView m TestBlock -> + FetchClientRegistry PeerId (Header TestBlock) TestBlock m -> + SharedResources m -> + ChainSyncResources m -> + SchedulerConfig -> + m (StrictTVar m (Maybe ChainSyncException)) +startChainSyncConnectionThread registry tracer cfg activeSlotCoefficient chainDbView fetchClientRegistry SharedResources {srPeerId, srCandidateFragment} ChainSyncResources {csrServer} SchedulerConfig {enableTimeouts} = do + let + slotLength = HardFork.eraSlotLength . topLevelConfigLedger $ cfg + timeouts | enableTimeouts = chainSyncTimeouts slotLength activeSlotCoefficient + | otherwise = chainSyncNoTimeouts + traceWith tracer $ "timeouts:" + traceWith tracer $ " canAwait = " ++ show (canAwaitTimeout timeouts) + traceWith tracer $ " intersect = " ++ show (intersectTimeout timeouts) + traceWith tracer $ " mustReply = " ++ show (mustReplyTimeout timeouts) + chainSyncException <- uncheckedNewTVarM Nothing + _ <- forkLinkedThread registry ("ChainSyncClient" <> condense srPeerId) $ + bracketSyncWithFetchClient fetchClientRegistry srPeerId $ do + res <- try $ runConnectedPeersPipelinedWithLimits + createConnectedChannels + nullTracer + codecChainSyncId + chainSyncNoSizeLimits + (timeLimitsChainSync timeouts) + (chainSyncClientPeerPipelined (basicChainSyncClient tracer cfg chainDbView srCandidateFragment)) + (chainSyncServerPeer csrServer) + case res of + Left exn -> do + atomically $ writeTVar chainSyncException $ Just $ ChainSyncException srPeerId exn + case fromException exn of + Just (ExceededSizeLimit _) -> + traceUnitWith tracer ("ChainSyncClient " ++ condense srPeerId) "Terminating because of size limit exceeded." + Just (ExceededTimeLimit _) -> + traceUnitWith tracer ("ChainSyncClient " ++ condense srPeerId) "Terminating because of time limit exceeded." + Nothing -> + pure () + throwIO exn + Right res' -> pure res' + pure chainSyncException + +-- | Start the BlockFetch client, using the supplied 'FetchClientRegistry' to +-- register it for synchronization with the ChainSync client. +startBlockFetchConnectionThread :: + (IOLike m, MonadTime m) => + ResourceRegistry m -> + FetchClientRegistry PeerId (Header TestBlock) TestBlock m -> + ControlMessageSTM m -> + SharedResources m -> + m () +startBlockFetchConnectionThread registry fetchClientRegistry controlMsgSTM SharedResources {srPeerId, srBlockTree, srCurrentState} = + void $ forkLinkedThread registry ("BlockFetchClient" <> condense srPeerId) $ + PeerSimulator.BlockFetch.runBlockFetchClient srPeerId fetchClientRegistry controlMsgSTM getCurrentChain + where + getCurrentChain = atomically $ do + nodeState <- readTVar srCurrentState + case nodeState of + Nothing -> retry + Just aps -> do + let PointSchedule.BlockPoint b = PointSchedule.block aps + case BT.findFragment (blockPoint b) srBlockTree of + Just f -> pure f + Nothing -> error "block tip is not in the block tree" + +-- | The 'Tick' contains a state update for a specific peer. +-- If the peer has not terminated by protocol rules, this will update its TMVar +-- with the new state, thereby unblocking the handler that's currently waiting +-- for new instructions. +dispatchTick :: + IOLike m => + Tracer m String -> + Map PeerId (PeerResources m) -> + Tick -> + m () +dispatchTick tracer peers Tick {active = Peer pid state} = + case peers Map.!? pid of + Just PeerResources {prChainSync = ChainSyncResources {csrNextState}} -> do + trace $ "Writing state " ++ condense state + atomically (tryPutTMVar csrNextState state) >>= \case + True -> trace $ "Waiting for full resolution of " ++ condense pid ++ "'s tick..." + False -> trace $ "Client for " ++ condense pid ++ " has ceased operation." + threadDelay 0.100 + trace $ condense pid ++ "'s tick is now done." + Nothing -> error "“The impossible happened,” as GHC would say." + where + trace = traceUnitWith tracer "Scheduler" + +-- | Iterate over a 'PointSchedule', sending each tick to the associated peer in turn, +-- giving each peer a chunk of computation time, sequentially, until it satisfies the +-- conditions given by the tick. +-- This usually means for the ChainSync server to have sent the target header to the +-- client. +runScheduler :: + IOLike m => + Tracer m String -> + PointSchedule -> + Map PeerId (PeerResources m) -> + m () +runScheduler tracer (PointSchedule ps) peers = do + traceWith tracer "Schedule is:" + for_ ps $ \tick -> traceWith tracer $ " " ++ condense tick + traceWith tracer "--------------------------------------------------------------------------------" + traceWith tracer "» Time says “Let there be”" + traceWith tracer "» every moment and instantly" + traceWith tracer "» there is space and the radiance" + traceWith tracer "» of each bright galaxy." + traceWith tracer "--------------------------------------------------------------------------------" + for_ ps (dispatchTick tracer peers) + traceWith tracer "--------------------------------------------------------------------------------" + traceWith tracer "» A Clock stopped -" + traceWith tracer "» Not the Mantel's -" + traceWith tracer "» Geneva's farthest skill" + traceWith tracer "» Can't put the puppet bowing" + traceWith tracer "» That just now dangled still -" + +-- | Construct STM resources, set up ChainSync and BlockFetch threads, and +-- send all ticks in a 'PointSchedule' to all given peers in turn. +runPointSchedule :: + forall m. + (IOLike m, MonadTime m, MonadTimer m) => + SchedulerConfig -> + GenesisTest -> + PointSchedule -> + Tracer m String -> + m (Either (NonEmpty ChainSyncException) TestFragH) +runPointSchedule schedulerConfig GenesisTest {gtSecurityParam = k, gtHonestAsc = asc, gtBlockTree} pointSchedule tracer = + withRegistry $ \registry -> do + resources <- makePeersResources tracer gtBlockTree (pointSchedulePeers pointSchedule) + let candidates = srCandidateFragment . prShared <$> resources + traceWith tracer $ "Security param k = " ++ show k + chainDb <- mkChainDb tracer candidates config registry + fetchClientRegistry <- newFetchClientRegistry + let chainDbView = defaultChainDbView chainDb + chainSyncRess <- for resources $ \PeerResources {prShared, prChainSync} -> do + chainSyncRes <- startChainSyncConnectionThread registry tracer config asc chainDbView fetchClientRegistry prShared prChainSync schedulerConfig + PeerSimulator.BlockFetch.startKeepAliveThread registry fetchClientRegistry (srPeerId prShared) + pure chainSyncRes + for_ resources $ \PeerResources {prShared} -> + startBlockFetchConnectionThread registry fetchClientRegistry (pure Continue) prShared + -- The block fetch logic needs to be started after the block fetch clients + -- otherwise, an internal assertion fails because getCandidates yields more + -- peer fragments than registered clients. + let getCandidates = traverse readTVar candidates + PeerSimulator.BlockFetch.startBlockFetchLogic registry chainDb fetchClientRegistry getCandidates + runScheduler tracer pointSchedule resources + chainSyncExceptions <- collectExceptions (Map.elems chainSyncRess) + b <- atomically $ ChainDB.getCurrentChain chainDb + pure $ maybe (Right b) Left chainSyncExceptions + where + config = defaultCfg k + + collectExceptions :: [StrictTVar m (Maybe ChainSyncException)] -> m (Maybe (NonEmpty ChainSyncException)) + collectExceptions vars = do + res <- mapM readTVarIO vars + pure $ nonEmpty [ e | Just e <- res, not (isAsyncCancelled e) ] + + isAsyncCancelled :: ChainSyncException -> Bool + isAsyncCancelled e = case fromException $ cseException e of + Just AsyncCancelled -> True + _ -> False + +-- | Create a ChainDB and start a BlockRunner that operate on the peers' +-- candidate fragments. +mkChainDb :: + IOLike m => + Tracer m String -> + Map PeerId (StrictTVar m TestFragH) -> + TopLevelConfig TestBlock -> + ResourceRegistry m -> + m (ChainDB m TestBlock) +mkChainDb tracer _candidateVars nodeCfg registry = do + chainDbArgs <- do + mcdbNodeDBs <- emptyNodeDBs + pure $ ( + fromMinimalChainDbArgs MinimalChainDbArgs { + mcdbTopLevelConfig = nodeCfg + , mcdbChunkInfo = mkTestChunkInfo nodeCfg + , mcdbInitLedger = testInitExtLedger + , mcdbRegistry = registry + , mcdbNodeDBs + } + ) { + cdbTracer = mkCdbTracer tracer + } + (_, (chainDB, ChainDB.Impl.Internal{intAddBlockRunner})) <- + allocate + registry + (\_ -> ChainDB.Impl.openDBInternal chainDbArgs False) + (ChainDB.closeDB . fst) + _ <- forkLinkedThread registry "AddBlockRunner" intAddBlockRunner + pure chainDB diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/ScheduledChainSyncServer.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/ScheduledChainSyncServer.hs new file mode 100644 index 0000000000..66e4562777 --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/ScheduledChainSyncServer.hs @@ -0,0 +1,214 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} + +-- | A ChainSync protocol server that allows external scheduling of its +-- operations, while deferring the implementation of the message handler +-- logic to a simplified, abstract interface provided as a parameter. +module Test.Consensus.PeerSimulator.ScheduledChainSyncServer ( + ChainSyncServerHandlers (..) + , FindIntersect (..) + , RequestNext (..) + , ScheduledChainSyncServer (..) + , runScheduledChainSyncServer + ) where + +import Control.Tracer (Tracer (Tracer), traceWith) +import Data.Foldable (traverse_) +import Ouroboros.Consensus.Block.Abstract (Point (..)) +import Ouroboros.Consensus.Util.Condense (Condense (..)) +import Ouroboros.Consensus.Util.IOLike (IOLike, MonadSTM (STM), + atomically) +import Ouroboros.Network.Block (Tip (..)) +import Ouroboros.Network.Protocol.ChainSync.Server + (ChainSyncServer (..), + ServerStIdle (ServerStIdle, recvMsgDoneClient, recvMsgFindIntersect, recvMsgRequestNext), + ServerStIntersect (SendMsgIntersectFound, SendMsgIntersectNotFound), + ServerStNext (SendMsgRollBackward, SendMsgRollForward)) +import Test.Consensus.PeerSimulator.Trace (traceUnitWith) +import Test.Util.TestBlock (Header (..), TestBlock) + +-- | Pure representation of the messages produced by the handler for the @StNext@ +-- protocol state of a ChainSync server. +data RequestNext = + RollForward (Header TestBlock) (Tip TestBlock) + | + RollBackward (Point TestBlock) (Tip TestBlock) + | + AwaitReply + deriving (Eq, Show) + +-- | Pure representation of the messages produced by the handler for the @StIntersect@ +-- protocol state of a ChainSync server. +data FindIntersect = + IntersectFound (Point TestBlock) (Tip TestBlock) + | + IntersectNotFound (Tip TestBlock) + deriving (Eq, Show) + +-- | Handlers for the request a ChainSync server might receive from a client. +-- These take an abstract argument that corresponds to the state of a point +-- schedule tick and return the simplified protocol message types. +-- +-- See 'runHandlerWithTrace' for the meaning of @[String]@. +data ChainSyncServerHandlers m a = + ChainSyncServerHandlers { + csshRequestNext :: a -> STM m (Maybe RequestNext, [String]), + csshFindIntersection :: a -> [Point TestBlock] -> STM m (FindIntersect, [String]) + } + +-- | Resources used by a ChainSync server mock. +data ScheduledChainSyncServer m a = + ScheduledChainSyncServer { + scssName :: String, + scssCurrentState :: STM m (Maybe a), + scssAwaitNextState :: STM m (Maybe a), + scssHandlers :: ChainSyncServerHandlers m a, + scssTracer :: Tracer m String + } + +-- | Block until the peer simulator has updated the concurrency primitive that +-- indicates that it's this peer's server's turn in the point schedule. +-- If the new state is 'Nothing', the point schedule has declared this peer as +-- offline for the current tick, so it will not resume operation and wait for +-- the next update. +awaitNextState :: + IOLike m => + ScheduledChainSyncServer m a -> + m a +awaitNextState server@ScheduledChainSyncServer{scssAwaitNextState} = do + atomically scssAwaitNextState >>= \case + Nothing -> awaitNextState server + Just resource -> pure resource + +-- | Fetch the current state from the STM action, and if it is 'Nothing', +-- wait for the next tick to be triggered in 'awaitNextState'. +-- +-- Since processing of a tick always ends when the RequestNext handler finishes +-- after serving the last header, this function is only relevant for the +-- initial state update. +ensureCurrentState :: + IOLike m => + ScheduledChainSyncServer m a -> + m a +ensureCurrentState server@ScheduledChainSyncServer{scssCurrentState} = + atomically scssCurrentState >>= \case + Nothing -> awaitNextState server + Just resource -> pure resource + +-- | Handler functions are STM actions for the usual race condition reasons, +-- which means that they cannot emit trace messages. +-- +-- For that reason, we allow them to return their messages alongside the +-- protocol result and emit them here. +runHandlerWithTrace :: + IOLike m => + Tracer m String -> + STM m (a, [String]) -> + m a +runHandlerWithTrace tracer handler = do + (result, handlerMessages) <- atomically handler + traverse_ (traceWith tracer) handlerMessages + pure result + +-- | Declare a mock ChainSync protocol server in its typed-protocols encoding +-- that halts and resumes operation in response to an external scheduler, +-- signalling via a blocking STM action that is sequenced by calling +-- 'awaitNextState' in 'recvMsgRequestNext' after the current state has been +-- fully processed, which is indicated by the handler for this message. +-- +-- Handlers are supplied as a record of STM callbacks ('ChainSyncServerHandlers') +-- by the caller. +-- +-- This architecture allows the server's behavior to be defined with a simple +-- interface separated from the scheduling and protocol plumbing infrastructure. +scheduledChainSyncServer :: + Condense a => + IOLike m => + ScheduledChainSyncServer m a -> + ChainSyncServer (Header TestBlock) (Point TestBlock) (Tip TestBlock) m () +scheduledChainSyncServer server@ScheduledChainSyncServer {scssHandlers, scssTracer, scssName} = + go + where + ChainSyncServerHandlers {csshRequestNext, csshFindIntersection} = scssHandlers + + go = + ChainSyncServer $ pure ServerStIdle { + recvMsgRequestNext + , recvMsgFindIntersect + , recvMsgDoneClient + } + + recvMsgRequestNext = do + currentState <- ensureCurrentState server + trace "handling MsgRequestNext" + trace $ " state is " ++ condense currentState + runHandlerWithTrace requestNextTracer (csshRequestNext currentState) >>= \case + Just (RollForward header tip) -> do + trace $ " gotta serve " ++ condense header + trace $ " tip is " ++ condense tip + trace "done handling MsgRequestNext" + pure $ Left $ SendMsgRollForward header tip go + Just (RollBackward point tip) -> do + trace "done handling MsgRequestNext" + pure $ Left $ SendMsgRollBackward point tip go + Just AwaitReply -> do + trace "done handling MsgRequestNext" + pure $ Right $ do -- beginning of the continuation + restart >>= \case + -- If we get 'Right', then we still do not have anything to serve + -- and we loop; what 'Right' contains is the continuation starting + -- at 'do' above; by unwrapping the 'Right', we do not send + -- another AwaitReply message (which Typed Protocols does not + -- allow anyway). + Right cont -> cont + Left msg -> pure msg + Nothing -> do + trace " cannot serve at this point; waiting for node state and starting again" + restart + where + -- Yield control back to the scheduler, then wait for the next state and + -- continue processing the client's current 'MsgRequestNext'. + restart = awaitNextState server *> recvMsgRequestNext + + recvMsgFindIntersect pts = do + currentState <- ensureCurrentState server + trace "handling MsgFindIntersect" + runHandlerWithTrace findIntersectTracer (csshFindIntersection currentState pts) >>= \case + IntersectNotFound tip -> do + trace " no intersection found" + trace "done handling MsgFindIntersect" + pure $ SendMsgIntersectNotFound tip go + IntersectFound intersection tip -> do + trace $ " intersection found: " ++ condense intersection + trace "done handling MsgFindIntersect" + pure $ SendMsgIntersectFound intersection tip go + + recvMsgDoneClient = do + trace "received MsgDoneClient" + pure () + + trace = traceWith mainTracer + requestNextTracer = Tracer $ traceUnitWith scssTracer ("RequestNext handler for " ++ scssName) + findIntersectTracer = Tracer $ traceUnitWith scssTracer ("FindIntersect handler for " ++ scssName) + mainTracer = Tracer $ traceUnitWith scssTracer ("ScheduledChainSyncServer " ++ scssName) + +-- | Construct a ChainSync server for the peer simulator. +-- +-- See 'scheduledChainSyncServer'. +runScheduledChainSyncServer :: + Condense a => + IOLike m => + String -> + STM m (Maybe a) -> + STM m (Maybe a) -> + Tracer m String -> + ChainSyncServerHandlers m a -> + ChainSyncServer (Header TestBlock) (Point TestBlock) (Tip TestBlock) m () +runScheduledChainSyncServer scssName scssAwaitNextState scssCurrentState scssTracer scssHandlers = + scheduledChainSyncServer ScheduledChainSyncServer { + scssName, + scssAwaitNextState, + scssCurrentState, + scssTracer, + scssHandlers + } diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs new file mode 100644 index 0000000000..de3a333b4a --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs @@ -0,0 +1,71 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} + +-- | Helpers for tracing used by the peer simulator. +module Test.Consensus.PeerSimulator.Trace ( + mkCdbTracer + , mkChainSyncClientTracer + , traceUnitWith + ) where + +import Control.Tracer (Tracer (Tracer), traceWith) +import Data.Time.Clock (diffTimeToPicoseconds) +import Ouroboros.Consensus.MiniProtocol.ChainSync.Client + (TraceChainSyncClientEvent (..)) +import qualified Ouroboros.Consensus.Storage.ChainDB.Impl as ChainDB.Impl +import Ouroboros.Consensus.Storage.ChainDB.Impl.Types + (SelectionChangedInfo (..), TraceAddBlockEvent (..)) +import Ouroboros.Consensus.Util.Condense (Condense (..)) +import Ouroboros.Consensus.Util.IOLike (IOLike, MonadMonotonicTime, + Time (Time), getMonotonicTime) +import Test.Util.TestBlock (TestBlock) +import Text.Printf (printf) + +mkCdbTracer :: + IOLike m => + Tracer m String -> + Tracer m (ChainDB.Impl.TraceEvent TestBlock) +mkCdbTracer tracer = + Tracer $ \case + ChainDB.Impl.TraceAddBlockEvent event -> + case event of + AddedToCurrentChain _ SelectionChangedInfo {newTipPoint} _ _ -> do + trace "Added to current chain" + trace $ "New tip: " ++ condense newTipPoint + SwitchedToAFork _ SelectionChangedInfo {newTipPoint} _ newFragment -> do + trace "Switched to a fork" + trace $ "New tip: " ++ condense newTipPoint + trace $ "New fragment: " ++ condense newFragment + _ -> pure () + _ -> pure () + where + trace = traceUnitWith tracer "ChainDB" + +mkChainSyncClientTracer :: + IOLike m => + Tracer m String -> + Tracer m (TraceChainSyncClientEvent TestBlock) +mkChainSyncClientTracer tracer = + Tracer $ \case + TraceRolledBack point -> + trace $ "Rolled back to: " ++ condense point + TraceFoundIntersection point _ourTip _theirTip -> + trace $ "Found intersection at: " ++ condense point + _ -> pure () + where + trace = traceUnitWith tracer "ChainSyncClient" + +-- | Trace using the given tracer, printing the current time (typically the time +-- of the simulation) and the unit name. +traceUnitWith :: MonadMonotonicTime m => Tracer m String -> String -> String -> m () +traceUnitWith tracer unit msg = do + time <- getMonotonicTime + traceWith tracer $ printf "%s %s | %s" (showTime time) unit msg + where + showTime :: Time -> String + showTime (Time time) = + let ps = diffTimeToPicoseconds time + milliseconds = (ps `div` 1000000000) `mod` 1000 + seconds = (ps `div` 1000000000000) `rem` 60 + minutes = (ps `div` 1000000000000) `quot` 60 + in printf "%02d:%02d.%03d" minutes seconds milliseconds diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PointSchedule.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PointSchedule.hs new file mode 100644 index 0000000000..250046158f --- /dev/null +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PointSchedule.hs @@ -0,0 +1,565 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Data types and generators that convert a 'BlockTree' to a 'PointSchedule'. +-- +-- Point schedules can have arbitrary configurations that model different behaviors +-- we want to use for tests. +-- +-- Each generator takes a set of 'AnchoredFragment's corresponding to the tested peers' +-- chains, and converts them to a 'PeerSchedule' consisting of a sequence of states +-- ('AdvertisedPoints'), each of which is associated with a single peer. +-- +-- The generated 'PeerSchedule' is transformed into a 'PointSchedule' that adds the current +-- states of the other peers to each entry (as a 'Tick'). +-- +-- When a schedule is executed in a test, each tick is processed in order. +-- The peer associated with the current tick is considered "active", which means that +-- its ChainSync server is allowed to continue processing messages, while all the other +-- peers' servers suspend operation by blocking on a concurrency primitive. +-- The state in the current tick determines the actions that the peer is allowed to perform, +-- and once it fulfills the state's criteria, it yields control back to the scheduler, +-- who then activates the next tick's peer. +-- +-- /Note/: At the moment this implementation is experimental. +module Test.Consensus.PointSchedule ( + AdvertisedPoints (..) + , BlockPoint (..) + , GenesisTest (..) + , GenesisWindow (..) + , HeaderPoint (..) + , NodeState (..) + , Peer (..) + , PeerId (..) + , PointSchedule (..) + , ScheduleType (..) + , TestFrag + , TestFragH + , Tick (..) + , TipPoint (..) + , genSchedule + , onlyHonestWithMintingPointSchedule + , pointSchedulePeers + ) where + +import Data.Foldable (toList) +import Data.Hashable (Hashable) +import Data.List (mapAccumL, transpose) +import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map +import Data.String (IsString (fromString)) +import Data.Word (Word64) +import GHC.Generics (Generic) +import Ouroboros.Consensus.Block.Abstract (HasHeader, getHeader) +import Ouroboros.Consensus.Protocol.Abstract (SecurityParam) +import Ouroboros.Consensus.Util.Condense (Condense (condense)) +import Ouroboros.Network.AnchoredFragment (AnchoredFragment, + AnchoredSeq (Empty, (:>)), anchorFromBlock) +import Ouroboros.Network.Block (SlotNo, Tip (Tip, TipGenesis), + blockNo, blockSlot, getTipSlotNo, tipFromHeader) +import Ouroboros.Network.Point (WithOrigin (At)) +import Test.Consensus.BlockTree (BlockTree (..), BlockTreeBranch (..)) +import Test.Ouroboros.Consensus.ChainGenerator.Params (Asc) +import Test.Util.TestBlock (Header (TestHeader), TestBlock) + +---------------------------------------------------------------------------------------------------- +-- Data types +---------------------------------------------------------------------------------------------------- + +type TestFrag = AnchoredFragment TestBlock + +type TestFragH = AnchoredFragment (Header TestBlock) + +-- | The current tip that a ChainSync server should advertise to the client in +-- a tick. +newtype TipPoint = + TipPoint (Tip TestBlock) + deriving (Eq, Show) + +instance Condense TipPoint where + condense (TipPoint TipGenesis) = "genesis" + condense (TipPoint (Tip slot _ bno)) = + "B:" <> condense bno <> ",S:" <> condense slot + +-- | The latest header that should be sent to the client by the ChainSync server +-- in a tick. +newtype HeaderPoint = + HeaderPoint (Header TestBlock) + deriving (Eq, Show) + +instance Condense HeaderPoint where + condense (HeaderPoint (TestHeader b)) = + "B:" <> condense (blockNo b) <> ",S:" <> condense (blockSlot b) + +-- | The latest block that should be sent to the client by the BlockFetch server +-- in a tick. +newtype BlockPoint = + BlockPoint TestBlock + deriving (Eq, Show) + +instance Condense BlockPoint where + condense (BlockPoint b) = + "B:" <> condense (blockNo b) <> ",S:" <> condense (blockSlot b) + +-- | The set of parameters that define the state that a peer should reach when it receives control +-- by the scheduler in a single tick. +-- +-- REVIEW: I find this rather poorly named. If it is really what is advertised +-- then isn't it weird to have the fragment in it? If it is the whole internal +-- state of the (online) node, then maybe we can call it that? +data AdvertisedPoints = + AdvertisedPoints { + tip :: TipPoint, + header :: HeaderPoint, + block :: BlockPoint + } + deriving (Eq, Show) + +instance Condense AdvertisedPoints where + condense AdvertisedPoints {tip, header, block} = + "TP " ++ condense tip ++ + " | HP " ++ condense header ++ + " | BP " ++ condense block + +-- | The state of a peer in a single tick. +-- +-- At the moment, this is only used to encode the fact that a peer does not have a current state +-- before it has been active for the first time. +-- +-- REVIEW: Is that necessary/useful? +data NodeState = + -- | The peer is online and advertise the given points. + NodeOnline AdvertisedPoints + | + -- | The peer should not respond to messages. + NodeOffline + deriving (Eq, Show) + +instance Condense NodeState where + condense = \case + NodeOnline points -> condense points + NodeOffline -> "*chrrrk* " + +-- | Identifier used to index maps and specify which peer is active during a tick. +data PeerId = + HonestPeer + | + PeerId String + deriving (Eq, Generic, Show, Ord) + +instance IsString PeerId where + fromString "honest" = HonestPeer + fromString i = PeerId i + +instance Condense PeerId where + condense = \case + HonestPeer -> "honest" + PeerId name -> name + +instance Hashable PeerId + +-- | General-purpose functor associated with a peer. +data Peer a = + Peer { + name :: PeerId, + value :: a + } + deriving (Eq, Show) + +instance Functor Peer where + fmap f Peer {name, value} = Peer {name, value = f value} + +instance Condense a => Condense (Peer a) where + condense Peer {name, value} = condense name ++ ": " ++ condense value + +-- | General-purpose functor for a set of peers. +-- +-- REVIEW: There is a duplicate entry for the honest peer, here. We should +-- probably either have only the 'Map' or have the keys of the map be 'String'? +-- +-- Alternatively, we could just have 'newtype PeerId = PeerId String' with an +-- alias for 'HonestPeer = PeerId "honest"'? +data Peers a = + Peers { + honest :: Peer a, + others :: Map PeerId (Peer a) + } + deriving (Eq, Show) + +instance Functor Peers where + fmap f Peers {honest, others} = Peers {honest = f <$> honest, others = fmap f <$> others} + +-- | Intermediate type that contains the states for only the active peers. +newtype PeerSchedule = + PeerSchedule [Peer NodeState] + deriving (Eq, Show) + +-- | A tick is an entry in a 'PointSchedule', containing the node states for all peers +-- as well as a designated peer that should be processing messages during this tick. +-- +-- REVIEW: What is the purpose of having the other peers as well in a +-- 'TickState'? +data Tick = + Tick { + active :: Peer NodeState, + peers :: Peers NodeState + } + deriving (Eq, Show) + +instance Condense Tick where + condense Tick {active} = condense active + +-- | A point schedule is a series of states for a set of peers. +-- +-- Each state defines which parts of the peer's chain are supposed to be served in the +-- given tick. +-- Each tick gives agency to only a single peer, which should process messages regularly +-- until the given state is reached, while the other peers block. +newtype PointSchedule = + PointSchedule {ticks :: NonEmpty Tick} + deriving (Eq, Show) + +instance Condense PointSchedule where + condense (PointSchedule ticks) = unlines (condense <$> toList ticks) + +---------------------------------------------------------------------------------------------------- +-- Accessors +---------------------------------------------------------------------------------------------------- + +-- | Extract all 'PeerId's. +getPeerIds :: Peers a -> NonEmpty PeerId +getPeerIds peers = HonestPeer :| Map.keys (others peers) + +-- | Extract the trunk and all the branches from the 'BlockTree' and store them in +-- an honest 'Peer' and several adversarial ones, respectively. +blockTreePeers :: BlockTree TestBlock -> Peers TestFrag +blockTreePeers BlockTree {btTrunk, btBranches} = + Peers { + honest = Peer HonestPeer btTrunk, + others = Map.fromList (branches btBranches) + } + where + branches = \case + [b] -> [peer "adversary" b] + bs -> uncurry branch <$> zip [1 :: Int ..] bs + + branch num = + peer (PeerId ("adversary " <> show num)) + + peer pid BlockTreeBranch {btbFull} = (pid, Peer pid btbFull) + +-- | Get the names of the peers involved in this point schedule. +-- This is the main motivation for requiring the point schedule to be +-- nonempty, so we don't have to carry around another value for the +-- 'PeerId's. +pointSchedulePeers :: PointSchedule -> NonEmpty PeerId +pointSchedulePeers PointSchedule{ticks = Tick {peers} :| _} = + getPeerIds peers + +---------------------------------------------------------------------------------------------------- +-- Conversion to 'PointSchedule' +---------------------------------------------------------------------------------------------------- + +-- | Ensure that a 'PointSchedule' isn't empty. +pointSchedule :: [Tick] -> Maybe PointSchedule +pointSchedule ticks = PointSchedule <$> nonEmpty ticks + +-- | Create the final 'PointSchedule' from a 'PeerSchedule', which consists of adding the inactive +-- peers' states to each tick. +-- +-- - Initialize all peers to 'NodeOffline'. +-- +-- - Fold over the list of active peer states in the 'PeerSchedule'. +-- +-- - In a fold 'step', update the active peer's state in the accumulator (in @updatePeer@), then +-- emit a 'Tick' with both the active peer's state and the accumulator in it. +-- +-- - 'mapAccumL' allows the step function to produce a new accumulator as well as a result list +-- element, so its final result is the accumulator after the last step as well as each step's +-- 'Tick' as a new list. +-- We discard the final accumulator and pass the new list of 'Tick's to 'pointSchedule', which +-- ensures that the schedule is nonempty, and returns 'Nothing' otherwise. +peer2Point :: Peers TestFrag -> PeerSchedule -> Maybe PointSchedule +peer2Point ps (PeerSchedule n) = + pointSchedule (snd (mapAccumL step initial n)) + where + + initial :: Peers NodeState + initial = NodeOffline <$ ps + + step :: Peers NodeState -> Peer NodeState -> (Peers NodeState, Tick) + step z active = + (new, Tick active new) + where + new = updatePeer z active + + updatePeer :: Peers a -> Peer a -> Peers a + updatePeer Peers {honest, others} active = + case name active of + HonestPeer -> Peers {honest = active, others} + name -> Peers {honest, others = Map.insert name active others} + +---------------------------------------------------------------------------------------------------- +-- Folding functions +---------------------------------------------------------------------------------------------------- + +-- | Fold a 'Peers' by applying the second argument to the honest 'Peer' as the initial +-- accumulator and applying the first argument to the accumulator and each 'Peer' in the +-- 'others' 'Map'. +foldHPeers :: (b -> Peer a -> b) -> (Peer a -> b) -> Peers a -> b +foldHPeers adv hon Peers {honest, others} = + Map.foldl' adv (hon honest) others + +-- | Combine two 'Peers' by creating tuples of the two honest 'Peer's and of each pair +-- of 'others' with the same 'PeerId', dropping any 'Peer' that is present in only one +-- of the 'Map's. +zipPeers :: Peers a -> Peers b -> Peers (a, b) +zipPeers a b = + Peers { + honest = Peer HonestPeer (value (honest a), value (honest b)), + others = Map.intersectionWith zp (others a) (others b) + } + where + zp p1 p2 = Peer (name p1) (value p1, value p2) + +type PSTrans = PeerId -> TestFrag -> PeerSchedule -> PeerSchedule + +-- | Generate a 'PeerSchedule' from a set of fragments and a set of transformations. +-- +-- The schedule is initialized by applying the transformation for the honest peer to the honest +-- fragment. +-- Then, it folds over all adversarial peers and applies each peer's transformation to the +-- accumulator and the peer's fragment. +foldGenPeers :: + Peers TestFrag -> + Peers PSTrans -> + PeerSchedule +foldGenPeers frags gen = + foldHPeers apA apH zp + where + zp = zipPeers frags gen + + apH :: Peer (TestFrag, PSTrans) -> PeerSchedule + apH (Peer i (frag, f)) = f i frag (PeerSchedule []) + + apA z (Peer i (frag, f)) = f i frag z + +---------------------------------------------------------------------------------------------------- +-- Schedule generators +---------------------------------------------------------------------------------------------------- + +-- | Create a peer schedule by serving one header in each tick. +banalStates :: TestFrag -> [NodeState] +banalStates (Empty _) = [] +banalStates frag@(_ :> tipBlock) = + spin [] frag + where + spin z (Empty _) = z + spin z (pre :> block) = + let header = HeaderPoint $ getHeader block + in spin + (NodeOnline AdvertisedPoints {tip, header, block = BlockPoint block} : z) + pre + tip = TipPoint $ tipFromHeader tipBlock + +-- | Generate a point schedule from a set of peer schedules by taking one element from each peer in +-- turn. +-- +-- Implemented by concatenating the peers' schedules and transposing the result. +-- +-- REVIEW: I see the point of this point schedule as an exercice to manipulate +-- them but I otherwise find it rather useless. +balanced :: + Peers [NodeState] -> + Maybe PointSchedule +balanced states = + pointSchedule (snd (mapAccumL step initial activeSeq)) + where + step :: Tick -> Peer NodeState -> (Tick, Tick) + step Tick {peers} active = + let next = Tick {active, peers = updatePeer peers active} + in (next, next) + + updatePeer :: Peers a -> Peer a -> Peers a + updatePeer Peers {honest, others} active = + case name active of + HonestPeer -> Peers {honest = active, others} + name -> Peers {honest, others = Map.insert name active others} + + -- Sequence containing the first state of all the nodes in order, then the + -- second in order, etc. + activeSeq = concat $ transpose $ seqPeer (honest states) : (seqPeer <$> Map.elems (others states)) + + seqPeer :: Peer [a] -> [Peer a] + seqPeer Peer {name, value} = + Peer name <$> value + + -- Initial state where all the peers are offline. + initial = Tick {active = initialH, peers = Peers initialH ((NodeOffline <$) <$> others states)} + initialH = Peer HonestPeer NodeOffline + +-- | Generate a point schedule that servers a single header in each tick for each peer in turn. +banalPointSchedule :: + BlockTree TestBlock -> + Maybe PointSchedule +banalPointSchedule blockTree = + balanced (banalStates <$> blockTreePeers blockTree) + +-- | Generate a point schedule for the scenario in which adversaries send blocks much faster +-- than the honest node. +-- +-- This is intended to test the Limit on Eagerness, which prevents the selection from advancing +-- far enough into a fork that the immutable tip moves into the fork as well (i.e. more than k +-- blocks). +-- +-- The LoE is only resolved when all peers with forks at that block have been disconnected from, +-- in particular due to a decision based on the Genesis density criterion. +-- +-- This is implemented by initializing the schedule to contain only the honest chain, then folding +-- over the adversaries and inserting ten ticks before each honest tick. +-- It only makes sense when there's a single adversary – otherwise, each subsequent adversary will +-- be ten times as fast as the previous one. +-- +-- It uses 'banalStates' to convert each fragment to a schedule (which advances by one block per +-- tick). +fastAdversaryPointSchedule :: + BlockTree TestBlock -> + Maybe PointSchedule +fastAdversaryPointSchedule blockTree = + peer2Point frags (foldGenPeers frags trans) + where + frags = blockTreePeers blockTree + + -- These transformations are applied to the initial empty schedule in turn (first the honest + -- one, then the adversaries in order of their names.) by 'foldGenPeers'. + trans :: Peers PSTrans + trans = Peers { + honest = Peer HonestPeer (\ _ frag _ -> PeerSchedule (Peer HonestPeer <$> banalStates frag)), + others = (transOther <$) <$> others frags + } + + transOther :: PSTrans + transOther i frag (PeerSchedule z) = + PeerSchedule (concat (snd (mapAccumL step states z))) + where + states :: [NodeState] + states = banalStates frag + + step :: [NodeState] -> Peer NodeState -> ([NodeState], [Peer NodeState]) + step rest p@(Peer HonestPeer _) + | let (pre, post) = splitAt 10 rest + = (post, p : (Peer i <$> pre)) + step rest p = (rest, [p]) + +-- | Generate a point schedule that consist of a single tick in which the honest peer advertises +-- its entire chain immediately. +onlyHonestPointSchedule :: BlockTree TestBlock -> Maybe PointSchedule +onlyHonestPointSchedule BlockTree {btTrunk = Empty _} = Nothing +onlyHonestPointSchedule BlockTree {btTrunk = _ :> tipBlock} = + Just $ PointSchedule (pure tick) + where + tick = Tick {active = honestPeerState, peers = Peers honestPeerState Map.empty} + honestPeerState = Peer HonestPeer (NodeOnline points) + points = AdvertisedPoints tipPoint headerPoint blockPoint + tipPoint = TipPoint $ tipFromHeader tipBlock + headerPoint = HeaderPoint $ getHeader tipBlock + blockPoint = BlockPoint tipBlock + +-- | Generate a point schedule that consist of a single tick in which the honest peer advertises +-- its entire chain as it becomes available. +-- +-- No idea what the point of this is. +onlyHonestWithMintingPointSchedule :: SlotNo -> Int -> TestFrag -> Maybe PointSchedule +onlyHonestWithMintingPointSchedule initialSlotNo _ticksPerSlot fullFragment@(_ :> finalBlock) = + pointSchedule (map tickAtSlotNo [initialSlotNo .. finalSlotNo]) + where + -- If we hold a block, we are guaranteed that the slot number cannot be + -- origin? + finalSlotNo = case getTipSlotNo $ tipFromHeader finalBlock of + At s -> s + _ -> error "unexpected alternative" + + advertisedPointsAtSlotNo :: SlotNo -> AdvertisedPoints + advertisedPointsAtSlotNo slotNo = + case fst $ splitFragmentAtSlotNo slotNo fullFragment of + Empty _ -> error "onlyHonestWithMintingPointSchedule: there should be a block at that slot" + (_ :> tipBlock) -> + let tipPoint = TipPoint $ tipFromHeader tipBlock + headerPoint = HeaderPoint $ getHeader tipBlock + blockPoint = BlockPoint tipBlock + in AdvertisedPoints tipPoint headerPoint blockPoint + + tickAtSlotNo :: SlotNo -> Tick + tickAtSlotNo slotNo = + let honestPeerState = + Peer HonestPeer $ + NodeOnline $ + advertisedPointsAtSlotNo slotNo + in + Tick { + active = honestPeerState, + peers = Peers honestPeerState Map.empty + } +onlyHonestWithMintingPointSchedule _initialSlotNo _ticksPerSlot _fullFragment = + error "unexpected alternative" + +-- onlyHonestWithMintingPointSchedule' :: SlotNo -> Int -> TestFrag -> PointSchedule +-- onlyHonestWithMintingPointSchedule' initialSlotNo ticksPerSlot fullFragment = +-- let (availFragment, futureFragment) = splitFragmentAtSlotNo (At initialSlotNo) fullFragment +-- blockSlotNos = map blockSlotNo toOldestFirst futureFragment + +-- | Given a slot number and an anchored fragment 'a', splits the fragment into +-- two 'b' and 'c' such that: +-- +-- - 'b' is anchored in the same place as 'a' and contains all the blocks of 'a' +-- that have a slot number smaller than (or equal to) the given one. +-- +-- - 'c' is anchored at the last block of 'b' and contains all the blocks of 'a' +-- that have a slot number strictly greater than the given one. +splitFragmentAtSlotNo :: + HasHeader b => + SlotNo -> + AnchoredFragment b -> + (AnchoredFragment b, AnchoredFragment b) +splitFragmentAtSlotNo slotNo (fragment :> block) = + if blockSlot block <= slotNo then + (fragment :> block, Empty $ anchorFromBlock block) + else + let (firstPart, secondPart) = splitFragmentAtSlotNo slotNo fragment in + (firstPart, secondPart :> block) +splitFragmentAtSlotNo _ (Empty anchor) = + (Empty anchor, Empty anchor) + +-- | Encodes the different scheduling styles for use with quickcheck generators. +data ScheduleType = + FastAdversary + | + Banal + | + OnlyHonest + deriving (Eq, Show) + +newtype GenesisWindow = GenesisWindow { getGenesisWindow :: Word64 } + deriving (Show) + +-- | All the data used by point schedule tests. +data GenesisTest = GenesisTest { + gtHonestAsc :: Asc, + gtSecurityParam :: SecurityParam, + gtGenesisWindow :: GenesisWindow, + gtBlockTree :: BlockTree TestBlock + } + +-- | Create a point schedule from the given block tree. +-- +-- The first argument determines the scheduling style. +genSchedule :: ScheduleType -> BlockTree TestBlock -> Maybe PointSchedule +genSchedule = \case + FastAdversary -> fastAdversaryPointSchedule + Banal -> banalPointSchedule + OnlyHonest -> onlyHonestPointSchedule diff --git a/ouroboros-consensus/ouroboros-consensus.cabal b/ouroboros-consensus/ouroboros-consensus.cabal index 34f809d940..e0f425e1a2 100644 --- a/ouroboros-consensus/ouroboros-consensus.cabal +++ b/ouroboros-consensus/ouroboros-consensus.cabal @@ -298,6 +298,7 @@ library unstable-consensus-testlib Test.Ouroboros.Consensus.ChainGenerator.RaceIterator Test.Ouroboros.Consensus.ChainGenerator.Slot Test.Ouroboros.Consensus.ChainGenerator.Some + Test.QuickCheck.Extras Test.Util.BoolProps Test.Util.ChainDB Test.Util.ChainUpdates @@ -549,6 +550,7 @@ test-suite infra-test , random , tasty , tasty-quickcheck + , unstable-consensus-testlib , vector test-suite storage-test diff --git a/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Adversarial.hs b/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Adversarial.hs index 5b3a7c758d..fc59c1de55 100644 --- a/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Adversarial.hs +++ b/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Adversarial.hs @@ -23,6 +23,7 @@ module Test.Ouroboros.Consensus.ChainGenerator.Adversarial ( , ChainSchema (ChainSchema) , RaceViolation (AdversaryWonRace, rvAdv, rvHon) , checkAdversarialChain + , genPrefixBlockCount ) where import Control.Applicative ((<|>)) @@ -570,3 +571,20 @@ withinYS :: Delta -> MaybeYS base -> RI.Race base -> Bool withinYS (Delta d) !mbYS !(RI.Race (C.SomeWindow Proxy win)) = case mbYS of KnownYS ys -> C.windowLast win C.+ d < ys UnknownYS -> True -- Honest Chain Growth ensures every Race Window is at most @'Scg' - 'Delta'@ slots wide + +-- | Draw a random active slot count for the prefix of a fork. +-- +-- The count will be strictly smaller than the number of active slots in the given 'ChainSchema'. +-- +-- REVIEW: why do we not allow forking off the block number 1? +genPrefixBlockCount :: R.RandomGen g => g -> ChainSchema base hon -> C.Var hon 'ActiveSlotE +genPrefixBlockCount g schedH = + if C.toVar numChoices < 2 then C.Count 0 {- can always pick genesis -} else do + C.toVar $ R.runSTGen_ g $ C.uniformIndex numChoices + where + ChainSchema _slots v = schedH + + numChoices = pc C.- 1 -- can't pick the last active slot + + -- 'H.uniformTheHonestChain' ensures 0 < pc + pc = BV.countActivesInV S.notInverted v diff --git a/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Honest.hs b/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Honest.hs index 8ad9a62e77..6dfbb96f3a 100644 --- a/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Honest.hs +++ b/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Honest.hs @@ -19,6 +19,7 @@ module Test.Ouroboros.Consensus.ChainGenerator.Honest ( , SomeHonestChainSchema (SomeHonestChainSchema) , checkHonestRecipe , countChainSchema + , genHonestRecipe , uniformTheHonestChain -- * Testing , HonestChainViolation (BadCount, BadScgWindow, BadLength) @@ -40,11 +41,13 @@ import qualified System.Random.Stateful as R import qualified Test.Ouroboros.Consensus.ChainGenerator.BitVector as BV import qualified Test.Ouroboros.Consensus.ChainGenerator.Counting as C import Test.Ouroboros.Consensus.ChainGenerator.Params (Asc, - Delta (Delta), Kcp (Kcp), Len (Len), Scg (Scg)) + Delta (Delta), Kcp (Kcp), Len (Len), Scg (Scg), genKSD) import qualified Test.Ouroboros.Consensus.ChainGenerator.Slot as S import Test.Ouroboros.Consensus.ChainGenerator.Slot (E (ActiveSlotE, SlotE), S) import qualified Test.Ouroboros.Consensus.ChainGenerator.Some as Some +import qualified Test.QuickCheck as QC +import Test.QuickCheck.Extras (sized1) ----- @@ -99,6 +102,13 @@ data NoSuchHonestChainSchema = BadLen deriving (Eq, Read, Show) +genHonestRecipe :: QC.Gen HonestRecipe +genHonestRecipe = sized1 $ \sz -> do + (kcp, Scg s, delta) <- genKSD + -- s <= l, most of the time + l <- QC.frequency [(9, (+ s) <$> QC.choose (0, 5 * sz)), (1, QC.choose (1, s))] + pure $ HonestRecipe kcp (Scg s) delta (Len l) + -- | Checks whether the given 'HonestRecipe' determines a valid input to -- 'uniformTheHonestChain' checkHonestRecipe :: HonestRecipe -> Exn.Except NoSuchHonestChainSchema SomeCheckedHonestRecipe diff --git a/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Params.hs b/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Params.hs index f5480dcc63..6cb7c10140 100644 --- a/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Params.hs +++ b/ouroboros-consensus/src/unstable-consensus-testlib/Test/Ouroboros/Consensus/ChainGenerator/Params.hs @@ -9,9 +9,14 @@ module Test.Ouroboros.Consensus.ChainGenerator.Params ( , ascFromBits , ascFromDouble , ascVal + , genAsc + , genKSD ) where import qualified Data.Bits as B +import Data.Word (Word8) +import qualified Test.QuickCheck as QC +import Test.QuickCheck.Extras (sized1) ----- @@ -23,7 +28,7 @@ import qualified Data.Bits as B -- the onset of slot @x + Δ + 1@. -- -- NOTE: If @Δ=0@, then the best block minted in each slot is selected by every --- (healthy) honset before the onset of the next slot. +-- (healthy) honest node before the onset of the next slot. -- -- NOTE: If the honest block @k+1@ after its intersection with an alternative -- chain was minted in slot @x@, then the alternative block @k+1@ after the @@ -87,3 +92,13 @@ ascFromBits w = ascFromDouble $ toEnum (fromEnum w) / (2 ^ B.finiteBitSize w) -- | Interpret 'Asc' as a 'Double' ascVal :: Asc -> Double ascVal (Asc x) = x + +genAsc :: QC.Gen Asc +genAsc = ascFromBits <$> QC.choose (1 :: Word8, maxBound - 1) + +genKSD :: QC.Gen (Kcp, Scg, Delta) +genKSD = sized1 $ \sz -> do + d <- QC.choose (0, div sz 4) + k <- QC.choose (1, 2 * sz) + s <- (+ k) <$> QC.choose (0, 3 * sz) -- ensures @k / s <= 1@ + pure (Kcp k, Scg s, Delta d) diff --git a/ouroboros-consensus/src/unstable-consensus-testlib/Test/QuickCheck/Extras.hs b/ouroboros-consensus/src/unstable-consensus-testlib/Test/QuickCheck/Extras.hs new file mode 100644 index 0000000000..e85e17ce3e --- /dev/null +++ b/ouroboros-consensus/src/unstable-consensus-testlib/Test/QuickCheck/Extras.hs @@ -0,0 +1,16 @@ +module Test.QuickCheck.Extras ( + sized1 + , unsafeMapSuchThatJust + ) where + +import qualified Test.QuickCheck as QC + +sized1 :: (Int -> QC.Gen a) -> QC.Gen a +sized1 f = QC.sized (f . succ) + +-- | A generator that checks its own satisfaction +-- +-- WARNING: 'QC.suchThat' et al often causes a /very/ confusing +-- non-termination when its argument is impossible/extremely unlikely +unsafeMapSuchThatJust :: QC.Gen (Maybe a) -> QC.Gen a +unsafeMapSuchThatJust m = QC.suchThatMap m id diff --git a/ouroboros-consensus/src/unstable-consensus-testlib/Test/Util/TestBlock.hs b/ouroboros-consensus/src/unstable-consensus-testlib/Test/Util/TestBlock.hs index 02329393bf..78f5ee4bd4 100644 --- a/ouroboros-consensus/src/unstable-consensus-testlib/Test/Util/TestBlock.hs +++ b/ouroboros-consensus/src/unstable-consensus-testlib/Test/Util/TestBlock.hs @@ -29,7 +29,7 @@ module Test.Util.TestBlock ( , Header (..) , StorageConfig (..) , TestBlockError (..) - , TestBlockWith (tbPayload, tbValid) + , TestBlockWith (tbPayload, tbSlot, tbValid) , TestHash (TestHash) , Validity (..) , firstBlockWithPayload diff --git a/ouroboros-consensus/test/infra-test/Test/Ouroboros/Consensus/ChainGenerator/Tests/Adversarial.hs b/ouroboros-consensus/test/infra-test/Test/Ouroboros/Consensus/ChainGenerator/Tests/Adversarial.hs index a3787614a3..304367f06a 100644 --- a/ouroboros-consensus/test/infra-test/Test/Ouroboros/Consensus/ChainGenerator/Tests/Adversarial.hs +++ b/ouroboros-consensus/test/infra-test/Test/Ouroboros/Consensus/ChainGenerator/Tests/Adversarial.hs @@ -4,7 +4,11 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TypeApplications #-} -module Test.Ouroboros.Consensus.ChainGenerator.Tests.Adversarial (tests) where +module Test.Ouroboros.Consensus.ChainGenerator.Tests.Adversarial ( + SomeTestAdversarial (..) + , TestAdversarial (..) + , tests + ) where import Control.Applicative ((<|>)) import qualified Control.Monad.Except as Exn @@ -12,24 +16,24 @@ import Data.Functor ((<&>)) import Data.Functor.Identity (runIdentity) import Data.IORef (modifyIORef', newIORef, readIORef, writeIORef) import Data.Proxy (Proxy (Proxy)) -import Data.Word (Word8) import qualified System.Random as R -import qualified System.Random.Stateful as R import qualified System.Timeout as IO (timeout) import qualified Test.Ouroboros.Consensus.ChainGenerator.Adversarial as A +import Test.Ouroboros.Consensus.ChainGenerator.Adversarial + (genPrefixBlockCount) import qualified Test.Ouroboros.Consensus.ChainGenerator.BitVector as BV import qualified Test.Ouroboros.Consensus.ChainGenerator.Counting as C import qualified Test.Ouroboros.Consensus.ChainGenerator.Honest as H import Test.Ouroboros.Consensus.ChainGenerator.Params (Asc, - Delta (Delta), Kcp (Kcp), Len (Len), Scg (Scg), - ascFromBits) + Delta (Delta), Kcp (Kcp), Len (Len), Scg (Scg), genAsc, + genKSD) import qualified Test.Ouroboros.Consensus.ChainGenerator.RaceIterator as RI import qualified Test.Ouroboros.Consensus.ChainGenerator.Slot as S -import Test.Ouroboros.Consensus.ChainGenerator.Slot - (E (ActiveSlotE, SlotE)) +import Test.Ouroboros.Consensus.ChainGenerator.Slot (E (SlotE)) import qualified Test.Ouroboros.Consensus.ChainGenerator.Some as Some import qualified Test.Ouroboros.Consensus.ChainGenerator.Tests.Honest as H import qualified Test.QuickCheck as QC +import Test.QuickCheck.Extras (sized1, unsafeMapSuchThatJust) import Test.QuickCheck.Random (QCGen) import qualified Test.Tasty as TT import qualified Test.Tasty.QuickCheck as TT @@ -85,21 +89,8 @@ data TestAdversarial base hon = TestAdversarial { } deriving (Read, Show) -genArPrefix :: H.ChainSchema base hon -> QC.Gen (C.Var hon ActiveSlotE) -genArPrefix schedH = - if C.toVar numChoices < 2 then pure (C.Count 0) {- can always pick genesis -} else do - g <- QC.arbitrary - pure $ C.toVar $ R.runSTGen_ (g :: QCGen) $ C.uniformIndex numChoices - where - H.ChainSchema _slots v = schedH - - numChoices = pc C.- 1 -- can't pick the last active slot - - -- 'H.uniformTheHonestChain' ensures 0 < pc - pc = BV.countActivesInV S.notInverted v - instance QC.Arbitrary SomeTestAdversarial where - arbitrary = H.unsafeMapSuchThatJust $ do + arbitrary = unsafeMapSuchThatJust $ do H.TestHonest { H.testAsc = testAscH , @@ -114,7 +105,9 @@ instance QC.Arbitrary SomeTestAdversarial where let arHonest = H.uniformTheHonestChain (Just testAscH) testRecipeH' testSeedH - arPrefix <- genArPrefix arHonest + testSeedPrefix <- QC.arbitrary @QCGen + + let arPrefix = genPrefixBlockCount testSeedPrefix arHonest let H.HonestRecipe kcp scg delta _len = testRecipeH @@ -126,7 +119,7 @@ instance QC.Arbitrary SomeTestAdversarial where A.arHonest } - testAscA <- ascFromBits <$> QC.choose (1 :: Word8, maxBound - 1) + testAscA <- genAsc case Exn.runExcept $ A.checkAdversarialRecipe testRecipeA of Left e -> case e of @@ -296,9 +289,9 @@ mutateAdversarial recipe mut = instance QC.Arbitrary SomeTestAdversarialMutation where arbitrary = do mut <- QC.elements [minBound .. maxBound :: AdversarialMutation] - H.unsafeMapSuchThatJust $ do - (kcp, scg, delta, len) <- H.sized1 $ \sz -> do - (kcp, Scg s, delta) <- H.genKSD + unsafeMapSuchThatJust $ do + (kcp, scg, delta, len) <- sized1 $ \sz -> do + (kcp, Scg s, delta) <- genKSD l <- (+ s) <$> QC.choose (0, 5 * sz) @@ -316,7 +309,9 @@ instance QC.Arbitrary SomeTestAdversarialMutation where let arHonest = H.uniformTheHonestChain Nothing recipeH' seedH - arPrefix <- genArPrefix arHonest + testSeedPrefix <- QC.arbitrary @QCGen + + let arPrefix = genPrefixBlockCount testSeedPrefix arHonest let recipeA = A.AdversarialRecipe { A.arPrefix diff --git a/ouroboros-consensus/test/infra-test/Test/Ouroboros/Consensus/ChainGenerator/Tests/Honest.hs b/ouroboros-consensus/test/infra-test/Test/Ouroboros/Consensus/ChainGenerator/Tests/Honest.hs index 39fbfae4bf..e30a172ae2 100644 --- a/ouroboros-consensus/test/infra-test/Test/Ouroboros/Consensus/ChainGenerator/Tests/Honest.hs +++ b/ouroboros-consensus/test/infra-test/Test/Ouroboros/Consensus/ChainGenerator/Tests/Honest.hs @@ -4,10 +4,7 @@ module Test.Ouroboros.Consensus.ChainGenerator.Tests.Honest ( -- * Re-use TestHonest (TestHonest, testAsc, testRecipe, testRecipe') - , genKSD - , sized1 , unlines' - , unsafeMapSuchThatJust -- * Tests , tests ) where @@ -18,14 +15,14 @@ import Data.Functor ((<&>)) import Data.Functor.Identity (runIdentity) import Data.List (intercalate) import Data.Proxy (Proxy (Proxy)) -import Data.Word (Word8) import qualified System.Random as R import qualified System.Timeout as IO (timeout) import qualified Test.Ouroboros.Consensus.ChainGenerator.Honest as H import Test.Ouroboros.Consensus.ChainGenerator.Params (Asc, - Delta (Delta), Kcp (Kcp), Len (Len), Scg (Scg), - ascFromBits) + Delta (Delta), Kcp (Kcp), Len (Len), Scg (Scg), genAsc, + genKSD) import qualified Test.QuickCheck as QC +import Test.QuickCheck.Extras (sized1, unsafeMapSuchThatJust) import Test.QuickCheck.Random (QCGen) import qualified Test.Tasty as TT import qualified Test.Tasty.QuickCheck as TT @@ -41,18 +38,6 @@ tests = [ ----- -sized1 :: (Int -> QC.Gen a) -> QC.Gen a -sized1 f = QC.sized (f . succ) - --- | A generator that checks its own satisfaction --- --- WARNING: 'QC.suchThat' et al often causes a /very/ confusing --- non-termination when its argument is impossible/extremely unlikely -unsafeMapSuchThatJust :: QC.Gen (Maybe a) -> QC.Gen a -unsafeMapSuchThatJust m = QC.suchThatMap m id - ------ - data TestHonest = TestHonest { testAsc :: !Asc , @@ -62,24 +47,10 @@ data TestHonest = TestHonest { } deriving (Read, Show) -genKSD :: QC.Gen (Kcp, Scg, Delta) -genKSD = sized1 $ \sz -> do - d <- QC.choose (0, div sz 4) - k <- QC.choose (1, 2 * sz) - s <- (\x -> x + k) <$> QC.choose (0, 3 * sz) -- ensures @k / s <= 1@ - pure (Kcp k, Scg s, Delta d) - instance QC.Arbitrary TestHonest where - arbitrary = sized1 $ \sz -> do - testAsc <- ascFromBits <$> QC.choose (1 :: Word8, maxBound - 1) - - testRecipe <- do - (kcp, Scg s, delta) <- genKSD - - -- s <= l, most of the time - l <- QC.frequency [(9, (+ s) <$> QC.choose (0, 5 * sz)), (1, QC.choose (1, s))] - - pure $ H.HonestRecipe kcp (Scg s) delta (Len l) + arbitrary = do + testAsc <- genAsc + testRecipe <- H.genHonestRecipe testRecipe' <- case Exn.runExcept $ H.checkHonestRecipe testRecipe of Left e -> error $ "impossible! " <> show (testRecipe, e) From 934a38ca902d54583a67f2f2ff302a44b6ea261f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Thu, 30 Nov 2023 11:19:14 +0000 Subject: [PATCH 02/15] Add idleTimeout --- .../Test/Consensus/Network/Driver/Limits/Extras.hs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/Driver/Limits/Extras.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/Driver/Limits/Extras.hs index 9b6641c1ce..4231eab189 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/Driver/Limits/Extras.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/Driver/Limits/Extras.hs @@ -78,6 +78,7 @@ chainSyncTimeouts t f = canAwaitTimeout , intersectTimeout , mustReplyTimeout + , idleTimeout } where canAwaitTimeout :: Maybe DiffTime @@ -97,10 +98,14 @@ chainSyncTimeouts t f = realToFrac (getSlotLength t) * log (1 - 0.999) / log (1 - ascVal f) + idleTimeout :: Maybe DiffTime + idleTimeout = Just 3673 -- taken from Ouroboros.Consensus.Node.stdChainSyncTimeout + chainSyncNoTimeouts :: ChainSyncTimeout chainSyncNoTimeouts = ChainSyncTimeout { canAwaitTimeout = Nothing , intersectTimeout = Nothing , mustReplyTimeout = Nothing + , idleTimeout = Nothing } From 820a5cd58eace9678e63f506bee43df2fe7cafa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Fri, 1 Dec 2023 17:41:15 +0000 Subject: [PATCH 03/15] Eliminate record wildcards as requested in #434 --- .../test/consensus-test/Test/Consensus/BlockTree.hs | 8 ++++---- .../Test/Consensus/Genesis/Setup/Classifiers.hs | 2 +- .../Test/Consensus/Genesis/Tests/LongRangeAttack.hs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/BlockTree.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/BlockTree.hs index 7e1513a9c2..7d878d9eb6 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/BlockTree.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/BlockTree.hs @@ -76,12 +76,12 @@ mkTrunk btTrunk = BlockTree { btTrunk, btBranches = [] } -- FIXME: we should enforce that the new branch' suffix does not contain any -- block in common with an existingbranch. addBranch :: AF.HasHeader blk => AF.AnchoredFragment blk -> BlockTree blk -> Maybe (BlockTree blk) -addBranch branch BlockTree{..} = do - (_, btbPrefix, _, btbSuffix) <- AF.intersect btTrunk branch +addBranch branch bt = do + (_, btbPrefix, _, btbSuffix) <- AF.intersect (btTrunk bt) branch -- NOTE: We could use the monadic bind for @Maybe@ here but we would rather -- catch bugs quicker. let btbFull = fromJust $ AF.join btbPrefix btbSuffix - pure $ BlockTree { btTrunk, btBranches = BlockTreeBranch { .. } : btBranches } + pure $ bt { btBranches = BlockTreeBranch { .. } : btBranches bt } -- | Same as @addBranch@ but assumes that the precondition holds. addBranch' :: AF.HasHeader blk => AF.AnchoredFragment blk -> BlockTree blk -> BlockTree blk @@ -90,7 +90,7 @@ addBranch' branch blockTree = -- | Return all the full fragments from the root of the tree. allFragments :: BlockTree blk -> [AF.AnchoredFragment blk] -allFragments BlockTree{..} = btTrunk : map btbFull btBranches +allFragments bt = btTrunk bt : map btbFull (btBranches bt) -- | Look for a point in the block tree and return a fragment going from the -- root of the tree to the point in question. diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/Classifiers.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/Classifiers.hs index a5657c26d7..edd128230d 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/Classifiers.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/Classifiers.hs @@ -48,7 +48,7 @@ classifiers GenesisTest {gtBlockTree, gtSecurityParam = SecurityParam k, gtGenes existsSelectableAdversary = any isSelectable branches - isSelectable BlockTreeBranch{..} = AF.length btbSuffix > fromIntegral k + isSelectable bt = AF.length (btbSuffix bt) > fromIntegral k SlotNo goodTipSlot = withOrigin 0 id (headSlot goodChain) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs index 26c4dac744..d1a6f267d4 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs @@ -34,14 +34,14 @@ genChainsAndSchedule numAdversaries scheduleType = prop_longRangeAttack :: QC.Gen QC.Property prop_longRangeAttack = do (genesisTest, schedule) <- genChainsAndSchedule 1 FastAdversary - let Classifiers {..} = classifiers genesisTest + let cls = classifiers genesisTest pure $ withMaxSuccess 10 $ runSimOrThrow $ runTest genesisTest schedule $ \fragment -> - classify genesisWindowAfterIntersection "Full genesis window after intersection" - $ existsSelectableAdversary ==> not $ isHonestTestFragH fragment + classify (genesisWindowAfterIntersection cls) "Full genesis window after intersection" + $ existsSelectableAdversary cls ==> not $ isHonestTestFragH fragment -- TODO - -- $ not existsSelectableAdversary ==> immutableTipBeforeFork fragment + -- $ not (existsSelectableAdversary cls) ==> immutableTipBeforeFork fragment where isHonestTestFragH :: TestFragH -> Bool isHonestTestFragH frag = case headAnchor frag of From a6d82eaf2b2cdbc674d09bbbc3b8434ac73247da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Fri, 1 Dec 2023 17:46:25 +0000 Subject: [PATCH 04/15] Note long-range attack test on Praos as requested in #434 --- .../Test/Consensus/Genesis/Tests/LongRangeAttack.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs index d1a6f267d4..b297b945dc 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Tests/LongRangeAttack.hs @@ -39,6 +39,8 @@ prop_longRangeAttack = do pure $ withMaxSuccess 10 $ runSimOrThrow $ runTest genesisTest schedule $ \fragment -> classify (genesisWindowAfterIntersection cls) "Full genesis window after intersection" + -- This is the expected behavior of Praos to be reversed with Genesis. + -- But we are testing Praos for the moment $ existsSelectableAdversary cls ==> not $ isHonestTestFragH fragment -- TODO -- $ not (existsSelectableAdversary cls) ==> immutableTipBeforeFork fragment From cb489e2d26eed793232e7d440cd1d4249a05d438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Fri, 1 Dec 2023 17:49:47 +0000 Subject: [PATCH 05/15] Edit slotLength documentation as requested in #434 --- .../Test/Consensus/Network/AnchoredFragment/Extras.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs index 4a4ecd3ce7..52ce796120 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs @@ -4,7 +4,7 @@ import Cardano.Slotting.Slot (SlotNo (unSlotNo), withOrigin) import Ouroboros.Network.AnchoredFragment (AnchoredFragment, HasHeader, anchor, anchorToSlotNo, headAnchor) --- | Number of slots on which the fragment spans. This is different from the +-- | The number of slots the fragment spans. This is different from the -- 'length' which is the number of blocks in the fragment. slotLength :: HasHeader blk => AnchoredFragment blk -> Int slotLength fragment = From 28267960ed36fbabb5403e1e3791f9487e27ff66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Fri, 1 Dec 2023 17:52:08 +0000 Subject: [PATCH 06/15] Explain purpose of Test.Consensus.Network.AnchoredFragment.Extras --- .../Test/Consensus/Network/AnchoredFragment/Extras.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs index 52ce796120..ed8430e946 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs @@ -1,3 +1,4 @@ +-- | Functions to move to Ouroboros.Network.AnchoredFragment module Test.Consensus.Network.AnchoredFragment.Extras (slotLength) where import Cardano.Slotting.Slot (SlotNo (unSlotNo), withOrigin) From 75f3d1e01bd37ea9283e8f772c5107cb7d2a946e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Fri, 1 Dec 2023 17:58:25 +0000 Subject: [PATCH 07/15] Explain provenance of hardcoded configuration values --- .../Test/Consensus/PeerSimulator/BlockFetch.hs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/BlockFetch.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/BlockFetch.hs index 45a9095225..cfac11572e 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/BlockFetch.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/BlockFetch.hs @@ -67,14 +67,18 @@ startBlockFetchLogic registry chainDb fetchClientRegistry getCandidates = do (TestBlockConfig $ NumCoreNodes 0) -- Only needed when minting blocks (BlockFetchClientInterface.defaultChainDbView chainDb) getCandidates + -- The size of headers in bytes is irrelevant because our tests + -- do not serialize the blocks. (\_hdr -> 1000) slotForgeTime (pure FetchModeBulkSync) + -- Values taken from + -- ouroboros-consensus-diffusion/src/unstable-diffusion-testlib/Test/ThreadNet/Network.hs blockFetchCfg = BlockFetchConfiguration - { bfcMaxConcurrencyBulkSync = 2 + { bfcMaxConcurrencyBulkSync = 1 , bfcMaxConcurrencyDeadline = 2 - , bfcMaxRequestsInflight = 4 + , bfcMaxRequestsInflight = 10 , bfcDecisionLoopInterval = 0 , bfcSalt = 0 } From 259f4da69ab21efb7cd22e800df4d78125614bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Fri, 1 Dec 2023 18:07:00 +0000 Subject: [PATCH 08/15] Generalize intersectWith --- .../consensus-test/Test/Consensus/PeerSimulator/Handlers.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs index 52f0ba022e..b39057095a 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs @@ -34,8 +34,8 @@ import Test.Consensus.PointSchedule (AdvertisedPoints (header, tip), import Test.Util.Orphans.IOLike () import Test.Util.TestBlock (TestBlock) --- | Find the first fragment contained in the first arg that starts at one of the given points. -intersectWith :: AnchoredFragment TestBlock -> [Point TestBlock] -> Maybe (Point TestBlock) +-- | Find the first point in the fragment +intersectWith :: HasHeader b => AnchoredFragment b -> [Point b] -> Maybe (Point b) intersectWith fullFrag pts = AF.anchorPoint . snd <$> getFirst (foldMap (First . AF.splitAfterPoint fullFrag) pts) From 869c1700b57ebfb88d074fb003bf3779cd770271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Fri, 1 Dec 2023 18:09:24 +0000 Subject: [PATCH 09/15] Simplify intersectWith --- .../Test/Consensus/PeerSimulator/Handlers.hs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs index b39057095a..7f6ecb6f22 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs @@ -15,9 +15,9 @@ import Control.Monad.Trans (lift) import Control.Monad.Writer.Strict (MonadWriter (tell), WriterT (runWriterT)) import Data.Coerce (coerce) -import Data.Maybe (fromJust) -import Data.Monoid (First (..)) -import Ouroboros.Consensus.Block.Abstract (Point (..), getHeader) +import Data.List (find) +import Data.Maybe (fromJust, isJust) +import Ouroboros.Consensus.Block.Abstract (HasHeader, Point (..), getHeader) import Ouroboros.Consensus.Util.Condense (Condense (..)) import Ouroboros.Consensus.Util.IOLike (IOLike, STM, StrictTVar, readTVar, writeTVar) @@ -36,8 +36,7 @@ import Test.Util.TestBlock (TestBlock) -- | Find the first point in the fragment intersectWith :: HasHeader b => AnchoredFragment b -> [Point b] -> Maybe (Point b) -intersectWith fullFrag pts = - AF.anchorPoint . snd <$> getFirst (foldMap (First . AF.splitAfterPoint fullFrag) pts) +intersectWith fullFrag = find (isJust . AF.splitAfterPoint fullFrag) -- | Handle a @MsgFindIntersect@ message. -- From bfeb9bc749bc78ba2254ef292b86946f28499a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Fri, 1 Dec 2023 18:14:08 +0000 Subject: [PATCH 10/15] Stage intersectWith for inclusion in Ouroboros.Network.AnchoredFragment as suggested in #434 --- .../Consensus/Network/AnchoredFragment/Extras.hs | 16 +++++++++++++--- .../Test/Consensus/PeerSimulator/Handlers.hs | 10 +++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs index ed8430e946..97ed1d8d55 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs @@ -1,9 +1,19 @@ -- | Functions to move to Ouroboros.Network.AnchoredFragment -module Test.Consensus.Network.AnchoredFragment.Extras (slotLength) where +module Test.Consensus.Network.AnchoredFragment.Extras + ( intersectWith + , slotLength + ) where import Cardano.Slotting.Slot (SlotNo (unSlotNo), withOrigin) -import Ouroboros.Network.AnchoredFragment (AnchoredFragment, - HasHeader, anchor, anchorToSlotNo, headAnchor) +import Data.List (find) +import Data.Maybe (isJust) +import Ouroboros.Network.AnchoredFragment (AnchoredFragment, Point, + HasHeader, anchor, anchorToSlotNo, headAnchor, splitAfterPoint) + + +-- | Find the first point in the fragment +intersectWith :: HasHeader b => AnchoredFragment b -> [Point b] -> Maybe (Point b) +intersectWith fullFrag = find (isJust . splitAfterPoint fullFrag) -- | The number of slots the fragment spans. This is different from the -- 'length' which is the number of blocks in the fragment. diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs index 7f6ecb6f22..5e6759ae15 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs @@ -15,13 +15,11 @@ import Control.Monad.Trans (lift) import Control.Monad.Writer.Strict (MonadWriter (tell), WriterT (runWriterT)) import Data.Coerce (coerce) -import Data.List (find) -import Data.Maybe (fromJust, isJust) -import Ouroboros.Consensus.Block.Abstract (HasHeader, Point (..), getHeader) +import Data.Maybe (fromJust) +import Ouroboros.Consensus.Block.Abstract (Point (..), getHeader) import Ouroboros.Consensus.Util.Condense (Condense (..)) import Ouroboros.Consensus.Util.IOLike (IOLike, STM, StrictTVar, readTVar, writeTVar) -import Ouroboros.Network.AnchoredFragment (AnchoredFragment) import qualified Ouroboros.Network.AnchoredFragment as AF import Ouroboros.Network.Block (blockPoint, getTipPoint) import qualified Test.Consensus.BlockTree as BT @@ -31,12 +29,10 @@ import Test.Consensus.PeerSimulator.ScheduledChainSyncServer RequestNext (AwaitReply, RollBackward, RollForward)) import Test.Consensus.PointSchedule (AdvertisedPoints (header, tip), HeaderPoint (HeaderPoint), TipPoint (TipPoint)) +import Test.Consensus.Network.AnchoredFragment.Extras (intersectWith) import Test.Util.Orphans.IOLike () import Test.Util.TestBlock (TestBlock) --- | Find the first point in the fragment -intersectWith :: HasHeader b => AnchoredFragment b -> [Point b] -> Maybe (Point b) -intersectWith fullFrag = find (isJust . AF.splitAfterPoint fullFrag) -- | Handle a @MsgFindIntersect@ message. -- From 606559ecbfb4348802ff593f72888f3f1d4fc432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Fri, 1 Dec 2023 18:26:08 +0000 Subject: [PATCH 11/15] Use NumericUnderscores to split integers as requested in #434 --- .../consensus-test/Test/Consensus/PeerSimulator/Trace.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs index de3a333b4a..6caaf7cf09 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs @@ -1,5 +1,6 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} -- | Helpers for tracing used by the peer simulator. module Test.Consensus.PeerSimulator.Trace ( @@ -65,7 +66,7 @@ traceUnitWith tracer unit msg = do showTime :: Time -> String showTime (Time time) = let ps = diffTimeToPicoseconds time - milliseconds = (ps `div` 1000000000) `mod` 1000 - seconds = (ps `div` 1000000000000) `rem` 60 - minutes = (ps `div` 1000000000000) `quot` 60 + milliseconds = (ps `div` 1_000_000_000) `mod` 1_000 + seconds = (ps `div` 1_000_000_000_000) `rem` 60 + minutes = (ps `div` 1_000_000_000_000) `quot` 60 in printf "%02d:%02d.%03d" minutes seconds milliseconds From 32785dae0396c9de0303c1949fabe7b8e558a9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Fri, 1 Dec 2023 20:45:11 +0000 Subject: [PATCH 12/15] Comment the purpose of Test.Consensus.Genesis.Setup.runTest --- .../test/consensus-test/Test/Consensus/Genesis/Setup.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup.hs index b06fa9e5d1..cd455e1326 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup.hs @@ -24,6 +24,8 @@ import Test.Util.Orphans.IOLike () import Test.Util.Tracer (recordingTracerTVar) import Test.Consensus.Genesis.Setup.GenChains +-- | Runs the given point schedule and evaluates the given property on the final +-- state view. runTest :: (IOLike m, MonadTime m, MonadTimer m) => GenesisTest -> From 88055fae5337a12317a55b55553913b703210c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Mon, 4 Dec 2023 14:36:12 +0000 Subject: [PATCH 13/15] Replace poetry with messages describing the start and end of a schedule run --- .../Test/Consensus/PeerSimulator/Run.hs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs index 890dc93610..22c69a7b45 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs @@ -220,18 +220,11 @@ runScheduler tracer (PointSchedule ps) peers = do traceWith tracer "Schedule is:" for_ ps $ \tick -> traceWith tracer $ " " ++ condense tick traceWith tracer "--------------------------------------------------------------------------------" - traceWith tracer "» Time says “Let there be”" - traceWith tracer "» every moment and instantly" - traceWith tracer "» there is space and the radiance" - traceWith tracer "» of each bright galaxy." + traceWith tracer "Running point schedule ..." traceWith tracer "--------------------------------------------------------------------------------" for_ ps (dispatchTick tracer peers) traceWith tracer "--------------------------------------------------------------------------------" - traceWith tracer "» A Clock stopped -" - traceWith tracer "» Not the Mantel's -" - traceWith tracer "» Geneva's farthest skill" - traceWith tracer "» Can't put the puppet bowing" - traceWith tracer "» That just now dangled still -" + traceWith tracer "Finished running point schedule" -- | Construct STM resources, set up ChainSync and BlockFetch threads, and -- send all ticks in a 'PointSchedule' to all given peers in turn. From d3dd9a5141204a70ed7bfa5f10bbd2ab961626e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Mon, 4 Dec 2023 14:37:22 +0000 Subject: [PATCH 14/15] Run stylish --- .../Test/Consensus/Network/AnchoredFragment/Extras.hs | 9 +++++---- .../Test/Consensus/PeerSimulator/Handlers.hs | 2 +- .../consensus-test/Test/Consensus/PeerSimulator/Trace.hs | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs index 97ed1d8d55..ae56621602 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Network/AnchoredFragment/Extras.hs @@ -1,14 +1,15 @@ -- | Functions to move to Ouroboros.Network.AnchoredFragment -module Test.Consensus.Network.AnchoredFragment.Extras - ( intersectWith +module Test.Consensus.Network.AnchoredFragment.Extras ( + intersectWith , slotLength ) where import Cardano.Slotting.Slot (SlotNo (unSlotNo), withOrigin) import Data.List (find) import Data.Maybe (isJust) -import Ouroboros.Network.AnchoredFragment (AnchoredFragment, Point, - HasHeader, anchor, anchorToSlotNo, headAnchor, splitAfterPoint) +import Ouroboros.Network.AnchoredFragment (AnchoredFragment, + HasHeader, Point, anchor, anchorToSlotNo, headAnchor, + splitAfterPoint) -- | Find the first point in the fragment diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs index 5e6759ae15..1e349ed4b7 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Handlers.hs @@ -24,12 +24,12 @@ import qualified Ouroboros.Network.AnchoredFragment as AF import Ouroboros.Network.Block (blockPoint, getTipPoint) import qualified Test.Consensus.BlockTree as BT import Test.Consensus.BlockTree (BlockTree) +import Test.Consensus.Network.AnchoredFragment.Extras (intersectWith) import Test.Consensus.PeerSimulator.ScheduledChainSyncServer (FindIntersect (..), RequestNext (AwaitReply, RollBackward, RollForward)) import Test.Consensus.PointSchedule (AdvertisedPoints (header, tip), HeaderPoint (HeaderPoint), TipPoint (TipPoint)) -import Test.Consensus.Network.AnchoredFragment.Extras (intersectWith) import Test.Util.Orphans.IOLike () import Test.Util.TestBlock (TestBlock) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs index 6caaf7cf09..1e29a19e93 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Trace.hs @@ -1,5 +1,5 @@ -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NumericUnderscores #-} -- | Helpers for tracing used by the peer simulator. From 2076baca16a7db374b9d92c7d535bca96debbfa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Mon, 4 Dec 2023 15:48:52 +0000 Subject: [PATCH 15/15] Pass a dummy future check to chainSyncClient --- .../Test/Consensus/PeerSimulator/Run.hs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs index 22c69a7b45..d32ac2eccd 100644 --- a/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs +++ b/ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/PeerSimulator/Run.hs @@ -20,11 +20,13 @@ import Data.Functor (void) import Data.List.NonEmpty (NonEmpty, nonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map +import Data.Proxy (Proxy (..)) import Data.Traversable (for) import Ouroboros.Consensus.Config (TopLevelConfig (..)) import qualified Ouroboros.Consensus.HardFork.History.EraParams as HardFork import Ouroboros.Consensus.MiniProtocol.ChainSync.Client (ChainDbView, Consensus, chainSyncClient, defaultChainDbView) +import qualified Ouroboros.Consensus.MiniProtocol.ChainSync.Client.InFutureCheck as InFutureCheck import Ouroboros.Consensus.Storage.ChainDB.API import qualified Ouroboros.Consensus.Storage.ChainDB.API as ChainDB import Ouroboros.Consensus.Storage.ChainDB.Impl @@ -79,7 +81,7 @@ data SchedulerConfig = } deriving (Show) -basicChainSyncClient :: +basicChainSyncClient :: forall m. IOLike m => Tracer m String -> TopLevelConfig TestBlock -> @@ -91,11 +93,20 @@ basicChainSyncClient tracer cfg chainDbView varCandidate = (pipelineDecisionLowHighMark 10 20) (mkChainSyncClientTracer tracer) cfg + dummyHeaderInFutureCheck chainDbView maxBound (return Continue) nullTracer varCandidate + where + dummyHeaderInFutureCheck :: InFutureCheck.HeaderInFutureCheck m TestBlock + dummyHeaderInFutureCheck = InFutureCheck.HeaderInFutureCheck + { InFutureCheck.proxyArrival = Proxy + , InFutureCheck.recordHeaderArrival = \_ -> pure () + , InFutureCheck.judgeHeaderArrival = \_ _ _ -> pure () + , InFutureCheck.handleHeaderArrival = \_ -> pure Nothing + } -- | A record to associate an exception thrown by the ChainSync -- thread with the peer that it was running for.