|
| 1 | +{-# LANGUAGE LambdaCase #-} |
| 2 | +{-# LANGUAGE NamedFieldPuns #-} |
| 3 | +{-# LANGUAGE ScopedTypeVariables #-} |
| 4 | + |
| 5 | +-- | Narrow tests for ChainSync Jumping |
| 6 | +module Test.Consensus.MiniProtocol.ChainSync.CSJ (tests) where |
| 7 | + |
| 8 | +import qualified Control.Concurrent.Class.MonadSTM.Strict.TVar as TVar |
| 9 | +import Control.Monad (void) |
| 10 | +import Control.Monad.Class.MonadTimer (MonadTimer) |
| 11 | +import Control.Monad.IOSim (runSim) |
| 12 | +import Control.ResourceRegistry |
| 13 | +import Control.Tracer (nullTracer) |
| 14 | +import Data.Typeable |
| 15 | +import Network.TypedProtocol.Channel |
| 16 | +import Network.TypedProtocol.Driver.Simple |
| 17 | +import Ouroboros.Consensus.Block |
| 18 | +import Ouroboros.Consensus.BlockchainTime |
| 19 | +import Ouroboros.Consensus.Config |
| 20 | +import qualified Ouroboros.Consensus.HeaderStateHistory as HeaderStateHistory |
| 21 | +import Ouroboros.Consensus.Ledger.Tables.Utils (forgetLedgerTables) |
| 22 | +import Ouroboros.Consensus.MiniProtocol.ChainSync.Client |
| 23 | + (CSJConfig (..), CSJEnabledConfig (..), ChainDbView (..), |
| 24 | + ChainSyncLoPBucketConfig (..), ChainSyncStateView (..), |
| 25 | + ConfigEnv (..), Consensus, DynamicEnv (..), |
| 26 | + bracketChainSyncClient, chainSyncClient, |
| 27 | + newChainSyncClientHandleCollection) |
| 28 | +import qualified Ouroboros.Consensus.MiniProtocol.ChainSync.Client.HistoricityCheck as HistoricityCheck |
| 29 | +import qualified Ouroboros.Consensus.MiniProtocol.ChainSync.Client.InFutureCheck as InFutureCheck |
| 30 | +import qualified Ouroboros.Consensus.Node.GsmState as GSM |
| 31 | +import Ouroboros.Consensus.Node.NetworkProtocolVersion |
| 32 | + (NodeToNodeVersion) |
| 33 | +import Ouroboros.Consensus.NodeId |
| 34 | +import Ouroboros.Consensus.Util.IOLike |
| 35 | +import Ouroboros.Consensus.Util.STM (Fingerprint (..), |
| 36 | + WithFingerprint (..)) |
| 37 | +import qualified Ouroboros.Network.AnchoredFragment as AF |
| 38 | +import Ouroboros.Network.ControlMessage (ControlMessage (..)) |
| 39 | +import qualified Ouroboros.Network.Mock.Chain as MockChain |
| 40 | +import Ouroboros.Network.Protocol.ChainSync.ClientPipelined |
| 41 | +import Ouroboros.Network.Protocol.ChainSync.Codec (codecChainSyncId) |
| 42 | +import Ouroboros.Network.Protocol.ChainSync.Examples |
| 43 | +import Ouroboros.Network.Protocol.ChainSync.PipelineDecision |
| 44 | + (pipelineDecisionLowHighMark) |
| 45 | +import Ouroboros.Network.Protocol.ChainSync.Server |
| 46 | +import Test.QuickCheck |
| 47 | +import Test.Tasty |
| 48 | +import Test.Tasty.QuickCheck |
| 49 | +import Test.Util.Orphans.Arbitrary () |
| 50 | +import Test.Util.Orphans.IOLike () |
| 51 | +import Test.Util.TestBlock |
| 52 | + |
| 53 | +{------------------------------------------------------------------------------- |
| 54 | + Top-level tests |
| 55 | +-------------------------------------------------------------------------------} |
| 56 | + |
| 57 | +tests :: TestTree |
| 58 | +tests = testGroup "Narrow CSJ" |
| 59 | + [ testProperty "CaughtUp" prop_CaughtUpCsj |
| 60 | + ] |
| 61 | + |
| 62 | +{------------------------------------------------------------------------------- |
| 63 | + Main property |
| 64 | +-------------------------------------------------------------------------------} |
| 65 | + |
| 66 | +data TestSetup = |
| 67 | + -- | This test is very simple for now, since it's so far merely a |
| 68 | + -- regression test of a simple property we're very surprised made it past |
| 69 | + -- code review. |
| 70 | + TestSetup |
| 71 | + deriving (Read, Show) |
| 72 | + |
| 73 | +instance Arbitrary TestSetup where |
| 74 | + arbitrary = pure TestSetup |
| 75 | + |
| 76 | +-- | When the node is CaughtUp, CSJ should be disabled. |
| 77 | +-- |
| 78 | +-- The test checks for that by testing whether two upstream peers joining |
| 79 | +-- causes them both to promptly receive @MsgRequestNext@. If CSJ were enabled, |
| 80 | +-- then instead only the first peer to join would promptly receive |
| 81 | +-- @MsgRequestNext@. |
| 82 | +prop_CaughtUpCsj :: TestSetup -> Property |
| 83 | +prop_CaughtUpCsj testSetup = |
| 84 | + case runSim $ runTest testSetup of |
| 85 | + |
| 86 | + Left exn -> |
| 87 | + counterexample ("`runTest' threw an exception: " <> show exn) |
| 88 | + $ property False |
| 89 | + |
| 90 | + Right results -> |
| 91 | + counterexample "At least one peer did not receive MsgRequestNext" |
| 92 | + $ results === (HasReceived, HasReceived) |
| 93 | + |
| 94 | +data WhetherReceivedMsgNextRequest = |
| 95 | + HasNotYetReceived |
| 96 | + | |
| 97 | + HasReceived |
| 98 | + deriving (Eq, Show) |
| 99 | + |
| 100 | +runTest :: forall m. |
| 101 | + ( |
| 102 | + IOLike m |
| 103 | + , |
| 104 | + MonadTimer m |
| 105 | + ) |
| 106 | + => TestSetup |
| 107 | + -> m (WhetherReceivedMsgNextRequest, WhetherReceivedMsgNextRequest) |
| 108 | +runTest TestSetup = withRegistry $ \registry -> do |
| 109 | + |
| 110 | + -- The "Ouroboros.Consensus.NodeKernel" does not do anything more than this |
| 111 | + -- in order to "initialize" CSJ. |
| 112 | + varHandles <- atomically newChainSyncClientHandleCollection |
| 113 | + |
| 114 | + let chainDbView :: ChainDbView m TestBlock |
| 115 | + chainDbView = ChainDbView { |
| 116 | + getCurrentChain = pure $ AF.Empty AF.AnchorGenesis |
| 117 | + , |
| 118 | + getHeaderStateHistory = |
| 119 | + pure |
| 120 | + $ HeaderStateHistory.fromChain |
| 121 | + topLevelCfg |
| 122 | + testInitExtLedger |
| 123 | + MockChain.Genesis |
| 124 | + , |
| 125 | + getPastLedger = pure . \case |
| 126 | + GenesisPoint -> Just $ forgetLedgerTables testInitExtLedger |
| 127 | + BlockPoint{} -> Nothing |
| 128 | + , |
| 129 | + getIsInvalidBlock = |
| 130 | + pure $ WithFingerprint (\_hash -> Nothing) (Fingerprint 0) |
| 131 | + } |
| 132 | + |
| 133 | + version :: NodeToNodeVersion |
| 134 | + version = maxBound |
| 135 | + |
| 136 | + lopBucketConfig :: ChainSyncLoPBucketConfig |
| 137 | + lopBucketConfig = ChainSyncLoPBucketDisabled |
| 138 | + |
| 139 | + csjConfig :: CSJEnabledConfig |
| 140 | + csjConfig = CSJEnabledConfig { csjcJumpSize = SlotNo 10000 } |
| 141 | + |
| 142 | + diffusionPipelining :: DiffusionPipeliningSupport |
| 143 | + diffusionPipelining = DiffusionPipeliningOn |
| 144 | + |
| 145 | + headerInFutureCheck :: |
| 146 | + InFutureCheck.SomeHeaderInFutureCheck m TestBlock |
| 147 | + headerInFutureCheck = |
| 148 | + InFutureCheck.SomeHeaderInFutureCheck |
| 149 | + $ InFutureCheck.HeaderInFutureCheck { |
| 150 | + InFutureCheck.proxyArrival = Proxy :: Proxy () |
| 151 | + , |
| 152 | + InFutureCheck.recordHeaderArrival = \_ -> pure () |
| 153 | + , |
| 154 | + InFutureCheck.judgeHeaderArrival = |
| 155 | + \_lcfg _lstate () -> pure () |
| 156 | + , |
| 157 | + InFutureCheck.handleHeaderArrival = |
| 158 | + \() -> pure $ pure $ RelativeTime 0 |
| 159 | + } |
| 160 | + |
| 161 | + mkClient :: |
| 162 | + ChainSyncStateView m TestBlock |
| 163 | + -> Consensus ChainSyncClientPipelined TestBlock m |
| 164 | + mkClient csv = |
| 165 | + let ChainSyncStateView { |
| 166 | + csvSetCandidate |
| 167 | + , |
| 168 | + csvSetLatestSlot |
| 169 | + , |
| 170 | + csvIdling |
| 171 | + , |
| 172 | + csvLoPBucket |
| 173 | + , |
| 174 | + csvJumping |
| 175 | + } = csv |
| 176 | + in |
| 177 | + chainSyncClient |
| 178 | + ConfigEnv { |
| 179 | + chainDbView |
| 180 | + , |
| 181 | + cfg = topLevelCfg |
| 182 | + , |
| 183 | + tracer = nullTracer |
| 184 | + , |
| 185 | + someHeaderInFutureCheck = headerInFutureCheck |
| 186 | + , |
| 187 | + historicityCheck = HistoricityCheck.noCheck |
| 188 | + , |
| 189 | + mkPipelineDecision0 = pipelineDecisionLowHighMark 10 20 |
| 190 | + , |
| 191 | + getDiffusionPipeliningSupport = diffusionPipelining |
| 192 | + } |
| 193 | + DynamicEnv { |
| 194 | + version |
| 195 | + , |
| 196 | + controlMessageSTM = return Continue |
| 197 | + , |
| 198 | + headerMetricsTracer = nullTracer |
| 199 | + , |
| 200 | + setCandidate = csvSetCandidate |
| 201 | + , |
| 202 | + idling = csvIdling |
| 203 | + , |
| 204 | + loPBucket = csvLoPBucket |
| 205 | + , |
| 206 | + setLatestSlot = csvSetLatestSlot |
| 207 | + , |
| 208 | + jumping = csvJumping |
| 209 | + } |
| 210 | + |
| 211 | + bracketedClient :: |
| 212 | + CoreNodeId |
| 213 | + -> (Consensus ChainSyncClientPipelined TestBlock m -> m a) |
| 214 | + -> m a |
| 215 | + bracketedClient peer k = |
| 216 | + bracketChainSyncClient |
| 217 | + nullTracer |
| 218 | + nullTracer |
| 219 | + chainDbView |
| 220 | + varHandles |
| 221 | + (pure GSM.CaughtUp) |
| 222 | + peer |
| 223 | + version |
| 224 | + lopBucketConfig |
| 225 | + (CSJEnabled csjConfig) |
| 226 | + diffusionPipelining |
| 227 | + (k . mkClient) |
| 228 | + |
| 229 | + spawnConnection :: |
| 230 | + CoreNodeId |
| 231 | + -> m (TVar.StrictTVar m WhetherReceivedMsgNextRequest) |
| 232 | + spawnConnection peer = do |
| 233 | + var <- TVar.newTVarIO HasNotYetReceived |
| 234 | + (clientChannel, serverChannel) <- createConnectedChannels |
| 235 | + void $ forkLinkedThread registry ("client " <> show peer) $ do |
| 236 | + bracketedClient peer $ \client -> do |
| 237 | + runPipelinedPeer |
| 238 | + nullTracer |
| 239 | + codecChainSyncId |
| 240 | + clientChannel |
| 241 | + (chainSyncClientPeerPipelined client) |
| 242 | + void $ forkLinkedThread registry ("server " <> show peer) $ do |
| 243 | + runPeer |
| 244 | + nullTracer |
| 245 | + codecChainSyncId |
| 246 | + serverChannel |
| 247 | + $ chainSyncServerPeer |
| 248 | + $ server |
| 249 | + $ atomically (TVar.writeTVar var HasReceived) |
| 250 | + pure var |
| 251 | + |
| 252 | + var1 <- spawnConnection $ CoreNodeId 1 |
| 253 | + var2 <- spawnConnection $ CoreNodeId 2 |
| 254 | + |
| 255 | + threadDelay testDuration |
| 256 | + |
| 257 | + atomically $ (,) <$> TVar.readTVar var1 <*> TVar.readTVar var2 |
| 258 | + |
| 259 | +-- | How long the test runs for |
| 260 | +-- |
| 261 | +-- The only time-sensitive thing in this test is the 'Exhausted' exception in |
| 262 | +-- 'server', so as long as that happens after this duration, time should be |
| 263 | +-- irrelevant. |
| 264 | +testDuration :: Num a => a |
| 265 | +testDuration = 100 |
| 266 | + |
| 267 | +server :: |
| 268 | + IOLike m |
| 269 | + => m () |
| 270 | + -- ^ action to perform on the first @MsgRequestNext@, after which this |
| 271 | + -- peer becomes unresponsive |
| 272 | + -> ChainSyncServer (Header TestBlock) (Point TestBlock) (Tip TestBlock) m () |
| 273 | +server onFirstMsgRequestNext = |
| 274 | + go |
| 275 | + where |
| 276 | + dummyTip = Tip (SlotNo 1000) (testHashFromList [0]) (BlockNo 1000) |
| 277 | + -- inconsequential for this test |
| 278 | + |
| 279 | + go = ChainSyncServer $ pure $ ServerStIdle { |
| 280 | + recvMsgRequestNext = do |
| 281 | + onFirstMsgRequestNext |
| 282 | + threadDelay $ testDuration + 1 |
| 283 | + throwIO Exhausted |
| 284 | + , |
| 285 | + recvMsgFindIntersect = \_points -> |
| 286 | + pure $ SendMsgIntersectFound GenesisPoint dummyTip go |
| 287 | + , |
| 288 | + recvMsgDoneClient = throwIO UnexpectedTermination |
| 289 | + } |
| 290 | + |
| 291 | +data TestException = |
| 292 | + -- | The test ran for longer than it expected to; see 'testDuration' |
| 293 | + Exhausted |
| 294 | + | |
| 295 | + -- | A peer received @MsgDone@, which shouldn't happen in this test |
| 296 | + UnexpectedTermination |
| 297 | + deriving (Eq, Show) |
| 298 | + |
| 299 | +instance Exception TestException |
| 300 | + |
| 301 | +-- | This data structure contains a lot of values that are inconsequential for |
| 302 | +-- this test, especially since this test doesn't actually involve any blocks |
| 303 | +topLevelCfg :: TopLevelConfig TestBlock |
| 304 | +topLevelCfg = singleNodeTestConfig |
0 commit comments