Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: genesis tests #367

Closed
wants to merge 64 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
a7566f8
Fix comments
nfrisby Mar 27, 2023
45839cb
Add new test modules
Niols Sep 21, 2023
32b20bd
Export tbSlot from .TestBlock
facundominguez Sep 21, 2023
e5eb04b
Export SomeTestAdversarial and TestAdversarial from .Adversarial
facundominguez Sep 21, 2023
53d62dd
Run genesis tests in infra-test testsuite
facundominguez Sep 25, 2023
7d6deb1
Remove uses of OverloadedRecordDot
facundominguez Sep 25, 2023
648ffc8
Remove redundant dependencies from infra-test
facundominguez Sep 26, 2023
10a8995
Fix warnings in the code
facundominguez Sep 26, 2023
7b97122
Remove redundant type class constraint
facundominguez Sep 26, 2023
274a455
Remove unused si-timers dependency
facundominguez Sep 26, 2023
400b2e1
Remove unused dependency io-class
facundominguez Sep 26, 2023
a82dcf9
Run `stylish-haskell` and `cabal-fmt`
Niols Sep 27, 2023
1bc8539
Introduce a proper type for block trees
Niols Sep 28, 2023
22281ea
Introduce helper `slotLength`
Niols Sep 21, 2023
486b55e
MockedChainSyncServer relying on the block tree
Niols Sep 27, 2023
655c134
More readable tracing
Niols Sep 25, 2023
0f4a827
Handle properly intersections further than our tip
Niols Sep 25, 2023
af4e0f7
More readable tracing
Niols Sep 25, 2023
15867ac
Prefix `BlockTreeBranch` fields
Niols Sep 27, 2023
c2ea0e0
Prefix `BlockTree` fields
Niols Sep 27, 2023
b6b55d7
Improve the comment for `addBranch`
Niols Sep 27, 2023
d6d24d2
Clearer documentation of block tree invariants
Niols Sep 27, 2023
8d578a3
Document `genChains`
Niols Sep 27, 2023
89dbc3e
Move `slotLength` to `...AnchoredFragment.Extras`
Niols Sep 27, 2023
29a186d
Document `prettyPrint`
Niols Sep 27, 2023
05055ef
Replace `firstJust` by `asum`
Niols Sep 27, 2023
2d4e30a
Document data type `MockedChainSyncServer`
Niols Sep 27, 2023
d8d4308
Typo
Niols Sep 27, 2023
65154a8
Better explanation of `findPath`
Niols Sep 27, 2023
4c5647e
Better return value and explanation of `findPath`
Niols Sep 27, 2023
c15a99a
Apply suggestions by @facundominguez
Niols Sep 28, 2023
c221925
Enter block tree (#368)
Niols Sep 28, 2023
ed1838a
Rename functions and types in the Sync module
facundominguez Oct 2, 2023
0a581da
Rename peers to connectionThreads in ConnectionThread
facundominguez Oct 2, 2023
2a9734d
Remove unused field of ConnectionThread
facundominguez Oct 2, 2023
0f04746
Inline syncTest only call
facundominguez Oct 2, 2023
d05b899
./scripts/ci/run-stylish.sh
facundominguez Oct 2, 2023
f05c8fa
Run the ChainSync client with limits
Niols Oct 4, 2023
b1b1b41
Add the block fetch mock server to the peer simulator
facundominguez Oct 3, 2023
6ba8d37
Separate block tree analysis and abstract point schedule dispatch
tek Oct 4, 2023
7a7038b
Reorganise Genesis tests
Niols Oct 9, 2023
b66fd42
Delete redundant `PointScheduleTest` module
Niols Oct 9, 2023
1cf6406
Extract common test setup for Genesis
Niols Oct 9, 2023
faf466b
Get rid of `ChainGenerator.Tests.Sync`
Niols Oct 9, 2023
7868d3e
Make `runPointSchedule` read the list of peers from the schedule
Niols Oct 9, 2023
0104cba
Delete redundant `ChainGenerator.Tests.ChainDb`
Niols Oct 10, 2023
7536fc2
Move `BlockTree` and `PointSchedule` out of `ChainGenerator.Tests`
Niols Oct 10, 2023
73c6fa3
Cleanup various things (#411)
tek Oct 11, 2023
4fa10f9
Remove the ConnectionThread data type
facundominguez Oct 11, 2023
d6a6e87
Attach the PeerId to the exceptions of the ChainSync client
facundominguez Oct 11, 2023
ccdfe4f
Enable the generation of multiple adversarial chains for the peer sim…
tek Oct 12, 2023
d552e14
Move Genesis tests to `ouroboros-consensus-diffusion:consensus-test`
Niols Oct 12, 2023
5115381
Make a pass over the documentation of Classifiers
facundominguez Oct 12, 2023
a37a2d5
Rename genesisWindowAfterIntersection to genesisWindowAfterIntersection
facundominguez Oct 12, 2023
831d788
Remove REVIEW comment
facundominguez Oct 12, 2023
28b4c0b
Rename fastAdversarySchedule to fastAdversaryPointSchedule
facundominguez Oct 12, 2023
41920cb
Remove the example for classifiers
facundominguez Oct 13, 2023
8246411
add AwaitReply to MsgRequest handler
tek Oct 11, 2023
751bd17
Document
Niols Oct 12, 2023
fe41dd6
Remove redundant imports
Niols Oct 12, 2023
ef899cd
remove AwaitReply when the peer falls behind; make timeouts configura…
tek Oct 13, 2023
65140c3
Send `AwaitReply` to prevent timeouts. (#432)
tek Oct 13, 2023
a89689a
add docs for PointSchedule
tek Oct 13, 2023
0222eaf
Update note in PointSchedule.hs
facundominguez Oct 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -202,36 +202,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
2 changes: 2 additions & 0 deletions ouroboros-consensus-diffusion/test/consensus-test/Main.hs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,4 +19,5 @@ tests =
Test.Consensus.HardFork.Combinator.tests
]
]
, Test.Consensus.Genesis.Tests.tests
]
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 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')
Original file line number Diff line number Diff line change
@@ -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}
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 BlockTreeBranch{..} = AF.length btbSuffix > fromIntegral k

SlotNo goodTipSlot = withOrigin 0 id (headSlot goodChain)

branches = btBranches gtBlockTree

goodChain = btTrunk gtBlockTree
Loading
Loading