-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement the peer simulator for Genesis tests (#434)
This PR introduces an initial implementation of a Peer Simulator for Genesis tests. A simulated peer is told how to behave by a list of scheduled events that is generated for every test. In essence, it is an implementation of a ChainSync server and a BlockFetch server that behave as prescribed by the schedule. The rest of the PR provides support infrastructure in the form of: * a Praos client node that plays the role of subject under test, * an example test for long range attacks, and * minimal definitions to generate a schedule from the chain schemas introduced in #240. This is a squash of the many commits in the integration branch of #367.
- Loading branch information
Showing
26 changed files
with
2,442 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
207 changes: 207 additions & 0 deletions
207
ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/BlockTree.hs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 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 $ 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 | ||
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 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. | ||
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') |
55 changes: 55 additions & 0 deletions
55
ouroboros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup.hs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
{-# 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 | ||
|
||
-- | Runs the given point schedule and evaluates the given property on the final | ||
-- state view. | ||
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} |
57 changes: 57 additions & 0 deletions
57
...boros-consensus-diffusion/test/consensus-test/Test/Consensus/Genesis/Setup/Classifiers.hs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 bt = AF.length (btbSuffix bt) > fromIntegral k | ||
|
||
SlotNo goodTipSlot = withOrigin 0 id (headSlot goodChain) | ||
|
||
branches = btBranches gtBlockTree | ||
|
||
goodChain = btTrunk gtBlockTree |
Oops, something went wrong.