33
44{-# HLINT ignore "Use fewer imports" #-}
55
6- -- | Generic utils for using `quickcheck-dynamic`
7- module Cardano.CEM.Testing.StateMachine where
6+ {- | Model-based testing based on `quickcheck-dynamic`.
7+ Main purpose of this kind of testing is to ensure that
8+ OnChain code works the same way OffChain code does.
9+ Additinally custom user invariants for OffChain code
10+ can be tested using 'CEMScriptRunModel' type class.
11+ -}
12+ module Cardano.CEM.Testing.StateMachine (
13+ -- * Model
14+ ScriptState (.. ),
15+ ScriptStateParams (.. ),
16+ TestConfig (.. ),
17+ Action (.. ),
18+ CEMScriptArbitrary (.. ),
19+
20+ -- * SUT Implementation utils
21+ CEMScriptRunModel (.. ),
22+ runActionsInClb ,
23+ findSkForPKH ,
24+ ) where
825
926import Cardano.Api (PaymentKey , SigningKey , TxId , Value )
1027import Cardano.CEM.DSL (
1128 CEMScript ,
1229 CEMScriptTypes (Params , State , Transition ),
1330 SameScriptArg (MkSameScriptArg ),
14- TxConstraint (Utxo ),
31+ TxConstraint (Noop , Utxo ),
1532 Utxo (SameScript ),
1633 UtxoKind (Out ),
1734 getMainSigner ,
35+ getMbMainSigner ,
1836 )
1937import Cardano.CEM.Monads (
2038 BlockchainMonadEvent (.. ),
@@ -27,7 +45,7 @@ import Cardano.CEM.Monads (
2745 )
2846import Cardano.CEM.Monads.CLB (ClbRunner , execOnIsolatedClb )
2947import Cardano.CEM.OffChain (
30- TxResolutionError (UnhandledSubmittingError ),
48+ TxResolutionError (CEMScriptTxInResolutionError , UnhandledSubmittingError ),
3149 compileActionConstraints ,
3250 construct ,
3351 process ,
@@ -69,18 +87,36 @@ import Prelude
6987-- Mutations
7088-- -----------------------------------------------------------------------------
7189
90+ -- We use mutations to verify that on-chain and off-chain implementations
91+ -- work the same way:
92+ -- 1. The order of constrainsts doesn't matter
93+ -- 2. All non-noop constraints are important - if we remove them both impls stop working.
94+
7295data TxMutation
7396 = RemoveConstraint { num :: Int }
74- | ShuffleConstraints
75- { shift :: Int }
97+ | ShuffleConstraints { shift :: Int }
7698 deriving stock (Eq , Show )
7799
78100deriveSpine ''TxMutation
79101
80- isNegativeMutation :: Maybe TxMutation -> Bool
81- isNegativeMutation Nothing = False
82- isNegativeMutation (Just (RemoveConstraint _)) = True
83- isNegativeMutation (Just (ShuffleConstraints {})) = False
102+ isNegativeMutation :: Maybe TxMutation -> [TxConstraint True script ] -> Bool
103+ isNegativeMutation Nothing _ = False
104+ isNegativeMutation m@ (Just (RemoveConstraint {})) cs =
105+ case applyMutation m cs of
106+ (_, Just Noop ) -> False
107+ _ -> True
108+ isNegativeMutation (Just (ShuffleConstraints {})) _ = False
109+
110+ applyMutation ::
111+ Maybe TxMutation ->
112+ [TxConstraint True script ] ->
113+ ([TxConstraint True script ], Maybe (TxConstraint True script ))
114+ applyMutation Nothing cs = (cs, Nothing )
115+ -- \| Removes num+1 element from the list of constraints
116+ applyMutation (Just (RemoveConstraint num)) cs =
117+ (take num cs ++ tail (drop num cs), Just (cs !! num))
118+ applyMutation (Just (ShuffleConstraints shift)) cs =
119+ (permute shift cs, Nothing )
84120
85121permute :: Int -> [a ] -> [a ]
86122permute ind as =
@@ -89,16 +125,6 @@ permute ind as =
89125 perms = permutations as
90126 n = length perms
91127
92- applyMutation ::
93- Maybe TxMutation ->
94- [TxConstraint True script ] ->
95- [TxConstraint True script ]
96- applyMutation Nothing cs = cs
97- applyMutation (Just (RemoveConstraint _num)) cs =
98- -- take num cs ++ tail (drop num cs)
99- cs
100- applyMutation (Just (ShuffleConstraints shift)) cs = permute shift cs
101-
102128-- -----------------------------------------------------------------------------
103129-- The model
104130-- -----------------------------------------------------------------------------
@@ -144,12 +170,17 @@ instance {-# OVERLAPS #-} HasVariables (Action (ScriptState script) a) where
144170-- CEMScriptArbitrary & StateModel instance
145171-- -----------------------------------------------------------------------------
146172
173+ -- | Arbitrary for a CEM Script (compiled).
147174class (CEMScriptCompiled script ) => CEMScriptArbitrary script where
148175 arbitraryParams ::
149176 [SigningKey PaymentKey ] -> Gen (Params script )
150177 arbitraryTransition ::
151178 ScriptStateParams script -> Maybe (State script ) -> Gen (Transition script )
152179
180+ {- | StateModel, which is QD model is basically our off-chain logic.
181+ It delivers checks to `compileActionConstraints` from "Cardano.CEM.Offchain"
182+ module. So `model === offchain`.
183+ -}
153184instance (CEMScriptArbitrary script ) => StateModel (ScriptState script ) where
154185 data Action (ScriptState script ) output where
155186 SetupConfig :: TestConfig -> Action (ScriptState script ) ()
@@ -174,31 +205,40 @@ instance (CEMScriptArbitrary script) => StateModel (ScriptState script) where
174205 ScriptState script ->
175206 Gen (Any (Action (ScriptState script )))
176207 arbitraryAction _varCtx modelState = case modelState of
177- -- SetupConfig action should be called manually
208+ -- SetupConfig action should be always called manually
178209 Void {} -> Gen. oneof []
179210 ConfigSet config -> Some . SetupParams <$> arbitraryParams (actors config)
180- ScriptState {dappParams, state} ->
181- do
182- transition <- arbitraryTransition dappParams state
183- Some <$> (ScriptTransition transition <$> genMutation transition)
184- where
185- genMutation :: Transition script -> Gen (Maybe TxMutation )
186- genMutation transition =
187- return Nothing
188-
189- -- let cemAction = MkCEMAction (params dappParams) transition
190- -- in case compileActionConstraints state cemAction of
191- -- Right cs ->
192- -- Gen.oneof
193- -- [ return Nothing
194- -- , Just . RemoveConstraint
195- -- <$> Gen.chooseInt (0, length cs - 1)
196- -- , Just
197- -- <$> ( ShuffleConstraints
198- -- <$> Gen.chooseInt (1, length cs)
199- -- )
200- -- ]
201- -- Left _ -> return Nothing
211+ ScriptState
212+ { dappParams =
213+ dappParams@ MkScriptStateParams
214+ { config = MkTestConfig {doMutationTesting}
215+ }
216+ , state
217+ } ->
218+ do
219+ transition <- arbitraryTransition dappParams state
220+ Some <$> (ScriptTransition transition <$> genMutation transition)
221+ where
222+ genMutation :: Transition script -> Gen (Maybe TxMutation )
223+ genMutation transition =
224+ if doMutationTesting
225+ then mutate
226+ else return Nothing
227+ where
228+ mutate =
229+ let cemAction = MkCEMAction (params dappParams) transition
230+ in case compileActionConstraints state cemAction of
231+ Right cs ->
232+ Gen. oneof
233+ [ return Nothing
234+ , Just . RemoveConstraint
235+ <$> Gen. chooseInt (0 , length cs - 1 )
236+ , Just
237+ <$> ( ShuffleConstraints
238+ <$> Gen. chooseInt (1 , length cs)
239+ )
240+ ]
241+ Left _ -> return Nothing
202242
203243 precondition ::
204244 (CEMScriptArbitrary script ) =>
@@ -215,7 +255,7 @@ instance (CEMScriptArbitrary script) => StateModel (ScriptState script) where
215255 compiled = compileActionConstraints state cemAction
216256 in
217257 case compiled of
218- Right _ -> not finished && not (isNegativeMutation mutation) -- FIXME: why not isNegative
258+ Right cs -> not finished && not (isNegativeMutation mutation cs)
219259 Left _ -> False
220260 -- Unreachable
221261 precondition _ _ = False
@@ -262,7 +302,8 @@ instance (CEMScriptArbitrary script) => StateModel (ScriptState script) where
262302 f _ = Nothing
263303 nextState _ _ _ = error " Unreachable"
264304
265- -- Precondition for filtering an Action that can meaningfully run but is supposed to fail.
305+ -- Precondition for filtering an Action that can meaningfully run
306+ -- but is supposed to fail.
266307 -- An action will run as a _negative_ action if the 'precondition' fails and
267308 -- 'validFailingAction' succeeds.
268309 -- A negative action should have _no effect_ on the model state.
@@ -273,8 +314,14 @@ instance (CEMScriptArbitrary script) => StateModel (ScriptState script) where
273314 ScriptState script ->
274315 Action (ScriptState script ) a ->
275316 Bool
276- validFailingAction (ScriptState {finished, state}) (ScriptTransition _ mutation) =
277- isNegativeMutation mutation && isJust state && not finished
317+ validFailingAction
318+ (ScriptState {dappParams, finished, state})
319+ (ScriptTransition transition mutation) =
320+ let cemAction = MkCEMAction (params dappParams) transition
321+ cs' = compileActionConstraints state cemAction
322+ in case cs' of
323+ Left _ -> True
324+ Right cs -> isNegativeMutation mutation cs && isJust state && not finished
278325 validFailingAction _ _ = False
279326
280327instance (CEMScriptArbitrary script ) => Show (Action (ScriptState script ) a ) where
@@ -303,6 +350,10 @@ class (CEMScriptArbitrary script) => CEMScriptRunModel script where
303350 Action (ScriptState script ) () ->
304351 m ()
305352
353+ {- | The SUT implementation is CLB-backed blockchain emulator. Here we execute
354+ both offchain part and the onchain part also. That way we can assume that
355+ `implementation === onchain`.
356+ -}
306357instance
307358 ( Realized m () ~ ()
308359 , MonadIO m
@@ -336,20 +387,19 @@ instance
336387 , ScriptTransition transition mutation
337388 ) -> do
338389 _ <- performHook modelState action
339- bimap show (const () ) <$> mutatedResolveAndSubmit
390+ bimap show (const () ) <$> mutateResolveAndSubmit
340391 where
341- -- This should work like `resolveAndSubmit`
342- -- FIXME: DRY it and move Mutations to main implementation
343- mutatedResolveAndSubmit :: m (Either TxResolutionError TxId )
344- mutatedResolveAndSubmit = runExceptT $ do
392+ mutateResolveAndSubmit :: m (Either TxResolutionError TxId )
393+ mutateResolveAndSubmit = runExceptT $ do
345394 let cemAction = MkCEMAction (params dappParams) transition
346- -- FIXME: refactor all ExceptT mess
347395 cs' <- ExceptT $ return $ compileActionConstraints state cemAction
348396 let
349- cs = applyMutation mutation cs'
350- signerPKH = getMainSigner cs
351- specSigner =
352- findSkForPKH (actors $ config dappParams) signerPKH
397+ (cs, _) = applyMutation mutation cs'
398+ mbSignerPKH = getMbMainSigner cs
399+ -- \| FIXME: can we delegate handling Nothing case to process/construct?
400+ specSigner <- case mbSignerPKH of
401+ Nothing -> ExceptT $ pure $ Left CEMScriptTxInResolutionError -- FIXME:
402+ Just signerPKH -> pure $ findSkForPKH (actors $ config dappParams) signerPKH
353403 resolutions <- mapM (process cemAction) cs
354404 let resolvedTx = (construct resolutions) {signer = specSigner}
355405 result <-
@@ -360,7 +410,7 @@ instance
360410 logEvent $
361411 SubmittedTxSpec spec (mapLeft (const () ) result)
362412 ExceptT $ return result
363- (_, _) -> error " Unreachable"
413+ (_, _) -> error " Unreachable branch of `perform` "
364414
365415 monitoring ::
366416 ( Realized m () ~ ()
0 commit comments