@@ -106,6 +106,12 @@ module Plutus.Contract.Test.ContractModel
106106 , TestStep (.. )
107107 , FailedStep (.. )
108108 , withDLTest
109+
110+ -- ** Standard properties
111+ --
112+ -- $noLockedFunds
113+ , NoLockedFundsProof (.. )
114+ , checkNoLockedFundsProof
109115 ) where
110116
111117import Control.Lens
@@ -121,21 +127,25 @@ import Data.Row (Row)
121127import Data.Typeable
122128
123129import Ledger.Slot
124- import Ledger.Value (Value )
125- import Plutus.Contract (Contract )
130+ import Ledger.Value (Value , isZero , leq )
131+ import Plutus.Contract (Contract , ContractInstanceId )
126132import Plutus.Contract.Test
127- import Plutus.Trace.Emulator as Trace (ContractHandle , ContractInstanceTag , EmulatorTrace ,
128- activateContract , walletInstanceTag )
133+ import Plutus.Trace.Effects.EmulatorControl (discardWallets )
134+ import Plutus.Trace.Emulator as Trace (ContractHandle (.. ), ContractInstanceTag ,
135+ EmulatorTrace , activateContract ,
136+ freezeContractInstance , walletInstanceTag )
129137import PlutusTx.Monoid (inv )
130138import qualified Test.QuickCheck.DynamicLogic.Monad as DL
131139import Test.QuickCheck.DynamicLogic.Quantify (Quantifiable (.. ), Quantification , arbitraryQ , chooseQ ,
132140 elementsQ , exactlyQ , frequencyQ , mapQ , oneofQ , whereQ )
133141import Test.QuickCheck.StateModel hiding (Action , Actions , arbitraryAction , initialState ,
134- monitoring , nextState , perform , precondition , shrinkAction )
142+ monitoring , nextState , perform , precondition , shrinkAction ,
143+ stateAfter )
135144import qualified Test.QuickCheck.StateModel as StateModel
136145
137146import Test.QuickCheck hiding ((.&&.) )
138- import Test.QuickCheck.Monadic as QC (PropertyM , monadic )
147+ import qualified Test.QuickCheck as QC
148+ import Test.QuickCheck.Monadic (PropertyM , monadic )
139149import qualified Test.QuickCheck.Monadic as QC
140150
141151-- | Key-value map where keys and values have three indices that can vary between different elements
@@ -188,7 +198,16 @@ data ContractInstanceSpec state where
188198 -> Contract w schema err () -- ^ The contract that is running in the instance
189199 -> ContractInstanceSpec state
190200
191- type Handles state = IMap (ContractInstanceKey state ) ContractHandle
201+ data WalletContractHandle w s e = WalletContractHandle Wallet (ContractHandle w s e )
202+
203+ type Handles state = IMap (ContractInstanceKey state ) WalletContractHandle
204+
205+ -- | Used to freeze other wallets when checking a `NoLockedFundsProof`.
206+ instancesForOtherWallets :: Wallet -> Handles state -> [ContractInstanceId ]
207+ instancesForOtherWallets _ IMNil = []
208+ instancesForOtherWallets w (IMCons _ (WalletContractHandle w' h) m)
209+ | w /= w' = chInstanceId h : instancesForOtherWallets w m
210+ | otherwise = instancesForOtherWallets w m
192211
193212
194213-- | A function returning the `ContractHandle` corresponding to a `ContractInstanceKey`. A
@@ -473,8 +492,8 @@ instance GetModelState (Spec state) where
473492handle :: (ContractModel s ) => Handles s -> HandleFun s
474493handle handles key =
475494 case imLookup key handles of
476- Just h -> h
477- Nothing -> error $ " handle: No handle for " ++ show key
495+ Just ( WalletContractHandle _ h) -> h
496+ Nothing -> error $ " handle: No handle for " ++ show key
478497
479498-- | The `EmulatorTrace` monad does not let you get the result of a computation out, but the way
480499-- "Test.QuickCheck.Monadic" is set up requires you to provide a function @m Property -> Property@.
@@ -502,13 +521,19 @@ setHandles a = State.modify (<> EmulatorAction (const a))
502521
503522instance ContractModel state => Show (StateModel. Action (ModelState state ) a ) where
504523 showsPrec p (ContractAction a) = showsPrec p a
524+ showsPrec p (Unilateral w) = showParen (p >= 11 ) $ showString " Unilateral " . showsPrec 11 w
505525
506526deriving instance ContractModel state => Eq (StateModel. Action (ModelState state ) a )
507527
508528instance ContractModel state => StateModel (ModelState state ) where
509529
510530 data Action (ModelState state ) a where
511531 ContractAction :: Action state -> StateModel. Action (ModelState state ) ()
532+ Unilateral :: Wallet -> StateModel. Action (ModelState state ) ()
533+ -- ^ This action disables all wallets other than the given wallet, by freezing their
534+ -- contract instances and removing their private keys from the emulator state. This can
535+ -- be used to check that a wallet can *unilaterally* achieve a desired outcome, without
536+ -- the help of other wallets.
512537
513538 type ActionMonad (ModelState state ) = ContractMonad state
514539
@@ -517,21 +542,30 @@ instance ContractModel state => StateModel (ModelState state) where
517542 return (Some @ () (ContractAction a))
518543
519544 shrinkAction s (ContractAction a) = [ Some @ () (ContractAction a') | a' <- shrinkAction s a ]
545+ shrinkAction _ Unilateral {} = []
520546
521547 initialState = ModelState { _currentSlot = 0
522548 , _balanceChanges = Map. empty
523549 , _minted = mempty
524550 , _contractState = initialState }
525551
526552 nextState s (ContractAction cmd) _v = runSpec (nextState cmd) s
553+ nextState s Unilateral {} _ = s
527554
528555 precondition s (ContractAction cmd) = precondition s cmd
556+ precondition _ Unilateral {} = True
529557
530558 perform s (ContractAction cmd) _env = () <$ runEmulator (\ h -> perform (handle h) s cmd)
559+ perform _ (Unilateral w) _env = () <$ runEmulator (\ h -> do
560+ let insts = instancesForOtherWallets w h
561+ mapM_ freezeContractInstance insts
562+ discardWallets (w /= )
563+ )
531564
532565 postcondition _s _cmd _env _res = True
533566
534567 monitoring (s0, s1) (ContractAction cmd) _env _res = monitoring (s0, s1) cmd
568+ monitoring _ Unilateral {} _ _ = id
535569
536570-- We present a simplified view of test sequences, and DL test cases, so
537571-- that users do not need to see the variables bound to results.
@@ -661,10 +695,11 @@ toDLTestStep (Witness a) = DL.Witness a
661695
662696fromDLTest :: forall s . DL. DynLogicTest (ModelState s ) -> DLTest s
663697fromDLTest (DL. BadPrecondition steps acts s) =
664- BadPrecondition (fromDLTestSteps steps) (map conv acts) (_contractState s)
665- where conv :: Any (StateModel. Action (ModelState s )) -> FailedStep s
666- conv (Some (ContractAction act)) = Action act
667- conv (Error e) = Assert e
698+ BadPrecondition (fromDLTestSteps steps) (concatMap conv acts) (_contractState s)
699+ where conv :: Any (StateModel. Action (ModelState s )) -> [FailedStep s ]
700+ conv (Some (ContractAction act)) = [Action act]
701+ conv (Some Unilateral {}) = []
702+ conv (Error e) = [Assert e]
668703fromDLTest (DL. Looping steps) =
669704 Looping (fromDLTestSteps steps)
670705fromDLTest (DL. Stuck steps s) =
@@ -673,11 +708,12 @@ fromDLTest (DL.DLScript steps) =
673708 DLScript (fromDLTestSteps steps)
674709
675710fromDLTestSteps :: [DL. TestStep (ModelState state )] -> [TestStep state ]
676- fromDLTestSteps steps = map fromDLTestStep steps
711+ fromDLTestSteps steps = concatMap fromDLTestStep steps
677712
678- fromDLTestStep :: DL. TestStep (ModelState state ) -> TestStep state
679- fromDLTestStep (DL. Do (_ := ContractAction act)) = Do act
680- fromDLTestStep (DL. Witness a) = Witness a
713+ fromDLTestStep :: DL. TestStep (ModelState state ) -> [TestStep state ]
714+ fromDLTestStep (DL. Do (_ := ContractAction act)) = [Do act]
715+ fromDLTestStep (DL. Do (_ := Unilateral {})) = []
716+ fromDLTestStep (DL. Witness a) = [Witness a]
681717
682718-- | Run a specific `DLTest`. Typically this test comes from a failed run of `forAllDL`
683719-- applied to the given `DL` scenario and property. Useful to check if a particular problem has
@@ -890,6 +926,7 @@ forAllDL dl prop = DL.forAllMappedDL toDLTest fromDLTest fromStateModelActions d
890926
891927instance ContractModel s => DL. DynLogicModel (ModelState s ) where
892928 restricted (ContractAction act) = restricted act
929+ restricted Unilateral {} = True
893930
894931instance GetModelState (DL state ) where
895932 type StateType (DL state ) = state
@@ -934,7 +971,7 @@ activateWallets [] = return IMNil
934971activateWallets (ContractInstanceSpec key wallet contract : spec) = do
935972 h <- activateContract wallet contract (instanceTag key wallet)
936973 m <- activateWallets spec
937- return $ IMCons key h m
974+ return $ IMCons key ( WalletContractHandle wallet h) m
938975
939976-- | Run a `Actions` in the emulator and check that the model and the emulator agree on the final
940977-- wallet balance changes. Equivalent to
@@ -1001,14 +1038,26 @@ propRunActionsWithOptions ::
10011038 -> Actions state -- ^ The actions to run
10021039 -> Property
10031040propRunActionsWithOptions opts handleSpecs predicate actions' =
1041+ propRunActionsWithOptions' opts handleSpecs predicate (toStateModelActions actions')
1042+
1043+ propRunActionsWithOptions' ::
1044+ ContractModel state
1045+ => CheckOptions -- ^ Emulator options
1046+ -> [ContractInstanceSpec state ] -- ^ Required wallet contract instances
1047+ -> (ModelState state -> TracePredicate ) -- ^ Predicate to check at the end
1048+ -> StateModel. Actions (ModelState state ) -- ^ The actions to run
1049+ -> Property
1050+ propRunActionsWithOptions' opts handleSpecs predicate actions =
10041051 monadic (flip State. evalState mempty ) $ finalChecks opts finalPredicate $ do
10051052 QC. run $ setHandles $ activateWallets handleSpecs
10061053 void $ runActionsInState StateModel. initialState actions
10071054 where
1008- finalState = stateAfter actions
1055+ finalState = StateModel. stateAfter actions
10091056 finalPredicate = predicate finalState .&&. checkBalances finalState
10101057 .&&. checkNoCrashes handleSpecs
1011- actions = toStateModelActions actions'
1058+
1059+ stateAfter :: ContractModel state => Actions state -> ModelState state
1060+ stateAfter actions = StateModel. stateAfter (toStateModelActions actions)
10121061
10131062checkBalances :: ModelState state -> TracePredicate
10141063checkBalances s = Map. foldrWithKey (\ w val p -> walletFundsChange w val .&&. p) (pure True ) (s ^. balanceChanges)
@@ -1020,3 +1069,68 @@ checkNoCrashes = foldr (\ (ContractInstanceSpec k w c) -> (assertOutcome c (inst
10201069 notError Failed {} = False
10211070 notError Done {} = True
10221071 notError NotDone {} = True
1072+
1073+ -- $noLockedFunds
1074+ -- Showing that funds can not be locked in the contract forever.
1075+
1076+ -- | A "proof" that you can always recover the funds locked by a contract. The first component is
1077+ -- a strategy that from any state of the contract can get all the funds out. The second component
1078+ -- is a strategy for each wallet that from the same state, shows how that wallet can recover the
1079+ -- same (or bigger) amount as using the first strategy, without relying on any actions being taken
1080+ -- by the other wallets.
1081+ --
1082+ -- For instance, in a two player game where each player bets some amount of funds and the winner
1083+ -- gets the pot, there needs to be a mechanism for the players to recover their bid if the other
1084+ -- player simply walks away (perhaps after realising the game is lost). If not, it won't be
1085+ -- possible to construct a `NoLockedFundsProof` that works in a state where both players need to
1086+ -- move before any funds can be collected.
1087+ data NoLockedFundsProof model = NoLockedFundsProof
1088+ { nlfpMainStrategy :: DL model ()
1089+ -- ^ Strategy to recover all funds from the contract in any reachable state.
1090+ , nlfpWalletStrategy :: Wallet -> DL model ()
1091+ -- ^ A strategy for each wallet to recover as much (or more) funds as the main strategy would
1092+ -- give them in a given state, without the assistance of any other wallet.
1093+ }
1094+
1095+ -- | Check a `NoLockedFundsProof`. Each test will generate an arbitrary sequence of actions
1096+ -- (`anyActions_`) and ask the `nlfpMainStrategy` to recover all funds locked by the contract
1097+ -- after performing those actions. This results in some distribution of the contract funds to the
1098+ -- wallets, and the test then asks each `nlfpWalletStrategy` to show how to recover their
1099+ -- allotment of funds without any assistance from the other wallets (assuming the main strategy
1100+ -- did not execute). When executing wallet strategies, the off-chain instances for other wallets
1101+ -- are killed and their private keys are deleted from the emulator state.
1102+ checkNoLockedFundsProof
1103+ :: (ContractModel model )
1104+ => CheckOptions
1105+ -> [ContractInstanceSpec model ]
1106+ -> NoLockedFundsProof model
1107+ -> Property
1108+ checkNoLockedFundsProof options spec NoLockedFundsProof {nlfpMainStrategy = mainStrat,
1109+ nlfpWalletStrategy = walletStrat } =
1110+ forAllDL anyActions_ $ \ (Actions as) ->
1111+ forAllDL (mainProp as) $ \ as' ->
1112+ let s = stateAfter as'
1113+ as'' = toStateModelActions as' in
1114+ foldl (QC. .&&.) (counterexample " Main strategy" $ prop as'') [ walletProp as w bal | (w, bal) <- Map. toList (s ^. balanceChanges) ]
1115+ where
1116+ prop = propRunActionsWithOptions' options spec (\ _ -> pure True )
1117+
1118+ mainProp as = do
1119+ mapM_ action as
1120+ mainStrat
1121+ lockedVal <- askModelState lockedValue
1122+ DL. assert (" Locked funds should be zero, but they are\n " ++ show lockedVal) $ isZero lockedVal
1123+
1124+ walletProp as w bal = counterexample (" Strategy for " ++ show w) $ DL. forAllDL dl prop
1125+ where
1126+ dl = do
1127+ mapM_ action as
1128+ DL. action $ Unilateral w
1129+ walletStrat w
1130+ bal' <- viewModelState (balanceChange w)
1131+ let err = " Unilateral strategy for " ++ show w ++ " should have gotten it at least\n " ++
1132+ " " ++ show bal ++ " \n " ++
1133+ " but it got\n " ++
1134+ " " ++ show bal'
1135+ DL. assert err (bal `leq` bal')
1136+
0 commit comments