Skip to content

Commit 836389a

Browse files
author
euonymos
committed
feat: rethink negative mutations
1 parent 6fece23 commit 836389a

File tree

5 files changed

+155
-69
lines changed

5 files changed

+155
-69
lines changed

src/Cardano/CEM/DSL.hs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module Cardano.CEM.DSL (
1717
UtxoKind (..),
1818
SameScriptArg (..),
1919
getMainSigner,
20+
getMbMainSigner,
2021

2122
-- * DSL
2223
ConstraintDSL (..),
@@ -304,6 +305,18 @@ getMainSigner cs = case mapMaybe f cs of
304305
f (MainSignerCoinSelect pkh _ _) = Just pkh
305306
f _ = Nothing
306307

308+
getMbMainSigner :: [TxConstraint True script] -> Maybe PubKeyHash
309+
getMbMainSigner cs = case mapMaybe f cs of
310+
[] -> Nothing
311+
[pkh] -> Just pkh
312+
_ ->
313+
error
314+
"Transition should have exactly one MainSigner* constraint"
315+
where
316+
f (MainSignerNoValue pkh) = Just pkh
317+
f (MainSignerCoinSelect pkh _ _) = Just pkh
318+
f _ = Nothing
319+
307320
-- -----------------------------------------------------------------------------
308321
-- Main CEM Script API
309322
-- -----------------------------------------------------------------------------

src/Cardano/CEM/Monads.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ data TxSubmittingError
124124

125125
-- | Error occurred while trying to execute CEMScript transition
126126
data TransitionError
127-
= CannotFindTransitionInput
127+
= CannotFindTransitionInput String
128128
| CompilationError String
129129
| SpecExecutionError {errorMessage :: String}
130130
deriving stock (Show, Eq)

src/Cardano/CEM/OffChain.hs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import Data.Map qualified as Map
5757
import Data.Maybe (fromJust)
5858
import Data.Singletons (sing)
5959
import Data.Spine (HasSpine (..))
60+
import Debug.Trace (traceShowId)
6061
import Plutarch (Config (..), (#))
6162
import Plutarch.Evaluate (evalTerm)
6263
import Plutarch.Lift (pconstant, plift)
@@ -138,8 +139,7 @@ construct rs = constructGo rs emptyResolvedTx
138139
, toMint = TxMintNone
139140
, additionalSigners = []
140141
, signer = error "TODO: Unreachable laziness trick"
141-
, -- FIXME
142-
interval = always
142+
, interval = always -- TODO: use validity intervals
143143
}
144144
constructGo (r : rest) !acc =
145145
let newAcc = case r of
@@ -197,7 +197,7 @@ process (MkCEMAction params transition) ec = case ec of
197197
map (withKeyWitness . fst) $ Map.toList $ unUTxO utxo
198198
-- FIXME: do actuall coin selection
199199
return $ TxInR $ head utxoPairs
200-
(Utxo kind fanFilter value) -> do
200+
c@(Utxo kind fanFilter value) -> do
201201
case kind of
202202
Out -> do
203203
let value' = convertTxOut $ fromPlutusValue value
@@ -208,6 +208,7 @@ process (MkCEMAction params transition) ec = case ec of
208208
someIn -> do
209209
utxo <- lift $ queryUtxo $ ByAddresses [address]
210210
let
211+
-- utxoPairs = traceShowId $ Map.toList $ unUTxO utxo
211212
utxoPairs = Map.toList $ unUTxO utxo
212213
matchingUtxos =
213214
map (addWittness . fst) $ filter predicate utxoPairs
@@ -217,7 +218,7 @@ process (MkCEMAction params transition) ec = case ec of
217218
In -> TxInR x
218219
InRef -> TxInRefR x
219220
[] ->
220-
throwError $ PerTransitionErrors [CannotFindTransitionInput]
221+
throwError $ PerTransitionErrors [CannotFindTransitionInput $ show c]
221222
where
222223
predicate (_, txOut) =
223224
txOutValue txOut == fromPlutusValue value

src/Cardano/CEM/Testing/StateMachine.hs

Lines changed: 108 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,36 @@
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

926
import Cardano.Api (PaymentKey, SigningKey, TxId, Value)
1027
import 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
)
1937
import Cardano.CEM.Monads (
2038
BlockchainMonadEvent (..),
@@ -27,7 +45,7 @@ import Cardano.CEM.Monads (
2745
)
2846
import Cardano.CEM.Monads.CLB (ClbRunner, execOnIsolatedClb)
2947
import 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+
7295
data TxMutation
7396
= RemoveConstraint {num :: Int}
74-
| ShuffleConstraints
75-
{shift :: Int}
97+
| ShuffleConstraints {shift :: Int}
7698
deriving stock (Eq, Show)
7799

78100
deriveSpine ''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

85121
permute :: Int -> [a] -> [a]
86122
permute 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).
147174
class (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+
-}
153184
instance (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

280327
instance (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+
-}
306357
instance
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

Comments
 (0)