Skip to content

Commit

Permalink
Allow to specify tests with multiple honest peers
Browse files Browse the repository at this point in the history
* Rewrite `Peers` to accept arbitrary number of peers
* Actually generate honest peers in CSJ happy path
* Support a field for extra honest peers in `GenesisTest`
* Allow `uniformPoints` to generate schedules with multiple honest peers
* Adapt CSJ test to use native multiple honest peers generation
* Share partial accessor functions used in tests
* Use partial accessor to retrieve the only honest peer
  • Loading branch information
Niols authored and facundominguez committed May 30, 2024
1 parent 2f7152b commit a969b37
Show file tree
Hide file tree
Showing 15 changed files with 431 additions and 281 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ test-suite consensus-test
Test.Consensus.PointSchedule.SinglePeer
Test.Consensus.PointSchedule.SinglePeer.Indices
Test.Consensus.PointSchedule.Tests
Test.Util.PartialAccessors
Test.Util.TersePrinting

build-depends:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ import Test.Consensus.Network.AnchoredFragment.Extras (slotLength)
import Test.Consensus.PeerSimulator.StateView
(PeerSimulatorResult (..), StateView (..), pscrToException)
import Test.Consensus.PointSchedule
import Test.Consensus.PointSchedule.Peers (Peer (..), PeerId (..),
Peers (..))
import Test.Consensus.PointSchedule.Peers (PeerId (..), Peers (..))
import Test.Consensus.PointSchedule.SinglePeer (SchedulePoint (..))
import Test.Util.Orphans.IOLike ()
import Test.Util.TestBlock (TestBlock, TestHash (TestHash),
Expand Down Expand Up @@ -165,15 +164,15 @@ resultClassifiers GenesisTest{gtSchedule} RunGenesisTestResult{rgtrStateView} =
StateView{svPeerSimulatorResults} = rgtrStateView

adversaries :: [PeerId]
adversaries = Map.keys $ others gtSchedule
adversaries = fmap AdversarialPeer $ Map.keys $ adversarialPeers gtSchedule

adversariesCount = fromIntegral $ length adversaries

adversariesExceptions :: [(PeerId, SomeException)]
adversariesExceptions = mapMaybe
(\PeerSimulatorResult{psePeerId, pseResult} -> case psePeerId of
HonestPeer -> Nothing
pid -> (pid,) <$> pscrToException pseResult
HonestPeer _ -> Nothing
pid -> (pid,) <$> pscrToException pseResult
)
svPeerSimulatorResults

Expand Down Expand Up @@ -251,18 +250,17 @@ scheduleClassifiers GenesisTest{gtSchedule = schedule} =
rollbacks :: Peers Bool
rollbacks = hasRollback <$> schedule

adversaryRollback = any value $ others rollbacks
adversaryRollback = any id $ adversarialPeers rollbacks
honestRollback = any id $ honestPeers rollbacks

honestRollback = value $ honest rollbacks

allAdversariesEmpty = all value $ others $ null <$> schedule
allAdversariesEmpty = all id $ adversarialPeers $ null <$> schedule

isTrivial :: PeerSchedule TestBlock -> Bool
isTrivial = \case
[] -> True
(t0, _):points -> all ((== t0) . fst) points

allAdversariesTrivial = all value $ others $ isTrivial <$> schedule
allAdversariesTrivial = all id $ adversarialPeers $ isTrivial <$> schedule

simpleHash ::
HeaderHash block ~ TestHash =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
module Test.Consensus.Genesis.Setup.GenChains (
GenesisTest (..)
, genChains
, genChainsWithExtraHonestPeers
) where

import Cardano.Slotting.Time (SlotLength, getSlotLength,
Expand Down Expand Up @@ -94,6 +95,9 @@ genAlternativeChainSchema (testRecipeH, arHonest) =
let H.ChainSchema _ v = A.uniformAdversarialChain (Just alternativeAsc) testRecipeA'' seed
pure $ Just (prefixCount, Vector.toList (getVector v))

genChains :: QC.Gen Word -> QC.Gen (GenesisTest TestBlock ())
genChains = genChainsWithExtraHonestPeers (pure 0)

-- | 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
Expand All @@ -103,8 +107,10 @@ genAlternativeChainSchema (testRecipeH, arHonest) =
-- trunk: O─────1──2──3──4─────5──6──7
-- │ ╰─────6
-- ╰─────3──4─────5
genChains :: QC.Gen Word -> QC.Gen (GenesisTest TestBlock ())
genChains genNumForks = do
-- For now, the @extraHonestPeers@ generator is only used to fill the GenesisTest field.
-- However, in the future it could also be used to generate "short forks" near the tip of the trunk.
genChainsWithExtraHonestPeers :: QC.Gen Word -> QC.Gen Word -> QC.Gen (GenesisTest TestBlock ())
genChainsWithExtraHonestPeers genNumExtraHonest genNumForks = do
(asc, honestRecipe, someHonestChainSchema) <- genHonestChainSchema

H.SomeHonestChainSchema _ _ honestChainSchema <- pure someHonestChainSchema
Expand All @@ -116,6 +122,7 @@ genChains genNumForks = do
HonestRecipe (Kcp kcp) (Scg scg) delta _len = honestRecipe

numForks <- genNumForks
gtExtraHonestPeers <- genNumExtraHonest
alternativeChainSchemas <- replicateM (fromIntegral numForks) (genAlternativeChainSchema (honestRecipe, honestChainSchema))
pure $ GenesisTest {
gtSecurityParam = SecurityParam (fromIntegral kcp),
Expand All @@ -131,6 +138,7 @@ genChains genNumForks = do
-- would make for interesting tests.
gtCSJParams = CSJParams $ fromIntegral scg,
gtBlockTree = foldl' (flip BT.addBranch') (BT.mkTrunk goodChain) $ zipWith (genAdversarialFragment goodBlocks) [1..] alternativeChainSchemas,
gtExtraHonestPeers,
gtSchedule = ()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@

module Test.Consensus.Genesis.Tests.CSJ (tests) where

import Control.Monad (replicateM)
import Data.Containers.ListUtils (nubOrd)
import Data.Functor (($>))
import Data.List (nub)
import Data.Maybe (mapMaybe)
import Ouroboros.Consensus.Block (blockSlot, succWithOrigin)
Expand All @@ -22,11 +20,11 @@ import Test.Consensus.PeerSimulator.Run (SchedulerConfig (..),
import Test.Consensus.PeerSimulator.StateView (StateView (..))
import Test.Consensus.PeerSimulator.Trace (TraceEvent (..))
import Test.Consensus.PointSchedule
import Test.Consensus.PointSchedule.Peers (Peer (..), Peers (..),
mkPeers)
import Test.Consensus.PointSchedule.Peers (Peers (..), peers')
import Test.Tasty
import Test.Tasty.QuickCheck
import Test.Util.Orphans.IOLike ()
import Test.Util.PartialAccessors
import Test.Util.TestBlock (Header, TestBlock)
import Test.Util.TestEnv (adjustQuickCheckMaxSize)

Expand Down Expand Up @@ -63,14 +61,11 @@ tests =
prop_happyPath :: Bool -> Property
prop_happyPath synchronized =
forAllGenesisTest
( do
gt <- genChains $ pure 0
honest <- genHonestSchedule gt
numOthers <- choose (1, 3)
otherHonests <- if synchronized
then pure $ replicate numOthers honest
else replicateM numOthers (genHonestSchedule gt)
pure $ gt $> mkPeers honest otherHonests
( if synchronized
then genChainsWithExtraHonestPeers (choose (2, 4)) (pure 0)
`enrichedWith` genUniformSchedulePoints
else genChains (pure 0)
`enrichedWith` genDuplicatedHonestSchedule
)
( defaultSchedulerConfig
{ scEnableCSJ = True
Expand Down Expand Up @@ -119,13 +114,12 @@ prop_happyPath synchronized =
(receivedHeadersOnlyOnce && receivedHeadersFromOnlyOnePeer)
)
where
-- | This might seem wasteful, as we discard generated adversarial schedules.
-- It actually isn't, since we call it on trees that have no branches besides
-- the trunk, so no adversaries are generated.
genHonestSchedule :: GenesisTest TestBlock () -> Gen (PeerSchedule TestBlock)
genHonestSchedule gt = do
ps <- genUniformSchedulePoints gt
pure $ value $ honest ps
genDuplicatedHonestSchedule :: GenesisTest TestBlock () -> Gen (PeersSchedule TestBlock)
genDuplicatedHonestSchedule gt@GenesisTest{gtExtraHonestPeers} = do
Peers {honestPeers} <- genUniformSchedulePoints gt
pure $ peers'
(replicate (fromIntegral gtExtraHonestPeers + 1) (getHonestPeer honestPeers))
[]

isNewerThanJumpSizeFromTip :: GenesisTestFull TestBlock -> Header TestBlock -> Bool
isNewerThanJumpSizeFromTip gt hdr =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import Test.QuickCheck.Extras (unsafeMapSuchThatJust)
import Test.Tasty
import Test.Tasty.QuickCheck
import Test.Util.Orphans.IOLike ()
import Test.Util.PartialAccessors
import Test.Util.TersePrinting (terseHFragment, terseHeader)
import Test.Util.TestBlock (TestBlock)
import Test.Util.TestEnv (adjustQuickCheckMaxSize,
Expand Down Expand Up @@ -120,7 +121,7 @@ staticCandidates GenesisTest {gtSecurityParam, gtGenesisWindow, gtBlockTree} =
tips = branchTip <$> candidates

candidates :: Map PeerId (AnchoredFragment TestBlock)
candidates = Map.fromList (zip (HonestPeer : enumerateAdversaries) chains)
candidates = Map.fromList (zip (HonestPeer 1 : enumerateAdversaries) chains)

chains = btTrunk gtBlockTree : (btbFull <$> branches)

Expand All @@ -134,8 +135,8 @@ prop_densityDisconnectStatic =
let (disconnect, _) = densityDisconnect sgen k (mkState <$> suffixes) suffixes loeFrag
counterexample "it should disconnect some node" (not (null disconnect))
.&&.
counterexample "it should not disconnect the honest peer"
(HonestPeer `notElem` disconnect)
counterexample "it should not disconnect the honest peers"
(not $ any isHonestPeerId disconnect)
where
mkState :: AnchoredFragment (Header TestBlock) -> ChainSyncState TestBlock
mkState frag =
Expand Down Expand Up @@ -193,7 +194,7 @@ initCandidates GenesisTest {gtSecurityParam, gtGenesisWindow, gtBlockTree} =
fullTree = gtBlockTree
}
where
peers = mkPeers (peer trunk (AF.Empty (AF.headAnchor trunk)) (btTrunk gtBlockTree)) (branchPeer <$> branches)
peers = peers' [peer trunk (AF.Empty (AF.headAnchor trunk)) (btTrunk gtBlockTree)] (branchPeer <$> branches)

branchPeer branch = peer (btbPrefix branch) (btbSuffix branch) (btbFull branch)

Expand Down Expand Up @@ -230,8 +231,8 @@ data UpdateEvent = UpdateEvent {
}

snapshotTree :: Peers EvolvingPeer -> BlockTree (Header TestBlock)
snapshotTree Peers {honest, others} =
foldr addBranch' (mkTrunk (candidate (value honest))) (candidate . value <$> others)
snapshotTree Peers {honestPeers, adversarialPeers} =
foldr addBranch' (mkTrunk (candidate (getHonestPeer honestPeers))) (candidate <$> adversarialPeers)

prettyUpdateEvent :: UpdateEvent -> [String]
prettyUpdateEvent UpdateEvent {target, added, killed, bounds, tree, loeFrag, curChain} =
Expand Down Expand Up @@ -274,7 +275,7 @@ updatePeers ::
UpdateEvent ->
Either (MonotonicityResult, Peers EvolvingPeer) Evolution
updatePeers (GenesisWindow sgen) peers killedBefore event@UpdateEvent {target, killed = killedNow}
| HonestPeer `Set.member` killedNow
| HonestPeer 1 `Set.member` killedNow
= Left (HonestKilled, peers)
| not (null violations)
= Left (Nonmonotonic event, peers)
Expand All @@ -287,12 +288,12 @@ updatePeers (GenesisWindow sgen) peers killedBefore event@UpdateEvent {target, k
violations = killedBefore \\ killedNow

-- The new state if no violations were detected
evo@Evolution {peers = Peers {others = remaining}}
evo@Evolution {peers = Peers {adversarialPeers = remaining}}
| targetExhausted
-- If the target is done, reset the set of killed peers, since other peers
-- may have lost only against the target.
-- Remove the target from the active peers.
= Evolution {peers = peers {others = Map.delete target (others peers)}, killed = mempty}
= Evolution {peers = deletePeer target peers, killed = mempty}
| otherwise
-- Otherwise replace the killed peers with the current set
= Evolution {peers, killed = killedNow}
Expand All @@ -312,11 +313,11 @@ updatePeers (GenesisWindow sgen) peers killedBefore event@UpdateEvent {target, k
-- The selection will then be computed by taking up to k blocks after the immutable tip
-- on this peer's candidate fragment.
firstBranch :: Peers EvolvingPeer -> Peer EvolvingPeer
firstBranch Peers {honest, others} =
firstBranch peers =
fromMaybe newest $
minimumBy (compare `on` forkAnchor) <$> nonEmpty (filter hasForked (toList others))
minimumBy (compare `on` forkAnchor) <$> nonEmpty (filter hasForked (toList (adversarialPeers'' peers)))
where
newest = maximumBy (compare `on` (AF.headSlot . candidate . value)) (honest : toList others)
newest = maximumBy (compare `on` (AF.headSlot . candidate . value)) (toList (honestPeers'' peers) ++ toList (adversarialPeers'' peers))
forkAnchor = fromWithOrigin 0 . AF.anchorToSlotNo . AF.anchor . forkSuffix . value
hasForked Peer {value = EvolvingPeer {candidate, forkSlot}} =
AF.headSlot candidate >= forkSlot
Expand All @@ -325,7 +326,7 @@ firstBranch Peers {honest, others} =
-- for all peers, and then taking the earliest among the results.
immutableTip :: Peers EvolvingPeer -> AF.Point (Header TestBlock)
immutableTip peers =
minimum (lastHonest <$> toList (others peers))
minimum (lastHonest <$> toList (adversarialPeers'' peers))
where
lastHonest Peer {value = EvolvingPeer {candidate, forkSlot = NotOrigin forkSlot}} =
AF.headPoint $
Expand Down Expand Up @@ -470,7 +471,7 @@ prop_densityDisconnectTriggersChainSel =

( \GenesisTest {gtBlockTree, gtSchedule} stateView@StateView {svTipBlock} ->
let
othersCount = Map.size (others gtSchedule)
othersCount = Map.size (adversarialPeers gtSchedule)
exnCorrect = case exceptionsByComponent ChainSyncClient stateView of
[fromException -> Just DensityTooLow] -> True
[] | othersCount == 0 -> True
Expand All @@ -482,16 +483,6 @@ prop_densityDisconnectTriggersChainSel =
)

where
getOnlyBranch :: BlockTree blk -> BlockTreeBranch blk
getOnlyBranch BlockTree {btBranches} = case btBranches of
[branch] -> branch
_ -> error "tree must have exactly one alternate branch"

getTrunkTip :: HasHeader blk => BlockTree blk -> blk
getTrunkTip tree = case btTrunk tree of
(AF.Empty _) -> error "tree must have at least one block"
(_ AF.:> tipBlock) -> tipBlock

-- 1. The adversary advertises blocks up to the intersection.
-- 2. The honest node advertises all its chain, which is
-- long enough to be blocked by the LoE.
Expand All @@ -506,16 +497,14 @@ prop_densityDisconnectTriggersChainSel =
intersect = case btbPrefix branch of
(AF.Empty _) -> Origin
(_ AF.:> tipBlock) -> At tipBlock
advTip = case btbFull branch of
(AF.Empty _) -> error "alternate branch must have at least one block"
(_ AF.:> tipBlock) -> tipBlock
in mkPeers
advTip = getOnlyBranchTip tree
in peers'
-- Eagerly serve the honest tree, but after the adversary has
-- advertised its chain up to the intersection.
[ (Time 0, scheduleTipPoint trunkTip),
[[(Time 0, scheduleTipPoint trunkTip),
(Time 0.5, scheduleHeaderPoint trunkTip),
(Time 0.5, scheduleBlockPoint trunkTip)
]
]]
-- Advertise the alternate branch early, but wait for the honest
-- node to have served its chain before disclosing the alternate
-- branch is not dense enough.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ import Test.Consensus.PeerSimulator.Run (SchedulerConfig (..),
defaultSchedulerConfig)
import Test.Consensus.PeerSimulator.StateView
import Test.Consensus.PointSchedule
import Test.Consensus.PointSchedule.Peers (Peers, mkPeers)
import Test.Consensus.PointSchedule.Peers (Peers, peers')
import Test.Consensus.PointSchedule.Shrinking (shrinkPeerSchedules)
import Test.Consensus.PointSchedule.SinglePeer (scheduleBlockPoint,
scheduleHeaderPoint, scheduleTipPoint)
import Test.Tasty
import Test.Tasty.QuickCheck
import Test.Util.Orphans.IOLike ()
import Test.Util.PartialAccessors
import Test.Util.TestEnv (adjustQuickCheckTests)

tests :: TestTree
Expand Down Expand Up @@ -76,27 +77,18 @@ prop_adversaryHitsTimeouts timeoutsEnabled =
in selectedCorrect && exceptionsCorrect
)
where
getOnlyBranch :: BlockTree blk -> BlockTreeBranch blk
getOnlyBranch BlockTree {btBranches} = case btBranches of
[branch] -> branch
_ -> error "tree must have exactly one alternate branch"

delaySchedule :: HasHeader blk => BlockTree blk -> Peers (PeerSchedule blk)
delaySchedule tree =
let trunkTip = case btTrunk tree of
(AF.Empty _) -> error "tree must have at least one block"
(_ AF.:> tipBlock) -> tipBlock
let trunkTip = getTrunkTip tree
branch = getOnlyBranch tree
intersectM = case btbPrefix branch of
(AF.Empty _) -> Nothing
(_ AF.:> tipBlock) -> Just tipBlock
branchTip = case btbFull branch of
(AF.Empty _) -> error "alternate branch must have at least one block"
(_ AF.:> tipBlock) -> tipBlock
in mkPeers
branchTip = getOnlyBranchTip tree
in peers'
-- Eagerly serve the honest tree, but after the adversary has
-- advertised its chain.
( (Time 0, scheduleTipPoint trunkTip) : case intersectM of
[ (Time 0, scheduleTipPoint trunkTip) : case intersectM of
Nothing ->
[ (Time 0.5, scheduleHeaderPoint trunkTip),
(Time 0.5, scheduleBlockPoint trunkTip)
Expand All @@ -107,7 +99,7 @@ prop_adversaryHitsTimeouts timeoutsEnabled =
(Time 5, scheduleHeaderPoint trunkTip),
(Time 5, scheduleBlockPoint trunkTip)
]
)
]
-- The one adversarial peer advertises and serves up to the
-- intersection early, then waits more than the short wait timeout.
[ (Time 0, scheduleTipPoint branchTip) : case intersectM of
Expand Down
Loading

0 comments on commit a969b37

Please sign in to comment.