diff --git a/doc/default.nix b/doc/default.nix index 3a278421a0..2347e214d7 100644 --- a/doc/default.nix +++ b/doc/default.nix @@ -1,7 +1,7 @@ { stdenv, lib, pythonPackages, sphinxcontrib-domaintools, sphinxcontrib-haddock, sphinx-markdown-tables, sphinxemoji, combined-haddock, ... }: stdenv.mkDerivation { name = "plutus-docs"; - src = lib.sourceFilesBySuffices ./. [ ".py" ".rst" ".hs" ".png" ".svg" ".bib" ".csv" ".css" ]; + src = lib.sourceFilesBySuffices ./. [ ".py" ".rst" ".hs" ".png" ".svg" ".bib" ".csv" ".css" ".html" ]; buildInputs = with pythonPackages; [ sphinx sphinx_rtd_theme diff --git a/doc/plutus-doc.cabal b/doc/plutus-doc.cabal index 7673ff7d5a..e44cd4ab2a 100644 --- a/doc/plutus-doc.cabal +++ b/doc/plutus-doc.cabal @@ -56,6 +56,13 @@ executable doc-doctests HandlingBlockchainEvents HelloWorldApp WriteScriptsTo + Escrow + Escrow2 + Escrow3 + Escrow4 + Escrow5 + Escrow6 + EscrowImpl build-depends: base >=4.9 && <5, template-haskell >=2.13.0.0, @@ -80,6 +87,8 @@ executable doc-doctests random -any, text -any, aeson -any, + tasty -any, + tasty-quickcheck -any if !(impl(ghcjs) || os(ghcjs)) build-depends: plutus-tx-plugin -any diff --git a/doc/plutus/tutorials/Auction.hs b/doc/plutus/tutorials/Auction.hs new file mode 100644 index 0000000000..232485325e --- /dev/null +++ b/doc/plutus/tutorials/Auction.hs @@ -0,0 +1,390 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +module Spec.Auction + ( tests + , options + , auctionTrace1 + , auctionTrace2 + , AuctionModel + , prop_Auction + , prop_FinishAuction + , prop_NoLockedFunds + , prop_NoLockedFundsFast + , prop_SanityCheckAssertions + , prop_Whitelist + , prop_CrashTolerance + , check_propAuctionWithCoverage + ) where + +import Control.Lens hiding (elements) +import Control.Monad (void, when) +import Control.Monad.Freer qualified as Freer +import Control.Monad.Freer.Error qualified as Freer +import Control.Monad.Freer.Extras.Log (LogLevel (..)) +import Data.Data +import Data.Default (Default (def)) +import Data.Monoid (Last (..)) + +import Ledger (Ada, Slot (..), Value) +import Ledger.Ada qualified as Ada +import Ledger.Generators (someTokenValue) +import Plutus.Contract hiding (currentSlot) +import Plutus.Contract.Test hiding (not) +import Streaming.Prelude qualified as S +import Wallet.Emulator.Folds qualified as Folds +import Wallet.Emulator.Stream qualified as Stream + +import Ledger qualified +import Ledger.TimeSlot (SlotConfig) +import Ledger.TimeSlot qualified as TimeSlot +import Plutus.Contract.Test.ContractModel +import Plutus.Contract.Test.ContractModel.CrashTolerance +import Plutus.Contract.Test.ContractModel.Symbolics (toSymValue) +import Plutus.Contract.Test.Coverage +import Plutus.Contracts.Auction hiding (Bid) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck hiding ((.&&.)) +import Test.Tasty +import Test.Tasty.QuickCheck (testProperty) + +slotCfg :: SlotConfig +slotCfg = def + +params :: AuctionParams +params = + AuctionParams + { apOwner = mockWalletPaymentPubKeyHash w1 + , apAsset = theToken + , apEndTime = TimeSlot.scSlotZeroTime slotCfg + 100000 + } + +{- START options -} +-- | The token that we are auctioning off. +theToken :: Value +theToken = + -- This currency is created by the initial transaction. + someTokenValue "token" 1 + +-- | 'CheckOptions' that includes 'theToken' in the initial distribution of Wallet 1. +options :: CheckOptions +options = defaultCheckOptionsContractModel + & changeInitialWalletValue w1 ((<>) theToken) +{- END options -} +seller :: Contract AuctionOutput SellerSchema AuctionError () +seller = auctionSeller (apAsset params) (apEndTime params) + +buyer :: ThreadToken -> Contract AuctionOutput BuyerSchema AuctionError () +buyer cur = auctionBuyer cur params + +trace1WinningBid :: Ada +trace1WinningBid = Ada.adaOf 50 + +auctionTrace1 :: Trace.EmulatorTrace () +auctionTrace1 = do + sellerHdl <- Trace.activateContractWallet w1 seller + void $ Trace.waitNSlots 3 + currency <- extractAssetClass sellerHdl + hdl2 <- Trace.activateContractWallet w2 (buyer currency) + void $ Trace.waitNSlots 1 + Trace.callEndpoint @"bid" hdl2 trace1WinningBid + void $ Trace.waitUntilTime $ apEndTime params + void $ Trace.waitNSlots 1 + +trace2WinningBid :: Ada +trace2WinningBid = Ada.adaOf 70 + +extractAssetClass :: Trace.ContractHandle AuctionOutput SellerSchema AuctionError -> Trace.EmulatorTrace ThreadToken +extractAssetClass handle = do + t <- auctionThreadToken <$> Trace.observableState handle + case t of + Last (Just currency) -> pure currency + _ -> Trace.throwError (Trace.GenericError "currency not found") + +auctionTrace2 :: Trace.EmulatorTrace () +auctionTrace2 = do + sellerHdl <- Trace.activateContractWallet w1 seller + void $ Trace.waitNSlots 3 + currency <- extractAssetClass sellerHdl + hdl2 <- Trace.activateContractWallet w2 (buyer currency) + hdl3 <- Trace.activateContractWallet w3 (buyer currency) + void $ Trace.waitNSlots 1 + Trace.callEndpoint @"bid" hdl2 (Ada.adaOf 50) + void $ Trace.waitNSlots 15 + Trace.callEndpoint @"bid" hdl3 (Ada.adaOf 60) + void $ Trace.waitNSlots 35 + Trace.callEndpoint @"bid" hdl2 trace2WinningBid + void $ Trace.waitUntilTime $ apEndTime params + void $ Trace.waitNSlots 1 + +trace1FinalState :: AuctionOutput +trace1FinalState = + AuctionOutput + { auctionState = Last $ Just $ Finished $ HighestBid + { highestBid = trace1WinningBid + , highestBidder = mockWalletPaymentPubKeyHash w2 + } + , auctionThreadToken = Last $ Just threadToken + } + +trace2FinalState :: AuctionOutput +trace2FinalState = + AuctionOutput + { auctionState = Last $ Just $ Finished $ HighestBid + { highestBid = trace2WinningBid + , highestBidder = mockWalletPaymentPubKeyHash w2 + } + , auctionThreadToken = Last $ Just threadToken + } + +threadToken :: ThreadToken +threadToken = + let con = getThreadToken :: Contract AuctionOutput SellerSchema AuctionError ThreadToken + fld = Folds.instanceOutcome con (Trace.walletInstanceTag w1) + getOutcome (Folds.Done a) = a + getOutcome e = error $ "not finished: " <> show e + in + either (error . show) (getOutcome . S.fst') + $ Freer.run + $ Freer.runError @Folds.EmulatorFoldErr + $ Stream.foldEmulatorStreamM fld + $ Stream.takeUntilSlot 10 + $ Trace.runEmulatorStream (options ^. emulatorConfig) + $ do + void $ Trace.activateContractWallet w1 (void con) + Trace.waitNSlots 3 + +-- * QuickCheck model +{- START model -} +data AuctionModel = AuctionModel + { _currentBid :: Integer + , _winner :: Wallet + , _endSlot :: Slot + , _phase :: Phase + } deriving (Show, Eq, Data) + +data Phase = NotStarted | Bidding | AuctionOver + deriving (Eq, Show, Data) +{- END model -} +makeLenses 'AuctionModel + +deriving instance Eq (ContractInstanceKey AuctionModel w s e params) +deriving instance Show (ContractInstanceKey AuctionModel w s e params) + +instance ContractModel AuctionModel where + + data ContractInstanceKey AuctionModel w s e params where + SellerH :: ContractInstanceKey AuctionModel AuctionOutput SellerSchema AuctionError () + BuyerH :: Wallet -> ContractInstanceKey AuctionModel AuctionOutput BuyerSchema AuctionError () + +{- START Action -} + data Action AuctionModel = Init | Bid Wallet Integer + deriving (Eq, Show, Data) +{- END Action -} + + initialState = AuctionModel + { _currentBid = 0 + , _winner = w1 + , _endSlot = TimeSlot.posixTimeToEnclosingSlot def $ apEndTime params + , _phase = NotStarted + } + + initialInstances = [ StartContract (BuyerH w) () | w <- [w2, w3, w4] ] + + startInstances _ Init = [StartContract SellerH ()] + startInstances _ _ = [] + + instanceWallet SellerH = w1 + instanceWallet (BuyerH w) = w + + instanceContract _ SellerH _ = seller + instanceContract _ BuyerH{} _ = buyer threadToken + + arbitraryAction s + | p /= NotStarted = do + oneof [ Bid w <$> validBid + | w <- [w2, w3, w4] ] + | otherwise = pure $ Init + where + p = s ^. contractState . phase + b = s ^. contractState . currentBid + validBid = choose ((b+1) `max` Ada.getLovelace Ledger.minAdaTxOut, + b + Ada.getLovelace (Ada.adaOf 100)) +{- START precondition -} + precondition s Init = s ^. contractState . phase == NotStarted + precondition s (Bid w bid) = + -- In order to place a bid, we need to satisfy the constraint where + -- each tx output must have at least N Ada. + s ^. contractState . phase /= NotStarted && + bid >= Ada.getLovelace (Ledger.minAdaTxOut) && + bid > s ^. contractState . currentBid +{-END precondition -} +{- START nextReactiveState -} + nextReactiveState slot' = do + end <- viewContractState endSlot + p <- viewContractState phase + when (slot' >= end && p == Bidding) $ do + w <- viewContractState winner + bid <- viewContractState currentBid + phase .= AuctionOver + deposit w $ Ada.toValue Ledger.minAdaTxOut <> theToken + deposit w1 $ Ada.lovelaceValueOf bid +{- END nextReactiveState -} + + {- +{- START extendedNextReactiveState -} + nextReactiveState slot' = do + end <- viewContractState endSlot + p <- viewContractState phase + when (slot' >= end && p == Bidding) $ do + w <- viewContractState winner + bid <- viewContractState currentBid + phase .= AuctionOver + deposit w $ Ada.toValue Ledger.minAdaTxOut <> theToken + deposit w1 $ Ada.lovelaceValueOf bid + -- NEW!!! + w1change <- viewModelState $ balanceChange w1 -- since the start of the test + assertSpec ("w1 final balance is wrong:\n "++show w1change) $ + w1change == toSymValue (inv theToken <> Ada.lovelaceValueOf bid) || + w1change == mempty +{- END extendedNextReactiveState -} + -} + +{- START nextState -} + nextState cmd = do + case cmd of + Init -> do + phase .= Bidding + withdraw w1 $ Ada.toValue Ledger.minAdaTxOut <> theToken + wait 3 + Bid w bid -> do + currentPhase <- viewContractState phase + when (currentPhase == Bidding) $ do + current <- viewContractState currentBid + leader <- viewContractState winner + withdraw w $ Ada.lovelaceValueOf bid + deposit leader $ Ada.lovelaceValueOf current + currentBid .= bid + winner .= w + wait 2 +{- END nextState -} + + perform _ _ _ Init = delay 3 + perform handle _ _ (Bid w bid) = do + -- FIXME: You cannot bid in certain slots when the off-chain code is busy, so to make the + -- tests pass we send two identical bids in consecutive slots. The off-chain code is + -- never busy for more than one slot at a time so at least one of the bids is + -- guaranteed to go through. If both reaches the off-chain code the second will be + -- discarded since it's not higher than the current highest bid. + Trace.callEndpoint @"bid" (handle $ BuyerH w) (Ada.lovelaceOf bid) + delay 1 + Trace.callEndpoint @"bid" (handle $ BuyerH w) (Ada.lovelaceOf bid) + delay 1 + + shrinkAction _ Init = [] + shrinkAction _ (Bid w v) = [ Bid w v' | v' <- shrink v ] + +{- START prop_Auction -} +prop_Auction :: Actions AuctionModel -> Property +prop_Auction script = + propRunActionsWithOptions (set minLogLevel Info options) defaultCoverageOptions + (\ _ -> pure True) -- TODO: check termination + script +{- END prop_Auction -} + +{- START prop_FinishAuction -} +finishAuction :: DL AuctionModel () +finishAuction = do + anyActions_ + finishingStrategy + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +finishingStrategy :: DL AuctionModel () +finishingStrategy = do + slot <- viewModelState currentSlot + end <- viewContractState endSlot + when (slot < end) $ waitUntilDL end + +prop_FinishAuction :: Property +prop_FinishAuction = forAllDL finishAuction prop_Auction +{- END prop_FinishAuction -} +-- | This does not hold! The Payout transition is triggered by the sellers off-chain code, so if the +-- seller walks away the buyer will not get their token (unless going around the off-chain code +-- and building a Payout transaction manually). +{- START noLockProof -} +noLockProof :: NoLockedFundsProof AuctionModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = const finishingStrategy } +{- END noLockProof -} + +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProofWithOptions (set minLogLevel Critical options) noLockProof + +prop_NoLockedFundsFast :: Property +prop_NoLockedFundsFast = checkNoLockedFundsProofFast noLockProof + +prop_SanityCheckAssertions :: Actions AuctionModel -> Property +prop_SanityCheckAssertions = propSanityCheckAssertions + +prop_Whitelist :: Actions AuctionModel -> Property +prop_Whitelist = checkErrorWhitelist defaultWhitelist + +{- START crashTolerance -} +instance CrashTolerance AuctionModel where + available (Bid w _) alive = (Key $ BuyerH w) `elem` alive + available Init alive = True + + restartArguments _ BuyerH{} = () + restartArguments _ SellerH{} = () + +prop_CrashTolerance :: Actions (WithCrashTolerance AuctionModel) -> Property +prop_CrashTolerance = + propRunActionsWithOptions (set minLogLevel Critical options) defaultCoverageOptions + (\ _ -> pure True) +{- END crashTolerance -} + +check_propAuctionWithCoverage :: IO () +check_propAuctionWithCoverage = do + cr <- quickCheckWithCoverage (set coverageIndex covIdx $ defaultCoverageOptions) $ \covopts -> + withMaxSuccess 1000 $ + propRunActionsWithOptions @AuctionModel + (set minLogLevel Critical options) covopts (const (pure True)) + writeCoverageReport "Auction" covIdx cr + +tests :: TestTree +tests = + testGroup "auction" + [ checkPredicateOptions options "run an auction" + (assertDone seller (Trace.walletInstanceTag w1) (const True) "seller should be done" + .&&. assertDone (buyer threadToken) (Trace.walletInstanceTag w2) (const True) "buyer should be done" + .&&. assertAccumState (buyer threadToken) (Trace.walletInstanceTag w2) ((==) trace1FinalState ) "wallet 2 final state should be OK" + .&&. walletFundsChange w1 (Ada.toValue (-Ledger.minAdaTxOut) <> Ada.toValue trace1WinningBid <> inv theToken) + .&&. walletFundsChange w2 (Ada.toValue Ledger.minAdaTxOut <> inv (Ada.toValue trace1WinningBid) <> theToken)) + auctionTrace1 + , checkPredicateOptions options "run an auction with multiple bids" + (assertDone seller (Trace.walletInstanceTag w1) (const True) "seller should be done" + .&&. assertDone (buyer threadToken) (Trace.walletInstanceTag w2) (const True) "buyer should be done" + .&&. assertDone (buyer threadToken) (Trace.walletInstanceTag w3) (const True) "3rd party should be done" + .&&. assertAccumState (buyer threadToken) (Trace.walletInstanceTag w2) ((==) trace2FinalState) "wallet 2 final state should be OK" + .&&. assertAccumState (buyer threadToken) (Trace.walletInstanceTag w3) ((==) trace2FinalState) "wallet 3 final state should be OK" + .&&. walletFundsChange w1 (Ada.toValue (-Ledger.minAdaTxOut) <> Ada.toValue trace2WinningBid <> inv theToken) + .&&. walletFundsChange w2 (Ada.toValue Ledger.minAdaTxOut <> inv (Ada.toValue trace2WinningBid) <> theToken) + .&&. walletFundsChange w3 mempty) + auctionTrace2 + , testProperty "QuickCheck property" $ + withMaxSuccess 10 prop_FinishAuction + , testProperty "NLFP fails" $ + expectFailure $ noShrinking prop_NoLockedFunds + , testProperty "prop_Reactive" $ + withMaxSuccess 1000 (propSanityCheckReactive @AuctionModel) + ] diff --git a/doc/plutus/tutorials/Auction.html b/doc/plutus/tutorials/Auction.html new file mode 100644 index 0000000000..8c3252e833 --- /dev/null +++ b/doc/plutus/tutorials/Auction.html @@ -0,0 +1,47 @@ +
+   124    auctionTransition
+   125        :: AuctionParams
+   126        -> State AuctionState
+   127        -> AuctionInput
+   128        -> Maybe (TxConstraints Void Void, State AuctionState)
+   129    auctionTransition AuctionParams{apOwner, apAsset, apEndTime} State{stateData=oldStateData, stateValue=oldStateValue} input =
+   130        case (oldStateData, input) of
+   131    
+   132            (Ongoing HighestBid{highestBid, highestBidder}, Bid{newBid, newBidder}) | newBid > highestBid -> -- if the new bid is higher,
+   133                let constraints = if highestBid == 0 then mempty else
+   134                        Constraints.mustPayToPubKey highestBidder (Ada.toValue highestBid) -- we pay back the previous highest bid
+   135                        <> Constraints.mustValidateIn (Interval.to $ apEndTime - 1) -- but only if we haven't gone past 'apEndTime'
+   136                    newState =
+   137                        State
+   138                            { stateData = Ongoing HighestBid{highestBid = newBid, highestBidder = newBidder}
+   139                            , stateValue = Value.noAdaValue oldStateValue
+   140                                        <> Ada.toValue (Ada.fromValue oldStateValue - highestBid)
+   141                                        <> Ada.toValue newBid -- and lock the new bid in the script output
+   142                            }
+   143                in Just (constraints, newState)
+   144    
+   145            (Ongoing h@HighestBid{highestBidder, highestBid}, Payout) ->
+   146                let constraints =
+   147                        Constraints.mustValidateIn (Interval.from apEndTime) -- When the auction has ended,
+   148                        <> Constraints.mustPayToPubKey apOwner (Ada.toValue highestBid) -- the owner receives the payment
+   149                        <> Constraints.mustPayToPubKey highestBidder apAsset -- and the highest bidder the asset
+   150                    newState = State { stateData = Finished h, stateValue = mempty }
+   151                in Just (constraints, newState)
+   152    
+   153            -- Any other combination of 'AuctionState' and 'AuctionInput' is disallowed.
+   154            -- This rules out new bids that don't go over the current highest bid.
+   155            _ -> Nothing
+   156    
+   157    
+   158    {-# INLINABLE auctionStateMachine #-}
+   159    auctionStateMachine :: (ThreadToken, AuctionParams) -> AuctionMachine
+   160    auctionStateMachine (threadToken, auctionParams) =
+   161        SM.mkStateMachine (Just threadToken) (auctionTransition auctionParams) isFinal
+   162      where
+   163        isFinal Finished{} = True
+   164        isFinal _          = False
+   165    
+   166    {-# INLINABLE mkValidator #-}
+   167    mkValidator :: (ThreadToken, AuctionParams) -> Scripts.ValidatorType AuctionMachine
+   168    mkValidator = SM.mkValidator . auctionStateMachine
+
diff --git a/doc/plutus/tutorials/Escrow.hs b/doc/plutus/tutorials/Escrow.hs new file mode 100644 index 0000000000..66cb44c105 --- /dev/null +++ b/doc/plutus/tutorials/Escrow.hs @@ -0,0 +1,279 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} +module Escrow + ( tests + , prop_Escrow + , EscrowModel + ) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void) +import Data.Data +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.Typed.Scripts qualified as Scripts +import Ledger.Value +import Plutus.Contract hiding (currentSlot) +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel + +import Plutus.Contracts.Tutorial.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck as QC hiding ((.&&.)) +import Test.Tasty +import Test.Tasty.QuickCheck hiding ((.&&.)) + +{- START EscrowModel -} +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + } deriving (Eq, Show, Data) + +makeLenses ''EscrowModel +{- END EscrowModel -} + +{- START ContractInstanceKeyDeriving :-} +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) +{- END ContractInstanceKeyDeriving -} +{- +{- START ContractModelInstance -} +instance ContractModel EscrowModel where ... +{- END ContractModelInstance -} +-} +instance ContractModel EscrowModel where +{- START ActionType -} + data Action EscrowModel = Pay Wallet Integer + | Redeem Wallet + deriving (Eq, Show, Data) +{- END ActionType -} + + -- | Refund Wallet + +{- START ContractInstanceKeyType -} + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError () +{- END ContractInstanceKeyType -} + +{- START initialState -} + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.fromList [ (w1, Ada.adaValueOf 10) + , (w2, Ada.adaValueOf 20) + ] + } +{- END initialState -} + +{- +{- START testContract -} +testContract = selectList [ void $ payEp escrowParams + , void $ redeemEp escrowParams + ] >> testContract +{- END testContract -} +-} + +{- + +{- START ContractKeySemantics -} + initialInstances = [StartContract (WalletKey w) () | w <- testWallets] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} _ = testContract +{- END ContractKeySemantics -} + +-} + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} _ = testContract + where + testContract = selectList [ void $ payEp escrowParams + , void $ redeemEp escrowParams + -- , void $ refundEp escrowParams + ] >> testContract +{- START initialInstances -} + initialInstances = [StartContract (WalletKey w) () | w <- testWallets] +{- END initialInstances -} +{- +{- START 0nextState -} + nextState a = case a of + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + contributions .= Map.empty + wait 1 +{- END 0nextState -} +-} +{- +{- START nextState1 -} + nextState a = case a of + Pay w v -> ... + Redeem w -> do + targets <- viewContractState targets + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + contribs <- viewContractState contributions -- NEW + let leftoverValue = fold contribs <> inv (fold targets) -- NEW + deposit w leftoverValue -- NEW + contributions .= Map.empty + wait 1 +{- END nextState1 -} +-} +{- START nextState -} + nextState a = case a of + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 +{- END nextState -} +{- Refund w -> do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 +-} + +{- +{- START precondition1 -} + precondition s a = case a of + Redeem _ -> (s ^. contractState . contributions . to fold) + `geq` + (s ^. contractState . targets . to fold) + _ -> True +{- END precondition1 -} +-} +{- +{- START precondition2 -} +precondition s a = case a of + Redeem _ -> (s ^. contractState . contributions . to fold) + `geq` + (s ^. contractState . targets . to fold) + Pay _ v -> Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut +{- END precondition2 -} +-} + precondition s a = case a of + Redeem _ -> (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + --Redeem _ -> (s ^. contractState . contributions . to fold) == (s ^. contractState . targets . to fold) + --Refund w -> Nothing /= (s ^. contractState . contributions . at w) + Pay _ v -> Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + +{- START perform -} + perform h _ _ a = case a of + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 +{- END perform -} + +{- Refund w -> do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 +-} + +{- +{- START arbitraryAction1 -} + arbitraryAction _ = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) + , (1, Redeem <$> elements testWallets) ] +{- END arbitraryAction1 -} +-} + {- ++ + [ (1, Refund <$> elements (s ^. contractState . contributions . to Map.keys)) + | Prelude.not . null $ s ^. contractState . contributions . to Map.keys ] + -} + +{- START arbitraryAction2 -} + arbitraryAction s = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) -- NEW + `geq` -- NEW + (s ^. contractState . targets . to fold) -- NEW + ] +{- END arbitraryAction2 -} + + +{- START shrinkAction -} + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] +{- END shrinkAction -} + +{- + monitoring _ (Redeem _) = classify True "Contains Redeem" + monitoring (s,s') _ = classify (redeemable s' && Prelude.not (redeemable s)) "Redeemable" + where redeemable s = precondition s (Redeem undefined) +-} +{- START testWallets -} +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] +{- END testWallets -} + +{- START prop_Escrow -} +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ +{- END prop_Escrow -} + +{- +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy (const True) + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +finishingStrategy :: (Wallet -> Bool) -> DL EscrowModel () +finishingStrategy walletAlive = do + contribs <- viewContractState contributions + monitor (classify (Map.null contribs) "no need for extra refund to recover funds") + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs, walletAlive w] + +prop_FinishEscrow :: Property +prop_FinishEscrow = forAllDL finishEscrow prop_Escrow + +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy (const True) + , nlfpWalletStrategy = finishingStrategy . (==) } + +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProof defaultCheckOptionsContractModel noLockProof +-} + +tests :: TestTree +tests = testGroup "escrow" + [ testProperty "QuickCheck ContractModel" $ withMaxSuccess 10 prop_Escrow +-- , testProperty "QuickCheck NoLockedFunds" $ withMaxSuccess 10 prop_NoLockedFunds + ] + +{- START escrowParams -} +escrowParams :: EscrowParams d +escrowParams = + EscrowParams + { escrowTargets = + [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w1) (Ada.adaValueOf 10) + , payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w2) (Ada.adaValueOf 20) + ] + } +{- END escrowParams -} diff --git a/doc/plutus/tutorials/Escrow.html b/doc/plutus/tutorials/Escrow.html new file mode 100644 index 0000000000..4e8b351588 --- /dev/null +++ b/doc/plutus/tutorials/Escrow.html @@ -0,0 +1,43 @@ +

Files


src/Plutus/Contracts/Escrow.hs

.
+.
+.
+   186    --   spending transaction to be paid to target addresses. This may happen if
+   187    --   the target address is also used as a change address for the spending
+   188    --   transaction, and allowing the target to be exceed prevents outsiders from
+   189    --   poisoning the contract by adding arbitrary outputs to the script address.
+   190    meetsTarget :: TxInfo -> EscrowTarget DatumHash -> Bool
+   191    meetsTarget ptx = \case
+   192        PaymentPubKeyTarget pkh vl ->
+   193            valuePaidTo ptx (unPaymentPubKeyHash pkh) `geq` vl
+   194        ScriptTarget validatorHash dataValue vl ->
+   195            case scriptOutputsAt validatorHash ptx of
+   196                [(dataValue', vl')] ->
+   197                    traceIfFalse "dataValue" (dataValue' == dataValue)
+   198                    && traceIfFalse "value" (vl' `geq` vl)
+   199                _ -> False
+   200    
+   201    {-# INLINABLE validate #-}
+   202    validate :: EscrowParams DatumHash -> PaymentPubKeyHash -> Action -> ScriptContext -> Bool
+   203    validate EscrowParams{escrowDeadline, escrowTargets} contributor action ScriptContext{scriptContextTxInfo} =
+   204        case action of
+   205            Redeem ->
+   206                traceIfFalse "escrowDeadline-after" (escrowDeadline `after` txInfoValidRange scriptContextTxInfo)
+   207                && traceIfFalse "meetsTarget" (all (meetsTarget scriptContextTxInfo) escrowTargets)
+   208            Refund ->
+   209                traceIfFalse "escrowDeadline-before" ((escrowDeadline - 1) `before` txInfoValidRange scriptContextTxInfo)
+   210                && traceIfFalse "txSignedBy" (scriptContextTxInfo `txSignedBy` unPaymentPubKeyHash contributor)
+   211    
+   212    typedValidator :: EscrowParams Datum -> Scripts.TypedValidator Escrow
+   213    typedValidator escrow = go (Haskell.fmap Ledger.datumHash escrow) where
+   214        go = Scripts.mkTypedValidatorParam @Escrow
+   215            $$(PlutusTx.compile [|| validate ||])
+.
+.
+.
+   361        -- Pay the value 'vl' into the contract
+   362        _ <- pay inst params vl
+   363        go
+   364    
+   365    covIdx :: CoverageIndex
+   366    covIdx = getCovIdx $$(PlutusTx.compile [|| validate ||])
+
\ No newline at end of file diff --git a/doc/plutus/tutorials/Escrow2.hs b/doc/plutus/tutorials/Escrow2.hs new file mode 100644 index 0000000000..2eace2ccf1 --- /dev/null +++ b/doc/plutus/tutorials/Escrow2.hs @@ -0,0 +1,217 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in Plutus.Contracts.Tutorial.Escrow, +-- with generated escrow targets. See the "Parameterising Models and +-- Dynamic Contract Instances" section of the tutorial. + +module Escrow2(prop_Escrow, EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void) +import Data.Data +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.Value +import Plutus.Contract +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel + +import Plutus.Contracts.Tutorial.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +{- START ModelState -} +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _phase :: Phase -- NEW! + } deriving (Eq, Show, Data) +{- END ModelState -} + +data Phase = Initial | Running deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where +{- START Action -} + data Action EscrowModel = Init [(Wallet, Integer)] -- NEW! + | Redeem Wallet + | Pay Wallet Integer + deriving (Eq, Show, Data) +{- END Action -} + +{- START ContractInstanceKey -} + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) +{- END ContractInstanceKey -} + +{- START initialState -} + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _phase = Initial + } +{- END initialState -} + +{- START initialInstances -} + initialInstances = [] +{- END initialInstances -} + +{- START startInstances -} + startInstances _ (Init wns) = + [StartContract (WalletKey w) (escrowParams wns) | w <- testWallets] + startInstances _ _ = [] +{- END startInstances -} + + instanceWallet (WalletKey w) = w + +{- START instanceContract -} + instanceContract _ WalletKey{} params = testContract params +{- END instanceContract -} + +{- +{- START nextState -} + nextState a = case a of + Init wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + ... +{- END nextState -} +-} + + nextState a = case a of + Init wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + -- omit next two lines to disable disbursement of the surplus + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + +{- +{- START precondition -} + precondition s a = case a of + Init _ -> currentPhase == Initial + Redeem _ -> currentPhase == Running && ... + Pay _ v -> currentPhase == Running && ... + where currentPhase = s ^. contractState . phase +{- END precondition -} + +{- START tightprecondition -} + precondition s a = case a of + Init tgts-> currentPhase == Initial + && and [Ada.adaValueOf (fromInteger n) `geq` Ada.toValue minAdaTxOut | (w,n) <- tgts] + ... +{- END tightprecondition -} +-} + + precondition s a = case a of + Init _ -> currentPhase == Initial + Redeem _ -> currentPhase == Running + && (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + where currentPhase = s ^. contractState . phase + +{- +{- START perform -} + perform h _ _ a = case a of + Init _ -> do + return () + ... +{- END perform -} +-} + + perform h _ _ a = case a of + Init _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + +{- +{- START arbitraryAction -} + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> arbitraryTargets + | otherwise + = ...as before... +{- END arbitraryAction -} +-} + + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> arbitraryTargets + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] + +{- START shrinkAction -} + shrinkAction _ (Init tgts) = map Init (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) +{- END shrinkAction -} + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +{- START arbitraryTargets -} +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs +{- END arbitraryTargets -} + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +{- START testContract -} +testContract :: EscrowParams Datum -> Contract () EscrowSchema EscrowError () +testContract params = selectList [ void $ payEp params + , void $ redeemEp params + ] >> testContract params +{- END testContract -} + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + +{- START escrowParams -} +escrowParams :: [(Wallet, Integer)] -> EscrowParams d +escrowParams tgts = + EscrowParams + { escrowTargets = + [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) (Ada.adaValueOf (fromInteger n)) + | (w,n) <- tgts + ] + } +{- END escrowParams -} diff --git a/doc/plutus/tutorials/Escrow3.hs b/doc/plutus/tutorials/Escrow3.hs new file mode 100644 index 0000000000..d8774fc858 --- /dev/null +++ b/doc/plutus/tutorials/Escrow3.hs @@ -0,0 +1,296 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in Plutus.Contracts.Tutorial.Escrow, +-- with generated escrow targets. See the "Parameterising Models and +-- Dynamic Contract Instances" section of the tutorial. + +module Escrow3(prop_Escrow, prop_FinishEscrow, prop_NoLockedFunds, EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void, when) +import Data.Data +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.Value +import Plutus.Contract +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel + +import Plutus.Contracts.Tutorial.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _phase :: Phase + } deriving (Eq, Show, Data) + +data Phase = Initial | Running deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where +{- START EscrowModel -} + data Action EscrowModel = Init [(Wallet, Integer)] + | Redeem Wallet + | Pay Wallet Integer + | Refund Wallet -- NEW! + deriving (Eq, Show, Data) +{- END EscrowModel -} + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _phase = Initial + } + + initialInstances = [] + + startInstances _ (Init wns) = + [StartContract (WalletKey w) (escrowParams wns) | w <- testWallets] + startInstances _ _ = [] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} params = testContract params + + nextState a = case a of + Init wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + -- omit next two lines to disable disbursement of the surplus + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + Refund w -> do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 + + precondition s a = case a of + Init tgts -> currentPhase == Initial + && and [Ada.adaValueOf (fromInteger n) `geq` Ada.toValue minAdaTxOut | (_,n) <- tgts] + Redeem _ -> currentPhase == Running + && fold (s ^. contractState . contributions) `geq` fold (s ^. contractState . targets) + -- && fold (s ^. contractState . contributions) == fold (s ^. contractState . targets) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + Refund w -> currentPhase == Running + && w `Map.member` (s ^. contractState . contributions) + where currentPhase = s ^. contractState . phase + +{- +{- START strongPrecondition -} +precondition s (Redeem _) = + currentPhase == Running + && fold (s ^. contractState . contributions) == fold (s ^. contractState . targets) +{- END strongPrecondition -} +-} + + perform h _ _ a = case a of + Init _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + Refund w -> do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 + +{- +{-START RefundModel -} + nextState (Refund w) = do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 + + precondition s (Refund w) = + currentPhase == Running + && w `Map.member` (s ^. contractState . contributions) + where currentPhase = s ^. contractState . phase + + perform h _ _ (Refund w) = do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 + + arbitraryAction s + ... + = frequency $ ... ++ + [ (1, Refund <$> elements testWallets) ] +{- END RefundModel -} + -} + + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> arbitraryTargets + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] ++ + [ (1, Refund <$> elements testWallets) ] + + + shrinkAction _ (Init tgts) = map Init (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +{- START testContract -} +testContract :: EscrowParams Datum -> Contract () EscrowSchema EscrowError () +testContract params = selectList [ void $ payEp params + , void $ redeemEp params + , void $ refundEp params -- NEW! + ] >> testContract params +{- END testContract -} + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + + +escrowParams :: [(Wallet, Integer)] -> EscrowParams d +escrowParams tgts = + EscrowParams + { escrowTargets = + [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) (Ada.adaValueOf (fromInteger n)) + | (w,n) <- tgts + ] + } + +{- +-- This is the first--bad--approach to recovering locked funds. +{- START finishEscrow -} +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy w1 + assertModel "Locked funds are not zero" (symIsZero . lockedValue) +{- END finishEscrow -} + +{- START badFinishingStrategy -} +finishingStrategy :: Wallet -> DL EscrowModel () +finishingStrategy w = do + currentPhase <- viewContractState phase + when (currentPhase /= Initial) $ do + action $ Redeem w +{- END badFinishingStrategy -} + +{- START finishingStrategy -} +finishingStrategy :: Wallet -> DL EscrowModel () +finishingStrategy w = do + currentPhase <- viewContractState phase + when (currentPhase /= Initial) $ do + currentTargets <- viewContractState targets + currentContribs <- viewContractState contributions + let deficit = fold currentTargets <> inv (fold currentContribs) + when (deficit `gt` Ada.adaValueOf 0) $ + action $ Pay w $ round $ Ada.getAda $ max minAdaTxOut $ Ada.fromValue deficit + action $ Redeem w +{- END finishingStrategy -} + +-- This unilateral strategy fails. +{- START noLockProof -} +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy w1 + , nlfpWalletStrategy = finishingStrategy } +{- END noLockProof -} +-} + +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +{- +{- START betterFinishingStrategy -} +finishingStrategy :: (Wallet -> Bool) -> DL EscrowModel () +finishingStrategy walletActive = do + contribs <- viewContractState contributions + monitor (classify (Map.null contribs) "no need for extra refund to recover funds") + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs, walletActive w] +{- END betterFinishingStrategy -} +-} + +{- START prop_FinishEscrow -} +prop_FinishEscrow :: Property +prop_FinishEscrow = forAllDL finishEscrow prop_Escrow +{- END prop_FinishEscrow -} + +{- START BetterNoLockProof -} +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = walletStrategy } +{- END BetterNoLockProof -} + +{- START prop_NoLockedFunds -} +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProof noLockProof +{- END prop_NoLockedFunds -} + +{- +{- START fixedTargets -} +fixedTargets :: DL EscrowModel () +fixedTargets = do + action $ Init [(w1,10),(w2,20)] + anyActions_ +{- END fixedTargets -} +-} + +{- START BetterStrategies -} +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] + +walletStrategy :: Wallet -> DL EscrowModel () +walletStrategy w = do + contribs <- viewContractState contributions + when (w `Map.member` contribs) $ action $ Refund w +{- END BetterStrategies -} diff --git a/doc/plutus/tutorials/Escrow4.hs b/doc/plutus/tutorials/Escrow4.hs new file mode 100644 index 0000000000..b5660a70f3 --- /dev/null +++ b/doc/plutus/tutorials/Escrow4.hs @@ -0,0 +1,283 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in Plutus.Contracts.Tutorial.Escrow, +-- with generated escrow targets. See the "Parameterising Models and +-- Dynamic Contract Instances" section of the tutorial. + +module Escrow4(prop_Escrow, prop_FinishEscrow, prop_NoLockedFunds, EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void, when) +import Data.Data +import Data.Default +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, Slot (..), minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.TimeSlot (SlotConfig (..)) +import Ledger.Value (Value, geq) +import Plutus.Contract (Contract, selectList) +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel +import Plutus.V1.Ledger.Time + +import Plutus.Contracts.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +{- START EscrowModel -} +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _refundSlot :: Slot -- NEW!!! + , _phase :: Phase + } deriving (Eq, Show, Data) +{- END EscrowModel -} + +{- START Phase -} +data Phase = Initial | Running | Refunding deriving (Eq, Show, Data) +{- END Phase -} + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where + +{- START Action -} + data Action EscrowModel = Init Slot [(Wallet, Integer)] -- NEW!!! + | Redeem Wallet + | Pay Wallet Integer + | Refund Wallet + deriving (Eq, Show, Data) +{- END Action -} + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _refundSlot = 0 + , _phase = Initial + } + + initialInstances = [] + +{- START startInstances -} + startInstances _ (Init s wns) = + [StartContract (WalletKey w) (escrowParams s wns) | w <- testWallets] +{- END startInstances -} + startInstances _ _ = [] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} params = testContract params + +{- START nextState -} + nextState (Init s wns) = do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + refundSlot .= s -- NEW!!! +{- END nextState -} + + nextState a = case a of + Init s wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + refundSlot .= s + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + Refund w -> do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 + +{- START nextReactiveState -} + nextReactiveState slot = do + deadline <- viewContractState refundSlot + when (slot >= deadline) $ phase .= Refunding +{- END nextReactiveState -} + +{- START precondition -} + precondition s a = case a of + Init s tgts -> currentPhase == Initial + && s > 1 + && and [Ada.adaValueOf (fromInteger n) `geq` Ada.toValue minAdaTxOut | (_,n) <- tgts] + Redeem _ -> currentPhase == Running + && fold (s ^. contractState . contributions) `geq` fold (s ^. contractState . targets) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + Refund w -> currentPhase == Refunding -- NEW!!! + && w `Map.member` (s ^. contractState . contributions) + where currentPhase = s ^. contractState . phase +{- END precondition -} + + perform h _ _ a = case a of + Init _ _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + Refund w -> do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 + +{- START arbitraryAction -} + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> (Slot . getPositive <$> arbitrary) <*> arbitraryTargets +{- END arbitraryAction -} + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] ++ + [ (1, Refund <$> elements testWallets) ] +{- +{- START weightedArbitraryAction -} + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> (Slot . getPositive <$> scale (*10) arbitrary) <*> arbitraryTargets +{- END weightedArbitraryAction -} +-} + +{- START shrinkAction -} + shrinkAction _ (Init s tgts) = map (Init s) (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) + ++ map (`Init` tgts) (map Slot . shrink . getSlot $ s) -- NEW!!! +{- END shrinkAction -} + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +testContract :: EscrowParams Datum -> Contract () EscrowSchema EscrowError () +testContract params = selectList [ void $ payEp params + , void $ redeemEp params + , void $ refundEp params + ] >> testContract params + + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + +{- START escrowParams -} +escrowParams :: Slot -> [(Wallet, Integer)] -> EscrowParams d +escrowParams s tgts = + EscrowParams + { escrowTargets = + [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) (Ada.adaValueOf (fromInteger n)) + | (w,n) <- tgts + ] + , escrowDeadline = scSlotZeroTime def + POSIXTime (getSlot s * scSlotLength def) -- NEW!!! + } +{- END escrowParams -} + + +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +{- +{- START oldFinishingStrategy -} +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] +{- END oldFinishingStrategy -} +-} + +{- START finishingStrategy -} +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + waitUntilDeadline -- NEW!!! + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] +{- END finishingStrategy -} +{- +{- START monitoredFinishingStrategy -} +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + phase <- viewContractState phase -- NEW!!! + monitor $ tabulate "Phase" [show phase] -- NEW!!! + waitUntilDeadline + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] +{- END monitoredFinishingStrategy -} +-} + +walletStrategy :: Wallet -> DL EscrowModel () +walletStrategy w = do + contribs <- viewContractState contributions + when (w `Map.member` contribs) $ do + --waitUntilDeadline + action $ Refund w + +{- START waitUntilDeadline -} +waitUntilDeadline :: DL EscrowModel () +waitUntilDeadline = do + deadline <- viewContractState refundSlot + slot <- viewModelState currentSlot + when (slot < deadline) $ waitUntilDL deadline +{- END waitUntilDeadline -} + +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = walletStrategy } + +{- START prop_FinishEscrow -} +prop_FinishEscrow :: Property +prop_FinishEscrow = forAllDL finishEscrow prop_Escrow +{- END prop_FinishEscrow -} + +{- +{- START prop_FinishFast -} +prop_FinishFast :: Property +prop_FinishFast = forAllDL finishEscrow $ const True +{- END prop_FinishFast -} +-} + +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProof noLockProof + diff --git a/doc/plutus/tutorials/Escrow5.hs b/doc/plutus/tutorials/Escrow5.hs new file mode 100644 index 0000000000..d0d8da8066 --- /dev/null +++ b/doc/plutus/tutorials/Escrow5.hs @@ -0,0 +1,237 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in Plutus.Contracts.Tutorial.Escrow, +-- with generated escrow targets. See the "Parameterising Models and +-- Dynamic Contract Instances" section of the tutorial. + +module Escrow5(prop_Escrow, prop_FinishEscrow, prop_FinishFast, prop_NoLockedFunds, prop_NoLockedFundsFast, + check_propEscrowWithCoverage, EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void, when) +import Data.Data +import Data.Default +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, Slot (..), minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.TimeSlot (SlotConfig (..)) +import Ledger.Value (Value, geq) +import Plutus.Contract (Contract, selectList) +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel +import Plutus.Contract.Test.Coverage +import Plutus.V1.Ledger.Time + +import Plutus.Contracts.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _refundSlot :: Slot + , _phase :: Phase + } deriving (Eq, Show, Data) + +data Phase = Initial | Running | Refunding deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where + data Action EscrowModel = Init Slot [(Wallet, Integer)] + | Redeem Wallet + | Pay Wallet Integer + | Refund Wallet + deriving (Eq, Show, Data) + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _refundSlot = 0 + , _phase = Initial + } + + initialInstances = [] + + startInstances _ (Init s wns) = + [StartContract (WalletKey w) (escrowParams s wns) | w <- testWallets] + startInstances _ _ = [] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} params = testContract params + + nextState a = case a of + Init s wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + refundSlot .= s + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + Refund w -> do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 + + + nextReactiveState slot = do + deadline <- viewContractState refundSlot + when (slot >= deadline) $ phase .= Refunding + + + precondition s a = case a of + Init s tgts -> currentPhase == Initial + && s > 1 + && and [Ada.adaValueOf (fromInteger n) `geq` Ada.toValue minAdaTxOut | (_,n) <- tgts] + Redeem _ -> currentPhase == Running + && fold (s ^. contractState . contributions) `geq` fold (s ^. contractState . targets) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + Refund w -> currentPhase == Refunding + && w `Map.member` (s ^. contractState . contributions) + where currentPhase = s ^. contractState . phase + + + perform h _ _ a = case a of + Init _ _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + Refund w -> do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 + + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> (Slot . getPositive <$> scale (*10) arbitrary) <*> arbitraryTargets + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] ++ + [ (1, Refund <$> elements testWallets) ] + + + shrinkAction _ (Init s tgts) = map (Init s) (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) + ++ map (`Init` tgts) (map Slot . shrink . getSlot $ s) + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +testContract :: EscrowParams Datum -> Contract () EscrowSchema EscrowError () +testContract params = selectList [ void $ payEp params + , void $ redeemEp params + , void $ refundEp params + ] >> testContract params + + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + + +escrowParams :: Slot -> [(Wallet, Integer)] -> EscrowParams d +escrowParams s tgts = + EscrowParams + { escrowTargets = + [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) (Ada.adaValueOf (fromInteger n)) + | (w,n) <- tgts + ] + , escrowDeadline = scSlotZeroTime def + POSIXTime (getSlot s * scSlotLength def) + } + +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + phase <- viewContractState phase + monitor $ tabulate "Phase" [show phase] + waitUntilDeadline + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] + +walletStrategy :: Wallet -> DL EscrowModel () +walletStrategy w = do + contribs <- viewContractState contributions + --waitUntilDeadline + when (w `Map.member` contribs) $ do + action $ Refund w + +waitUntilDeadline :: DL EscrowModel () +waitUntilDeadline = do + deadline <- viewContractState refundSlot + slot <- viewModelState currentSlot + when (slot < deadline) $ waitUntilDL deadline + +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = walletStrategy } + +prop_FinishEscrow :: Property +prop_FinishEscrow = forAllDL finishEscrow prop_Escrow + +prop_FinishFast :: Property +prop_FinishFast = forAllDL finishEscrow $ const True + +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProof noLockProof + +prop_NoLockedFundsFast :: Property +prop_NoLockedFundsFast = checkNoLockedFundsProofFast noLockProof + +{- START check_propEscrowWithCoverage -} +check_propEscrowWithCoverage :: IO () +check_propEscrowWithCoverage = do + cr <- quickCheckWithCoverage stdArgs (set coverageIndex covIdx $ defaultCoverageOptions) $ \covopts -> + withMaxSuccess 1000 $ + propRunActionsWithOptions @EscrowModel defaultCheckOptionsContractModel covopts + (const (pure True)) + writeCoverageReport "Escrow" covIdx cr +{- END check_propEscrowWithCoverage -} diff --git a/doc/plutus/tutorials/Escrow6.hs b/doc/plutus/tutorials/Escrow6.hs new file mode 100644 index 0000000000..c7f92e02c2 --- /dev/null +++ b/doc/plutus/tutorials/Escrow6.hs @@ -0,0 +1,273 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in Plutus.Contracts.Tutorial.Escrow, +-- with generated escrow targets. See the "Parameterising Models and +-- Dynamic Contract Instances" section of the tutorial. + +module Escrow6 + ( prop_Escrow + , prop_FinishEscrow + , prop_FinishFast + , prop_NoLockedFunds + , prop_NoLockedFundsFast + , prop_CrashTolerance + , check_propEscrowWithCoverage + , EscrowModel + ) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void, when) +import Data.Data +import Data.Default +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, Slot (..), minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.TimeSlot (SlotConfig (..)) +import Ledger.Value (Value, geq) +import Plutus.Contract (Contract, selectList) +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel +import Plutus.Contract.Test.ContractModel.CrashTolerance +import Plutus.Contract.Test.Coverage +import Plutus.V1.Ledger.Time + +import Plutus.Contracts.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _refundSlot :: Slot + , _phase :: Phase + } deriving (Eq, Show, Data) + +data Phase = Initial | Running | Refunding deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where + data Action EscrowModel = Init Slot [(Wallet, Integer)] + | Redeem Wallet + | Pay Wallet Integer + | Refund Wallet + deriving (Eq, Show, Data) + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _refundSlot = 0 + , _phase = Initial + } + + initialInstances = [] + + startInstances _ (Init s wns) = + [StartContract (WalletKey w) (escrowParams s wns) | w <- testWallets] + startInstances _ _ = [] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} params = testContract params + + nextState a = case a of + Init s wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + refundSlot .= s + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + Refund w -> do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 + + + nextReactiveState slot = do + deadline <- viewContractState refundSlot + when (slot >= deadline) $ phase .= Refunding + + + precondition s a = case a of + Init s tgts -> currentPhase == Initial + && s > 1 + && and [Ada.adaValueOf (fromInteger n) `geq` Ada.toValue minAdaTxOut | (_,n) <- tgts] + Redeem _ -> currentPhase == Running + && fold (s ^. contractState . contributions) `geq` fold (s ^. contractState . targets) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + Refund w -> currentPhase == Refunding + && w `Map.member` (s ^. contractState . contributions) + where currentPhase = s ^. contractState . phase + + + perform h _ _ a = case a of + Init _ _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + Refund w -> do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 + + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> (Slot . getPositive <$> scale (*10) arbitrary) <*> arbitraryTargets + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] ++ + [ (1, Refund <$> elements testWallets) ] + + + shrinkAction _ (Init s tgts) = map (Init s) (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) + ++ map (`Init` tgts) (map Slot . shrink . getSlot $ s) + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +testContract :: EscrowParams Datum -> Contract () EscrowSchema EscrowError () +testContract params = selectList [ void $ payEp params + , void $ redeemEp params + , void $ refundEp params + ] >> testContract params + + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + +{- START escrowParams -} +escrowParams :: Slot -> [(Wallet, Integer)] -> EscrowParams d +escrowParams s tgts = escrowParams' s [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- tgts] + +escrowParams' :: Slot -> [(Wallet,Value)] -> EscrowParams d +escrowParams' s tgts' = + EscrowParams + { escrowTargets = [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) v | (w,v) <- tgts' ] + , escrowDeadline = scSlotZeroTime def + POSIXTime (getSlot s * scSlotLength def) + } +{- END escrowParams -} +{- +{- START betterEscrowParams -} +escrowParams' :: Slot -> [(Wallet,Value)] -> EscrowParams d +escrowParams' s tgts' = + EscrowParams + { escrowTargets = [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) v + | (w,v) <- sortBy (compare `on` fst) tgts' ] -- NEW!! + , escrowDeadline = scSlotZeroTime def + POSIXTime (getSlot s * scSlotLength def) + } +{- END betterEscrowParams -} +-} +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + phase <- viewContractState phase + monitor $ tabulate "Phase" [show phase] + waitUntilDeadline + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] + +walletStrategy :: Wallet -> DL EscrowModel () +walletStrategy w = do + contribs <- viewContractState contributions + --waitUntilDeadline + when (w `Map.member` contribs) $ do + action $ Refund w + +waitUntilDeadline :: DL EscrowModel () +waitUntilDeadline = do + deadline <- viewContractState refundSlot + slot <- viewModelState currentSlot + when (slot < deadline) $ waitUntilDL deadline + +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = walletStrategy } + +prop_FinishEscrow :: Property +prop_FinishEscrow = forAllDL finishEscrow prop_Escrow + +prop_FinishFast :: Property +prop_FinishFast = forAllDL finishEscrow $ const True + +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProof noLockProof + +prop_NoLockedFundsFast :: Property +prop_NoLockedFundsFast = checkNoLockedFundsProofFast noLockProof + +{- START CrashTolerance -} +instance CrashTolerance EscrowModel where + available (Init _ _) _ = True + available a alive = (Key $ WalletKey w) `elem` alive + where w = case a of + Pay w _ -> w + Redeem w -> w + Refund w -> w + Init _ _ -> undefined + + restartArguments s WalletKey{} = escrowParams' slot tgts + where slot = s ^. contractState . refundSlot + tgts = Map.toList (s ^. contractState . targets) +{- END CrashTolerance -} + +{- START prop_CrashTolerance -} +prop_CrashTolerance :: Actions (WithCrashTolerance EscrowModel) -> Property +prop_CrashTolerance = propRunActions_ +{- END prop_CrashTolerance -} + +check_propEscrowWithCoverage :: IO () +check_propEscrowWithCoverage = do + cr <- quickCheckWithCoverage stdArgs (set coverageIndex covIdx $ defaultCoverageOptions) $ \covopts -> + withMaxSuccess 1000 $ propRunActionsWithOptions @EscrowModel defaultCheckOptionsContractModel covopts (const (pure True)) + writeCoverageReport "Escrow" covIdx cr diff --git a/doc/plutus/tutorials/EscrowImpl.hs b/doc/plutus/tutorials/EscrowImpl.hs new file mode 100644 index 0000000000..7ca6c98317 --- /dev/null +++ b/doc/plutus/tutorials/EscrowImpl.hs @@ -0,0 +1,373 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:debug-context #-} +{- START OPTIONS_GHC -} +{-# OPTIONS_GHC -g -fplugin-opt PlutusTx.Plugin:coverage-all #-} +{- END OPTIONS_GHC -} +-- | A general-purpose escrow contract in Plutus +module EscrowImpl( + -- $escrow + Escrow + , EscrowError(..) + , AsEscrowError(..) + , EscrowParams(..) + , EscrowTarget(..) + , payToScriptTarget + , payToPaymentPubKeyTarget + , targetTotal + , escrowContract + , payRedeemRefund + , typedValidator + -- * Actions + , pay + , payEp + , redeem + , redeemEp + , refund + , refundEp + , RedeemFailReason(..) + , RedeemSuccess(..) + , RefundSuccess(..) + , EscrowSchema + -- * Exposed for test endpoints + , Action(..) + -- * Coverage + , covIdx + ) where + +import Control.Lens (makeClassyPrisms, review, view) +import Control.Monad (void) +import Control.Monad.Error.Lens (throwing) +import Data.Aeson (FromJSON, ToJSON) +import GHC.Generics (Generic) + +import Ledger (Datum (..), DatumHash, POSIXTime, PaymentPubKeyHash (unPaymentPubKeyHash), TxId, ValidatorHash, + getCardanoTxId, interval, scriptOutputsAt, txSignedBy, valuePaidTo) +import Ledger qualified +import Ledger.Constraints (TxConstraints) +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts (ScriptContext (..), TxInfo (..)) +import Ledger.Interval (after, before, from) +import Ledger.Interval qualified as Interval +import Ledger.Tx qualified as Tx +import Ledger.Typed.Scripts (TypedValidator) +import Ledger.Typed.Scripts qualified as Scripts +import Ledger.Value (Value, geq, lt) + +import Plutus.Contract +import Plutus.Contract.Typed.Tx qualified as Typed +import PlutusTx qualified +{- START imports -} +import PlutusTx.Code +import PlutusTx.Coverage +{- END imports -} +import PlutusTx.Prelude hiding (Applicative (..), Semigroup (..), check, foldMap) + +import Prelude (Semigroup (..), foldMap) +import Prelude qualified as Haskell + +type EscrowSchema = + Endpoint "pay-escrow" Value + .\/ Endpoint "redeem-escrow" () + .\/ Endpoint "refund-escrow" () + +data RedeemFailReason = DeadlinePassed | NotEnoughFundsAtAddress + deriving stock (Haskell.Eq, Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +data EscrowError = + RedeemFailed RedeemFailReason + | RefundFailed + | EContractError ContractError + deriving stock (Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +makeClassyPrisms ''EscrowError + +instance AsContractError EscrowError where + _ContractError = _EContractError + +-- $escrow +-- The escrow contract implements the exchange of value between multiple +-- parties. It is defined by a list of targets (public keys and script +-- addresses, each associated with a value). It works similar to the +-- crowdfunding contract in that the contributions can be made independently, +-- and the funds can be unlocked only by a transaction that pays the correct +-- amount to each target. A refund is possible if the outputs locked by the +-- contract have not been spent by the deadline. (Compared to the crowdfunding +-- contract, the refund policy is simpler because here because there is no +-- "collection period" during which the outputs may be spent after the deadline +-- has passed. This is because we're assuming that the participants in the +-- escrow contract will make their deposits as quickly as possible after +-- agreeing on a deal) +-- +-- The contract supports two modes of operation, manual and automatic. In +-- manual mode, all actions are driven by endpoints that exposed via 'payEp' +-- 'redeemEp' and 'refundEp'. In automatic mode, the 'pay', 'redeem' and +-- 'refund'actions start immediately. This mode is useful when the escrow is +-- called from within another contract, for example during setup (collection of +-- the initial deposits). + +-- | Defines where the money should go. Usually we have `d = Datum` (when +-- defining `EscrowTarget` values in off-chain code). Sometimes we have +-- `d = DatumHash` (when checking the hashes in on-chain code) +data EscrowTarget d = + PaymentPubKeyTarget PaymentPubKeyHash Value + | ScriptTarget ValidatorHash d Value + deriving (Haskell.Functor) + +PlutusTx.makeLift ''EscrowTarget + +-- | An 'EscrowTarget' that pays the value to a public key address. +payToPaymentPubKeyTarget :: PaymentPubKeyHash -> Value -> EscrowTarget d +payToPaymentPubKeyTarget = PaymentPubKeyTarget + +-- | An 'EscrowTarget' that pays the value to a script address, with the +-- given data script. +payToScriptTarget :: ValidatorHash -> Datum -> Value -> EscrowTarget Datum +payToScriptTarget = ScriptTarget + +-- | Definition of an escrow contract, consisting of a deadline and a list of targets +data EscrowParams d = + EscrowParams + { escrowDeadline :: POSIXTime + -- ^ Latest point at which the outputs may be spent. + , escrowTargets :: [EscrowTarget d] + -- ^ Where the money should go. For each target, the contract checks that + -- the output 'mkTxOutput' of the target is present in the spending + -- transaction. + } deriving (Haskell.Functor) + +PlutusTx.makeLift ''EscrowParams + +-- | The total 'Value' that must be paid into the escrow contract +-- before it can be unlocked +targetTotal :: EscrowParams d -> Value +targetTotal = foldl (\vl tgt -> vl + targetValue tgt) mempty . escrowTargets + +-- | The 'Value' specified by an 'EscrowTarget' +targetValue :: EscrowTarget d -> Value +targetValue = \case + PaymentPubKeyTarget _ vl -> vl + ScriptTarget _ _ vl -> vl + +-- | Create a 'Ledger.TxOut' value for the target +mkTx :: EscrowTarget Datum -> TxConstraints Action PaymentPubKeyHash +mkTx = \case + PaymentPubKeyTarget pkh vl -> + Constraints.mustPayToPubKey pkh vl + ScriptTarget vs ds vl -> + Constraints.mustPayToOtherScript vs ds vl + +data Action = Redeem | Refund + +data Escrow +instance Scripts.ValidatorTypes Escrow where + type instance RedeemerType Escrow = Action + type instance DatumType Escrow = PaymentPubKeyHash + +PlutusTx.unstableMakeIsData ''Action +PlutusTx.makeLift ''Action + +{-# INLINABLE meetsTarget #-} +-- | @ptx `meetsTarget` tgt@ if @ptx@ pays at least @targetValue tgt@ to the +-- target address. +-- +-- The reason why this does not require the target amount to be equal +-- to the actual amount is to enable any excess funds consumed by the +-- spending transaction to be paid to target addresses. This may happen if +-- the target address is also used as a change address for the spending +-- transaction, and allowing the target to be exceed prevents outsiders from +-- poisoning the contract by adding arbitrary outputs to the script address. +meetsTarget :: TxInfo -> EscrowTarget DatumHash -> Bool +meetsTarget ptx = \case + PaymentPubKeyTarget pkh vl -> + valuePaidTo ptx (unPaymentPubKeyHash pkh) `geq` vl + ScriptTarget validatorHash dataValue vl -> + case scriptOutputsAt validatorHash ptx of + [(dataValue', vl')] -> + traceIfFalse "dataValue" (dataValue' == dataValue) + && traceIfFalse "value" (vl' `geq` vl) + _ -> False + +{-# INLINABLE validate #-} +validate :: EscrowParams DatumHash -> PaymentPubKeyHash -> Action -> ScriptContext -> Bool +validate EscrowParams{escrowDeadline, escrowTargets} contributor action ScriptContext{scriptContextTxInfo} = + case action of + Redeem -> + traceIfFalse "escrowDeadline-after" (escrowDeadline `after` txInfoValidRange scriptContextTxInfo) + && traceIfFalse "meetsTarget" (all (meetsTarget scriptContextTxInfo) escrowTargets) + Refund -> + traceIfFalse "escrowDeadline-before" ((escrowDeadline - 1) `before` txInfoValidRange scriptContextTxInfo) + && traceIfFalse "txSignedBy" (scriptContextTxInfo `txSignedBy` unPaymentPubKeyHash contributor) + +{- START typedValidator -} +typedValidator :: EscrowParams Datum -> Scripts.TypedValidator Escrow +typedValidator escrow = go (Haskell.fmap Ledger.datumHash escrow) where + go = Scripts.mkTypedValidatorParam @Escrow + $$(PlutusTx.compile [|| validate ||]) + $$(PlutusTx.compile [|| wrap ||]) + wrap = Scripts.wrapValidator +{- END typedValidator -} +escrowContract + :: EscrowParams Datum + -> Contract () EscrowSchema EscrowError () +escrowContract escrow = + let inst = typedValidator escrow + payAndRefund = endpoint @"pay-escrow" $ \vl -> do + _ <- pay inst escrow vl + _ <- awaitTime $ escrowDeadline escrow + refund inst escrow + in selectList + [ void payAndRefund + , void $ redeemEp escrow + ] + +-- | 'pay' with an endpoint that gets the owner's public key and the +-- contribution. +payEp :: + forall w s e. + ( HasEndpoint "pay-escrow" Value s + , AsEscrowError e + ) + => EscrowParams Datum + -> Promise w s e TxId +payEp escrow = promiseMap + (mapError (review _EContractError)) + (endpoint @"pay-escrow" $ pay (typedValidator escrow) escrow) + +-- | Pay some money into the escrow contract. +pay :: + forall w s e. + ( AsContractError e + ) + => TypedValidator Escrow + -- ^ The instance + -> EscrowParams Datum + -- ^ The escrow contract + -> Value + -- ^ How much money to pay in + -> Contract w s e TxId +pay inst escrow vl = do + pk <- ownPaymentPubKeyHash + let tx = Constraints.mustPayToTheScript pk vl + <> Constraints.mustValidateIn (Ledger.interval 1 (escrowDeadline escrow)) + utx <- mkTxConstraints (Constraints.typedValidatorLookups inst) tx + getCardanoTxId <$> submitUnbalancedTx (Constraints.adjustUnbalancedTx utx) + +newtype RedeemSuccess = RedeemSuccess TxId + deriving (Haskell.Eq, Haskell.Show) + +-- | 'redeem' with an endpoint. +redeemEp :: + forall w s e. + ( HasEndpoint "redeem-escrow" () s + , AsEscrowError e + ) + => EscrowParams Datum + -> Promise w s e RedeemSuccess +redeemEp escrow = promiseMap + (mapError (review _EscrowError)) + (endpoint @"redeem-escrow" $ \() -> redeem (typedValidator escrow) escrow) + +-- | Redeem all outputs at the contract address using a transaction that +-- has all the outputs defined in the contract's list of targets. +redeem :: + forall w s e. + ( AsEscrowError e + ) + => TypedValidator Escrow + -> EscrowParams Datum + -> Contract w s e RedeemSuccess +redeem inst escrow = mapError (review _EscrowError) $ do + let addr = Scripts.validatorAddress inst + current <- currentTime + unspentOutputs <- utxosAt addr + let + valRange = Interval.to (Haskell.pred $ escrowDeadline escrow) + tx = Typed.collectFromScript unspentOutputs Redeem + <> foldMap mkTx (escrowTargets escrow) + <> Constraints.mustValidateIn valRange + if current >= escrowDeadline escrow + then throwing _RedeemFailed DeadlinePassed + else if foldMap (view Tx.ciTxOutValue) unspentOutputs `lt` targetTotal escrow + then throwing _RedeemFailed NotEnoughFundsAtAddress + else do + utx <- mkTxConstraints ( Constraints.typedValidatorLookups inst + <> Constraints.unspentOutputs unspentOutputs + ) tx + RedeemSuccess . getCardanoTxId <$> submitUnbalancedTx (Constraints.adjustUnbalancedTx utx) + +newtype RefundSuccess = RefundSuccess TxId + deriving newtype (Haskell.Eq, Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +-- | 'refund' with an endpoint. +refundEp :: + forall w s. + ( HasEndpoint "refund-escrow" () s + ) + => EscrowParams Datum + -> Promise w s EscrowError RefundSuccess +refundEp escrow = endpoint @"refund-escrow" $ \() -> refund (typedValidator escrow) escrow + +-- | Claim a refund of the contribution. +refund :: + forall w s. + TypedValidator Escrow + -> EscrowParams Datum + -> Contract w s EscrowError RefundSuccess +refund inst escrow = do + pk <- ownPaymentPubKeyHash + unspentOutputs <- utxosAt (Scripts.validatorAddress inst) + let flt _ ciTxOut = either id Ledger.datumHash (Tx._ciTxOutDatum ciTxOut) == Ledger.datumHash (Datum (PlutusTx.toBuiltinData pk)) + tx' = Typed.collectFromScriptFilter flt unspentOutputs Refund + <> Constraints.mustValidateIn (from (Haskell.succ $ escrowDeadline escrow)) + if Constraints.modifiesUtxoSet tx' + then do + utx <- mkTxConstraints ( Constraints.typedValidatorLookups inst + <> Constraints.unspentOutputs unspentOutputs + ) tx' + RefundSuccess . getCardanoTxId <$> submitUnbalancedTx (Constraints.adjustUnbalancedTx utx) + else throwing _RefundFailed () + +-- | Pay some money into the escrow contract. Then release all funds to their +-- specified targets if enough funds were deposited before the deadline, +-- or reclaim the contribution if the goal has not been met. +payRedeemRefund :: + forall w s. + EscrowParams Datum + -> Value + -> Contract w s EscrowError (Either RefundSuccess RedeemSuccess) +payRedeemRefund params vl = do + let inst = typedValidator params + go = do + cur <- utxosAt (Scripts.validatorAddress inst) + let presentVal = foldMap (view Tx.ciTxOutValue) cur + if presentVal `geq` targetTotal params + then Right <$> redeem inst params + else do + time <- currentTime + if time >= escrowDeadline params + then Left <$> refund inst params + else waitNSlots 1 >> go + -- Pay the value 'vl' into the contract + _ <- pay inst params vl + go + +{- START covIdx -} +covIdx :: CoverageIndex +covIdx = getCovIdx $$(PlutusTx.compile [|| validate ||]) +{- END covIdx -} diff --git a/doc/plutus/tutorials/contract-models.rst b/doc/plutus/tutorials/contract-models.rst new file mode 100644 index 0000000000..063362bee6 --- /dev/null +++ b/doc/plutus/tutorials/contract-models.rst @@ -0,0 +1,3021 @@ +.. highlight:: haskell +.. _contract_models_tutorial: + +Testing Plutus Contracts with Contract Models +============================================= + +Introduction +------------ + +In this tutorial we will see how to test Plutus contracts with +*contract models*, using the framework provided by +:hsmod:`Plutus.Contract.Test.ContractModel`. This framework generates +and runs tests on the Plutus emulator, where each test may involve a +number of emulated wallets, each running a collection of Plutus +contracts, all submitting transactions to an emulated blockchain. +Once the user has defined a suitable model, then QuickCheck can +generate and run many thousands of scenarios, taking the application +through a wide variety of states, and checking that it behaves +correctly in each one. Once the underlying contract model is in place, +then the framework can check user-defined properties specific to the +application, generic properties such as that no funds remain locked in +contracts for ever, and indeed both positive and negative +tests---where positive tests check that the contracts allow the +intended usages, and negative tests check that they do *not* allow +unintended ones. + +The `ContractModel` framework is quite rich in features, but we will +introduce them gradually and explain how they can best be used. + +Basic Contract Models +--------------------- + +Example: A Simple Escrow Contract +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We begin by showing how to construct a model for a simplified escrow +contract, which can be found in +:hsmod:`Plutus.Contracts.Tutorial.Escrow`. This contract enables a +group of wallets to make a predetermined exchange of tokens, for +example selling an NFT for Ada. There are two endpoints, a ``pay`` +endpoint, and a ``redeem`` endpoint. Each wallet pays in its +contribution to the contract using the ``pay`` endpoint, and once all +the wallets have done so, then any wallet can trigger the +predetermined payout using the ``redeem`` endpoint. + +For simplicity, we will begin by testing the contract for a fixed set +of predetermined payouts. These are defined by the ``EscrowParams``, a +type exported by the escrow contract, and which is actually passed to +the on-chain validators. :hsmod:`Plutus.Contract.Test` provides ten +emulated wallets for use in tests, ``w1`` to ``w10``; in this case we +will use five of them: + +.. literalinclude:: Escrow.hs + :start-after: START testWallets + :end-before: END testWallets + +Let us decide arbitrarily that ``w1`` will receive a payout of 10 Ada, +and ``w2`` will receive a payout of 20, and define an ``EscrowParams`` +value to represent that: + +.. literalinclude:: Escrow.hs + :start-after: START escrowParams + :end-before: END escrowParams + +The Contract Model Type +^^^^^^^^^^^^^^^^^^^^^^^ + +In order to generate sensible tests, and to decide how they should +behave, we need to track the expected state of the system. The first +step in defining a contract model is to define a type to represent +this expected state. We usually need to refine it as the model +evolves, but for now we keep things simple. + +In this case, as wallets make payments into the escrow, we will need +to keep track of how much each wallet has paid in. So let us define an +``EscrowModel`` type that records these contributions. Once the +contributions reach the targets, then the escrow may be redeemed, so +let us keep track of these targets in the model too. We define + +.. literalinclude:: Escrow.hs + :start-after: START EscrowModel + :end-before: END EscrowModel + +Note that we use `lenses `_ +to access the fields of the model. This is why the field names begin +with an underscore; the ``makeLenses`` call creates lenses called just +``contributions`` and ``targets`` for these fields, which we will use +to access and modify the fields below. + +We turn this type into a contract model by making it an instance of +the ``ContractModel`` class: + +.. literalinclude:: Escrow.hs + :start-after: START ContractModelInstance + :end-before: END ContractModelInstance + +The rest of the contract model is provided by defining the methods and +associated data types of this class. + +What contracts shall we test? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In general, a contract model can be used to test any number of +contracts, of differing types, running in any of the emulated +wallets. But we need to tell the framework *which* contracts we are +going to test, and we need a way for the model to refer to each +*contract instance*, so that we can invoke the right endpoints. We do +so using a ``ContractInstanceKey``, but since different models will be +testing different collections of contracts, then this type is not +*fixed*, it is defined as part of each model, as an associated type of +the ``ContractModel`` class. + +In this case only one kind of contract is involved in the tests, the +escrow contract, but there will be many instances of it, one running +in each wallet. To identify a contract instance, a +``ContractInstanceKey`` just has to record the wallet it is running +in, we only need one constructor in the type. In general there will be +one constructor for each type of contract instance in the test. + +.. literalinclude:: Escrow.hs + :start-after: START ContractInstanceKeyType + :end-before: END ContractInstanceKeyType + +Note that the ``ContractInstanceKey`` type is a GADT, so it tracks not only the +model type it belongs to, but also the type of the contract instance +it refers to. + +The framework also needs to be able to show and compare +``ContractInstanceKey``, so you might expect that we would add a +deriving clause to this type definition. But a deriving clause is +actually not supported here, because the type is a GADT, so instead we +have to give separate 'standalone deriving' declarations outside the +``ContractModel`` instance: + +.. literalinclude:: Escrow.hs + :start-after: START ContractInstanceKeyDeriving + :end-before: END ContractInstanceKeyDeriving + + +Defining ``ContractInstanceKey`` is only part of the story: we also +have to tell the framework how to *interpret* the contract instance +keys, in particular + + +#. which contract instances to start +#. which emulated wallets to run them in +#. which actual contract each contract instance should run. + + +We do so by defining three methods in the ``ContractModel`` class: + + +.. literalinclude:: Escrow.hs + :start-after: START ContractKeySemantics + :end-before: END ContractKeySemantics + +The first line above tells the test framework to start a contract +instance in each of the test wallets (with contract parameter ``()``), +the second line tells the framework which wallet each contract key +should run in, and the third line tells the framework which contract +to run for each key--in this case, the same ``testContract`` in each +wallet. ``Spec.Tutorial.Escrow`` does not actually export a complete +concrete, only contract endpoints, so for the purposes of the test we +just define a contract that invokes those endpoints repeatedly: + +.. literalinclude:: Escrow.hs + :start-after: START testContract + :end-before: END testContract + + +What actions should tests perform? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The type of Actions +''''''''''''''''''' + +The final *type* we need to define as part of a contract model tells +the framework what *actions* to include in generated tests. This is +defined as another associated datatype of the ``ContractModel`` class, +and in this case, we will just need actions to invoke the two contract +endpoints: + +.. literalinclude:: Escrow.hs + :start-after: START ActionType + :end-before: END ActionType + +The framework needs to be able to show and compare +``Action`` too, but in this case we *can* just add a ``deriving`` +clause to the definition. + +Performing Actions +'''''''''''''''''' + +QuickCheck will generate sequences of ``Action`` as tests, but in +order to *run* the tests, we need to specify how each action should be +performed. This is done by defining the ``perform`` method of the +``ContractModel`` class, which maps ``Action`` to a computation in the +emulator. ``perform`` takes several parameters besides the ``Action`` to +perform, but for now we ignore all but the first, whose purpose is to +translate a ``ContractInstanceKey``, used in the model, into a +``ContractHandle``, used to refer to a contract instance in the +emulator. The ``perform`` method is free to use any ``EmulatorTrace`` +operations, but in practice we usually keep it simple, interpreting +each ``Action`` as a single call to a contract endpoint. This gives +QuickCheck maximal control over the interaction between the tests and +the contracts. In this case, we just call either the ``pay`` or the +``redeem`` endpoint. + +.. literalinclude:: Escrow.hs + :start-after: START perform + :end-before: END perform + +Notice that we *do* need to allow each ``Action`` time to complete, so +we include a ``delay`` to tell the emulator to move on to the next +slot after each endpoint call. Of course we are free *not* to do this, +but then tests will submit many endpoint calls per slot, and fail +because the endpoints are not ready to perform them. This is not the +most interesting kind of test failure, and so we avoid it by delaying +an appropriate number of slots after each endpoint call. The number of +slots we need to wait varies from contract to contract, so we usually +determine these numbers experimentally. Exactly the same problem +arises in writing unit tests, of course. + +Modelling Actions +''''''''''''''''' + +Remember that we need to track the real state of the system using the +contract model state? We defined a type for this purpose: + +.. literalinclude:: Escrow.hs + :start-after: START EscrowModel + :end-before: END EscrowModel + +We need to tell the framework what the *effect* of each ``Action`` is +expected to be, both on wallet contents, and in terms of the model state. We do +this by defining the ``nextState`` method of the ``ContractModel`` +class, which just takes an ``Action`` as a parameter, and interprets +it in the ``Spec`` monad, provided by the ``ContractModel`` framework. + +.. literalinclude:: Escrow.hs + :start-after: START 0nextState + :end-before: END 0nextState + +You can see that the ``Spec`` monad allows us to withdraw and deposit +values in wallets, so that the framework can predict their expected +contents, and also to read and update the model state using the lenses +generated from its type definition. For a ``Pay`` action, we withdraw +the payment from the wallet, and record the contribution in the model +state, using ``(%=)`` to update the ``contributions`` field. For a +``Redeem`` action, we read the targets from the model state (using +``viewContractState`` and the lens generated from the type +definition), and then make the corresponding payments to the wallets +concerned. In both cases we tell the model to ``wait`` one slot, +corresponding to the ``delay`` call in ``perform``; this is necessary +to avoid the model and the emulator getting out of sync. + +We also have to specify the *initial* model state at the beginning of +each test: we just record that no contributions have been made yet, +along with the targets we chose for testing with. + +.. literalinclude:: Escrow.hs + :start-after: START initialState + :end-before: END initialState + +Given these definitions, the framework can predict the expected model +state after any sequence of ``Action``. + +Generating Actions +^^^^^^^^^^^^^^^^^^ + +The last step, before we can actually run tests, is to tell the +framework how to *generate* random actions. We do this by defining the +``arbitraryAction`` method, which is just a QuickCheck generator for +the ``Action`` type. It gets the current model state as a parameter, +so we can if need be adapt the generator depending on the state, but +for now that is not important: we just choose between making a payment +from a random wallet, and invoking ``redeem`` from a random +wallet. Since we expect to need several payments to fund a redemption, +we generate ``Pay`` actions a bit more often than ``Redeem`` ones. + +.. literalinclude:: Escrow.hs + :start-after: START arbitraryAction1 + :end-before: END arbitraryAction1 + +Strictly speaking the framework now has enough information to generate +and run tests, but it is good practice to define *shrinking* every +time we define *generation*; we just defined a generator for actions, +so we should define a shrinker too. We do so by defining the +``shrinkAction`` method, which, like the QuickCheck ``shrink`` +function, just returns a list of smaller ``Action`` to try replacing +an action by when a test fails. It is always worth defining a +shrinker: the small amount of effort required is repaid *very* +quickly, since failed tests become much easier to understand. + +In this case, as in most others, we can just reuse the existing +shrinking for those parts of an ``Action`` that make sense to +shrink. There is no sensible way to shrink a wallet, really, so we +just shrink the amount in a payment. + +.. literalinclude:: Escrow.hs + :start-after: START shrinkAction + :end-before: END shrinkAction + +With this definition, failing test cases will be reported with the +*minimum* payment value that causes a failure. + + +Running tests and debugging the model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We are finally ready to run some tests! We do still need to define a +*property* that we can call QuickCheck with, but the ``ContractModel`` +framework provides a standard one that we can just reuse. So we define + +.. literalinclude:: Escrow.hs + :start-after: START prop_Escrow + :end-before: END prop_Escrow + +The important information here is in the type signature, which tells +QuickCheck *which* contract model we want to generate and run tests +for. + +A failing test +'''''''''''''' + +Once the property is defined, we are ready to test--and a test fails +immediately! This is not unexpected--it is quite rare that a model and +implementation match on the first try, so we should expect a little +debugging--*of the model*--before we start to find interesting bugs in +contracts. When models are written *after* the implementation, as in +this case, then the new code--the model code--is likely to be where +bugs appear first. + +Looking at the test output, the first thing QuickCheck reports is the +failed test case: + +.. code-block:: text + + Prelude Spec.Tutorial.Escrow Test.QuickCheck Main> quickCheck prop_Escrow + *** Failed! Assertion failed (after 7 tests and 2 shrinks): + Actions + [Redeem (Wallet 5)] + +Here we see what generated tests looks like: they are essentially +lists of ``Action``, performed in sequence. In this case there is only +one ``Action``: wallet 5 just attempted to redeem the funds in the +contract. + +The next lines of output tell us why the test failed: + +.. code-block:: text + + Expected funds of W[2] to change by + Value (Map [(,Map [("",20000000)])]) + but they did not change + Expected funds of W[1] to change by + Value (Map [(,Map [("",10000000)])]) + but they did not change + +Remember we defined the expected payout to be 10 Ada to ``w1``, and 20 +Ada to ``w2``. Our model says (in ``nextState``) that when we perform +a ``Redeem`` then the payout should be made (in Lovelace, not Ada, +which is why the numbers are a million times larger than those in the +model). But the wallets did not get the money--which is hardly +surprising since no payments at all have been made *to* the contract, +so there is no money to disburse. + +The remaining output displays a log from the failing contract +instance, and the emulator log, both containing the line + +.. code-block:: text + + Contract instance stopped with error: RedeemFailed NotEnoughFundsAtAddress + +This is an error thrown by the off-chain ``redeem`` endpoint code, +which (quite correctly) checks the funds available, and fails since there +are not enough. + +Positive testing with preconditions +''''''''''''''''''''''''''''''''''' + +We now have a failing test, that highlights a discrepancy between the +model and the implementation--and it is the model that is wrong. The +question is how to fix it, and there is a choice to be made. Either we +could decide that the ``nextState`` function in the model should +*check* whether sufficient funds are available, and if they are not, +predict that no payments are made. Or perhaps, we should *restrict our +tests so they do not attempt to use ``Redeem`` when it should not +succeed*. + +Both choices are reasonable. The first alternative is usually called +*negative testing*--we deliberately test error situations, and make +sure that the implementation correctly detects and handles those +errors. The second alternative is *positive testing* (or "happy path" +testing), when we make sure that the implementation provides the +functionality that it should, when the user makes *correct* use of its +API. + +It is usually a good idea to focus on positive testing first--indeed, +good positive testing is a prerequisite for good negative testing, +because it enables us to get the system into a wide variety of +interesting states (in which to perform negative tests). So we shall +return to negative testing later, and focus--in this section--on +making positive testing work well. + +To do so, we have to *restrict* test cases, so that they do not +include ``Redeem`` actions, when there are insufficient funds in the +escrow. We restrict actions by defining the ``precondition`` method of +the ``ContractModel`` class: any ``Action`` for which ``precondition`` +returns ``False`` will *not* be included in any generated test. The +``precondition`` method is also given the current ``ModelState`` as a +parameter, so that it can decide to accept or reject an ``Action`` +based on where it appears in a test. + +In this case, we want to *allow* a ``Redeem`` action only if there are +sufficient funds in the escrow, so we just need to compare the +contributions made so far to the targets: + +.. literalinclude:: Escrow.hs + :start-after: START precondition1 + :end-before: END precondition1 + +In this code, ``s`` is the entire model state maintained by the +framework (including wallet contents, slot number etc), but it +contains the "contract state", which is the state we have defined +ourselves, the ``EscrowModel``. The *lens* ``contractState +. contributions . to fold`` extracts the ``EscrowModel``, extracts the +``contributions`` field from it, and then combines all the ``Value`` +using ``fold``. When we apply it to ``s`` using ``(^.)``, then we get +the total value of all contributions. Likewise, the second lens +application computes the combined value of all the targets. If the +contributions exceed the targets, then the ``Redeem`` is allowed; +otherwise, it will not be included in the test. Once +we define ``precondition``, then it has to be defined for every form +of ``Action``, so we just add a default branch that returns ``True``. + +.. note:: + + We can't use ``(>=)`` to compare ``Value``; there is no + ``Ord`` instance. That is because some ``Value`` are incomparable, + such as one Ada and one NFT, which would break our expectations about + ``Ord``. That is why we have to compare them using ``geq`` instead. + +With this precondition, the failing test we have seen can no longer be +generated, and will not appear again in our ``quickCheck`` runs. + +A second infelicity in the model +'''''''''''''''''''''''''''''''' + +Adding a precondition for ``Redeem`` prevents the previous failing +test from being generated, but it does not make the tests pass: it +just allows QuickCheck to reveal the next problem in the +model. Running tests again, we see: + +.. code-block:: text + + Prelude Spec.Tutorial.Escrow Test.QuickCheck Main> quickCheck prop_Escrow + *** Failed! Assertion failed (after 4 tests and 5 shrinks): + Actions + [Pay (Wallet 2) 0] + +This time the test just consists of a single ``Pay`` action, making a +payment of zero (!) Ada to the the contract. + +.. note:: + + It may seem surprising that the test tries to make a zero payment, + given that the *generator* we saw above only generates payments in + the range 1 to 30 Ada. But remember that the failing test cases we + see are not necessarily freshly generated, they may also have been + *shrunk*. In this case, the zero is a result of shrinking: the + shrinker we saw can certainly shrink payments to zero, and the + *precondition* for ``Pay`` allows that... it's always ``True``. And + so, a zero payment can appear in tests. If we wanted to prevent + this, the correct way would be to tighten the precondition of + ``Pay``. + +The next part of the output explains why the test failed: + +.. code-block:: text + + Expected funds of W[2] to change by + Value (Map []) + but they changed by + Value (Map [(,Map [("",-2000000)])]) + a discrepancy of + Value (Map [(,Map [("",-2000000)])]) + +In other words, the model expected that a payment of zero would not +affect the funds held by the calling wallet, but in fact, the wallet +lost 2 Ada. + +Why did this happen? In this case, the emulator log that follows +provides an explanation: + +.. code-block:: text + + . + . + . + [INFO] Slot 1: W[2]: Balancing an unbalanced transaction: + Tx: + Tx 2dc052b47a1faeacc0f50b99359990302885a34104df0109576597cc490b8a98: + {inputs: + collateral inputs: + outputs: + - Value (Map [(,Map [("",2000000)])]) addressed to + ScriptCredential: bcf453ff769866e23d14d5104c36ce4da0ff5bcbed23c622f46b94f1 (no staking credential) + mint: Value (Map []) + fee: Value (Map []) + mps: + signatures: + validity range: Interval {ivFrom = LowerBound NegInf True, ivTo = UpperBound PosInf True} + data: + "\128\164\244[V\184\141\DC19\218#\188L quickCheck prop_Escrow + *** Failed! Assertion failed (after 8 tests and 5 shrinks): + Actions + [Pay (Wallet 4) 11, + Pay (Wallet 5) 20, + Redeem (Wallet 5)] + Expected funds of W[5] to change by + Value (Map [(,Map [("",-20000000)])]) + but they changed by + Value (Map [(,Map [("",-19000000)])]) + a discrepancy of + Value (Map [(,Map [("",1000000)])]) + +Here we made two payments, totalling 31 Ada, *which is exactly one Ada +more than the combined targets* (recall our targets are 10 Ada to +``w1`` and 20 Ada to ``w2``). Then ``w5`` redeemed the escrow, *and +ended up with 1 Ada too much* (last line). That extra Ada is, of +course, the extra unnecessary Ada that was paid to the script in the +previous action. + +This raises the question: what *should* happen if an escrow holds more +funds than are needed to make the target payments? The designers of +this contract decided that any surplus should be paid to the wallet +submitting the ``redeem`` transaction. Since this is part of the +intended behaviour of the contract, then our model has to reflect +it. We can do so with a small extension to the ``nextState`` function +in the model: + +.. literalinclude:: Escrow.hs + :start-after: START nextState1 + :end-before: END nextState1 + +The extra code just computes the total contributions and the surplus, +and deposits the surplus in the calling wallet. + +Now, at last, the tests pass! + +.. code-block:: text + + Prelude Spec.Tutorial.Escrow Test.QuickCheck Main> quickCheck prop_Escrow + +++ OK, passed 100 tests. + +By default, quickCheck runs 100 tests, which is enough to reveal +easily-caught bugs such as those we have seen, but far too few to +catch really subtle issues. So at this point, it's wise to run many +more tests--the number is limited only by your patience and the speed +of the emulator: + +.. code-block:: text + + Prelude Spec.Tutorial.Escrow Test.QuickCheck Main> quickCheck . withMaxSuccess 1000 $ prop_Escrow + +++ OK, passed 1000 tests. + +Analysing the distribution of tests +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Once tests are passing, then the framework displays statistics +collected from the running tests. These statistics give us important +information about the *effectiveness* of our tests; a risk with any +automated test case generation is that, since we do not usually +inspect the running tests, we may not notice if almost all of them +are trivial. + +The contract model framework gathers some basic statistics by default, +and can be configured to gather more, but for now we just consider the +built-in ones. After each successful test run, we see a number of +tables, starting with a distribution of the actions performed by +tests: + +.. code-block:: text + + Prelude Spec.Tutorial.Escrow Test.QuickCheck Main> quickCheck . withMaxSuccess 1000 $ prop_Escrow + +++ OK, passed 1000 tests. + + Actions (25363 in total): + 75.894% Pay + 12.771% Redeem + 11.335% WaitUntil + +Here we ran 1,000 tests, and as we see from the table, around 25,000 +actions were generated. So, on average, each test case consisted of +around 25 actions. + +Of those actions, three quarters were ``Pay`` actions, and 10-15% were +``Redeem``. This is not unreasonable--we decided when we wrote the +``Action`` generator to generate more payments than redemptions. The +remaining actions are ``WaitUntil`` actions, inserted by the +framework, which simply wait a number of slots to test for timing +dependence; we shall return to them later, but can ignore them for +now. Thus this distribution looks quite reasonable. + +The second table that appears tells us how often a *generated* +``Action`` could not be included in a test, because its *precondition* +failed. + +.. code-block:: text + + Actions rejected by precondition (360 in total): + 87.8% Redeem + 12.2% Pay + +We can see that 360 actions--in addition to the 25,000 that were +included in tests--were generated, but *discarded* because their +preconditions were not true. This does represent wasted generation +effort, although rejecting 360 out of over 25,000 actions is not +really a serious problem--especially given that test case generation +is so very much faster than the emulator. + +Nevertheless, we can see that the vast majority of rejected actions +were ``Redeem`` actions, and this is because a ``Redeem`` is not +allowed until sufficient payments have been made--but our generator +produces them anyway. + +We can, of course, change this, to generate ``Redeem`` actions only +when redemption is actually possible: + +.. literalinclude:: Escrow.hs + :start-after: START arbitraryAction2 + :end-before: END arbitraryAction2 + +Measuring the distribution again after this change, we see that only +valid ``Redeem`` actions are now generated; the only discarded actions +are ``Pay`` actions. + +.. code-block:: text + + Prelude Spec.Tutorial.Escrow Test.QuickCheck Main> quickCheck . withMaxSuccess 1000 $ prop_Escrow + +++ OK, passed 1000 tests. + + Actions (25693 in total): + 76.717% Pay + 13.035% Redeem + 10.248% WaitUntil + + Actions rejected by precondition (650 in total): + 100.0% Pay + +The main *disadvantage* of making this change is that it limits the +tests that *can* be generated, if the precondition of ``Redeem`` +should be changed in the future. In particular, when we move on to +negative testing, then we will want to test invalid attempts to redeem +the escrow also. Once the generator is changed like this, then +relaxing the precondition is no longer enough to introduce invalid +calls. For this reason it could be preferable to *keep* the +possibility of generation invalid calls alongside the generator for +valid calls, but to assign the potentially-invalid generator a much +lower weight. + +We will discuss the remaining tables in a later section. + +Exercises +^^^^^^^^^ + +You can find the final version of the contract model discussed in this +section in ``Spec.Tutorial.Escrow1``, in the ``plutus-apps`` repo. + +#. Try running the code in ``ghci``. You can do so by starting a + ``nix`` shell, and starting ``ghci`` using + + .. code-block:: text + + cabal repl plutus-use-cases-test + + Then import QuickCheck and the contract model: + + .. code-block:: text + + import Test.QuickCheck + import Spec.Tutorial.Escrow1 + + and run tests using + + .. code-block:: text + + quickCheck prop_Escrow + + The tests should pass, and you should see tables showing the + distribution of tested actions, and so on. + +#. Try removing the preconditions for ``Pay`` and ``Redeem``, and + reinserting them one by one. Run ``quickCheck`` after each change, + and inspect the failing tests that are generated. + +#. Try removing the line + + .. code-block:: text + + deposit w leftoverValue + + from the ``nextState`` function, and verify that tests fail as + expected. + +#. Try removing one of the lines + + .. code-block:: text + + wait 1 + + from the ``nextState`` function (so that the model and the + implementation get out of sync). What happens when you run tests? + +#. This model does generate ``Pay`` actions that are discarded by the + precondition. Adjust the generator so that invalid ``Pay`` actions + are no longer generated, and run ``quickCheck`` to verify that this + is no longer the case. + + +Parameterising Models and Dynamic Contract Instances +---------------------------------------------------- + +One of the unsatisfactory aspects of the tests developed in the +previous section is that they *always* pay 10 Ada to wallet 1, and 20 +Ada to wallet 2. What if the contract only works for certain amounts, +or what if it only works with exactly two beneficiary wallets? Of +course, we would like to *generate* a random set of payment targets +for each test. Such a generator is easy to write: + +.. literalinclude:: Escrow2.hs + :start-after: START arbitraryTargets + :end-before: END arbitraryTargets + +but it is a little more intricate to make the model *use* these +generated targets. + +There are two problems to overcome: + +#. The generated targets are an important part of a test case, *so they + must be included in the test case* somehow. But a test case is just + a list of actions. So where do we put the targets? + +#. The running contracts need to know what the targets are--but our + model just contains a static list of contract instances in the test + (``initialInstances``). How can we pass the generated targets to + each running contract instance? + + +Solve these two problems, and we can test escrows with arbitrary payout targets. The techniques we learn will be applicable in many other situations. + +Adding an initial action, and test case phases +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The first problem is quite easy to solve, in principle. The generated +payment targets are an important part of a test case, and a test case +is just a list of actions, therefore the generated payment targets +must be included in one or more of the actions. Quite simply, *any +generated data in a contract model test must be part of an action*. In +this case, we just decide that every test should begin with an +``Init`` action, that specifies the targets to be used in this test +case. So we must extend the ``Action`` type to include ``Init``: + +.. literalinclude:: Escrow2.hs + :start-after: START Action + :end-before: END Action + +We must also ensure that ``Init`` actions *only* appear as the first +action of a test case, and that every test case starts with an +``Init`` action. We restrict the form of test cases using +preconditions, so this means that we must refine the ``precondition`` +function so that the ``Init`` precondition only holds at the beginning +of a test case, and the other operations' preconditions only hold +*after* an ``Init`` has taken place. + +However, the ``precondition`` method is only given the action and the +contract state as parameters, which means in turn that we must be able +to tell whether or not we are at the beginning of the test case, just +from the model state. So we have to add a field to the model, to keep +track of where in a test case we are. In this simple case we could +just add a boolean ``initialised``, but we will be a little more +general and say that a test case is made up of a number of *phases*, +in this case just two, ``Initial`` and ``Running``: + +.. literalinclude:: Escrow2.hs + :start-after: START ModelState + :end-before: END ModelState + +Now we can specify that at the beginning of a test case we are in the +``Initial`` phase, and there are no targets: + +.. literalinclude:: Escrow2.hs + :start-after: START initialState + :end-before: END initialState + +and that when we model the ``Init`` action, we update both the phase +and the targets accordingly: + +.. literalinclude:: Escrow2.hs + :start-after: START nextState + :end-before: END nextState + +We have to specify how to perform an ``Init`` action also, but in this +case it exists only to initialise the model state with generated +targets, so performing it need not do anything: + +.. literalinclude:: Escrow2.hs + :start-after: START perform + :end-before: END perform + +Now we can add a precondition for ``Init``, and restrict the other +actions to the ``Running`` phase only: + +.. literalinclude:: Escrow2.hs + :start-after: START precondition + :end-before: END precondition + +It only remains to *generate* ``Init`` actions, using the +generator for targets that we saw above. We can take the phase into +account, and generate an ``Init`` action only at the start of the test +case, and other actions only in the ``Running`` phase. + +.. literalinclude:: Escrow2.hs + :start-after: START arbitraryAction + :end-before: END arbitraryAction + +.. note:: + + Here we ensure that we always *generate* test cases that begin with + ``Init``, but this is *not* enough to ensure that every test case + we *run* begins with ``Init``. Remember that failed tests are + always shrunk, and the first thing the shrinker will try is to + discard the leading ``Init`` action (if that still results in a + failing test, which it probably will). The only way to prevent + shrinking from discarding the leading ``Init`` is for the + *preconditions* to require it to be there. This is why we focussed + on writing the preconditions first: they are more important. + +As a matter of principle, when we write a generator, we also write a shrinker, which just requires a one-line addition to the ``shrinkAction`` function: + +.. literalinclude:: Escrow2.hs + :start-after: START shrinkAction + :end-before: END shrinkAction + +We cannot shrink wallets, which is why we can't simply apply +``shrink`` to the list of targets, but using the ``shrinkList`` +function from ``Test.QuickCheck`` we can easily write a shrinker that +will discard list elements and shrink the target values. + +Dynamic contract instances +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +At this point we can generate tests that begin by initialising the +escrow targets randomly, but we cannot yet run them successfully. If we try, we see failures like this: + +.. code-block:: text + + *** Failed! Assertion failed (after 11 tests and 5 shrinks): + Actions + [Init [], + Redeem (Wallet 1)] + Contract instance log failed to validate: + ... + Slot 1: 00000000-0000-4000-8000-000000000000 {Wallet W[1]}: + Contract instance stopped with error: RedeemFailed NotEnoughFundsAtAddress + ... + +Here we started a test with an empty list of targets, and tried to +redeem the escrow, but failed because there were 'not enough +funds'. Why not? Because *the contracts we are running still expect +the fixed targets* that we started with; we have not yet passed our generated +targets to the contract instances under test. + +Recall the contract we are testing: + +.. literalinclude:: Escrow.hs + :start-after: START testContract + :end-before: END testContract + + +It invokes the contract endpoints with the fixed set of ``EscrowParams`` +we defined earlier. Clearly we need to parameterise the contract on +these ``EscrowParams`` instead: + +.. literalinclude:: Escrow2.hs + :start-after: START testContract + :end-before: END testContract + +Now the question is: how do we pass this parameter to each ``testContract`` as we start them? + +Recall the way we started contracts in the previous section. We +defined the contracts to start at the beginning of a test in the +``initialInstances`` method: + +.. literalinclude:: Escrow.hs + :start-after: START initialInstances + :end-before: END initialInstances + + + +Each contract is specified by a ``StartContract``, containing not only +a contract instance key, but also a *parameter*--in this case ``()``, +since we did not need to pass any generated values to +``testContract``. Now we do need to, so we must replace that ``()`` +with escrow parameters generated from our payment targets. Moreover, +we can no longer start the contracts at the beginning of the test--we +must see the ``Init`` action first, so that we know what the generated +targets are. To do so, we redefine + +.. literalinclude:: Escrow2.hs + :start-after: START initialInstances + :end-before: END initialInstances + +and instead add a definition of the ``startInstances`` method: + +.. literalinclude:: Escrow2.hs + :start-after: START startInstances + :end-before: END startInstances + +where the escrow parameters are now constructed from our generated targets: + +.. literalinclude:: Escrow2.hs + :start-after: START escrowParams + :end-before: END escrowParams + +The effect of this is to start the contracts *just before* the +``Init`` action; in fact, using this mechanism, we can start contracts +dynamically at any point in a test case. + +.. note:: + + We should be careful to avoid reusing the same contract instance + key more than once, though, since this may lead to confusing + results. + + You may wonder why we don't simply start new contract instances in the + ``perform`` method instead. The answer is the framework needs to track + the running contract instances, and using ``startInstances`` makes + this explicit. + +The ``StartContract`` just specifies the ``ContractInstanceKey`` to be +started; we define the actual contract to start in the +``instanceContract`` method, *which receives the contract parameter* +from ``StartContract`` as its last argument. So we can just define + +.. literalinclude:: Escrow2.hs + :start-after: START instanceContract + :end-before: END instanceContract + +and our work is (almost) done. The last step is just to update the +*type* of ``WalletKey``, since it includes the type of the parameter +that ``StartContract`` accepts. + +.. literalinclude:: Escrow2.hs + :start-after: START ContractInstanceKey + :end-before: END ContractInstanceKey + +Now, at last, our extended model is complete. + +Running our extended tests; another design issue +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We can now run our new tests, and, as so often happens when the scope +of QuickCheck tests is extended, they do not pass. Here is an example +of a failure: + +.. code-block:: text + + Actions + [Init [(Wallet 5,0)], + Redeem (Wallet 4)] + Expected funds of W[4] to change by + Value (Map []) + but they changed by + Value (Map [(,Map [("",-2000000)])]) + a discrepancy of + Value (Map [(,Map [("",-2000000)])]) + Expected funds of W[5] to change by + Value (Map []) + but they changed by + Value (Map [(,Map [("",2000000)])]) + a discrepancy of + Value (Map [(,Map [("",2000000)])]) + Test failed. + +In this case the generated target just specifies that wallet 5 should +receive 0 Ada--a slightly odd target, perhaps, but not obviously +invalid. Since the total of all targets is 0 Ada, then the target is +already met, and wallet 4 attempts to redeem the escrow. We might +expect the effect to be a no-op--and this is what our model +predicts--but it is not what happens. Instead, *wallet 4 pays two Ada +to wallet 5*! + +The reason this happens is the blockchain rule that every transaction +output must contain at least 2 Ada. When wallet 4 attempts to redeem +the escrow, then the off-chain code attempts to create a transaction +with an output paying 0 Ada to wallet 5, but that is increased to 2 +Ada to make the transaction valid. Then when the transaction is +balanced, the 2 Ada is taken from the submitting wallet. + +Is this a bug in the contract? It is certainly an inconsistency with +the ``nextState`` function in the model, and we could modify that +function to reflect the *actual* transfers of Ada that the contract +performs. But these transfers were surely not intentional: a more +reasonable approach is to say that target payments that are too small +to be accepted by the blockchain are invalid; such targets should not +be chosen. + +We can make our tests pass by tightening the precondition of ``Init`` +so that targets below the minimum are not accepted: + +.. literalinclude:: Escrow2.hs + :start-after: START tightprecondition + :end-before: END tightprecondition + +This demonstrates that the contract works as expected, provided we +*don't* specify targets less than the minimum, but nothing prevents a +*user* of the contract from specifying such targets--and we know that +the contract code will accept them, and deliver surprising results in +those cases. Arguably *all* the contract endpoints should check that +the specified targets are valid, and raise an error if they are +not. This would prevent the *creation* of invalid escrows, rather than +generating unexpected behaviour when they are redeemed. + +Thus these failing tests *do* suggest a way in which the contract +implementation can be improved, even if the failing cases are fairly +unlikely in practice. + +.. note:: + + QuickCheck was able to find this bug because our *generator* for + target payments includes invalid values; we chose values in the + range 1 to 31, where 1 is invalid (and shrinking reduced the 1 to a + 0 in the failing case that was reported). It is a good thing we did + not ensure, from the start, that only valid target values could be + generated--had we done so, we would not have discovered this + anomalous behaviour. + + In general, it is a good idea for generators to produce, at least + occasionally, every kind of input that a user can actually supply, + even if some of them are invalid (and may be filtered out by + preconditions). Doing so enables this kind of strange behaviour to + be discovered. + +Exercises +^^^^^^^^^ + +#. You will find the code presented here in ``Spec.Tutorial.Escrow2``\, + with the exception of the last precondition we discussed for + ``Init``\. Run the tests using + + .. code-block:: text + + quickCheck prop_Escrow + + and make sure you understand how they fail. + +#. Make your own copy of the code, and add the tighter precondition + for ``Init``\. Verify that the tests then pass. + +#. An alternative explanation for the problem might have been that a + target of *zero* should not be allowed (and perhaps the contract + implementation should interpret a target of zero by not creating a + transaction output at all). *Change the precondition* of ``Init`` + so that it only excludes targets of zero, rather than any target + below the minimum. Verify that the tests still fail, and make sure + you understand the (slightly more complex) failure. + +#. There are quite a few steps involved in introducing these + dynamically chosen targets. You can practice these steps by taking + the code from ``Spec.Tutorial.Escrow1``\, which uses fixed targets, + and following the steps outlined in this tutorial to turn it into a + copy of ``Spec.Tutorial.Escrow2``. + + + +Testing "No Locked Funds" with Dynamic Logic +-------------------------------------------- + +So far, we have tested that a contract's actual transfers of tokens +are consistent with the model. That is, *nothing goes wrong*--or to +put it bluntly, nobody steals your money. This is an example of a +*safety property*. But when we use smart contracts, this is not the +only kind of property we care about. Very often, we *also* want to be +certain that we can eventually reach some kind of *goal* state--an +example of a *liveness property*. In particular, it would be bad if +tokens were to be trapped in a contract for ever, with no possibility +of recovering them. The Cardano model certainly allows this... imagine +a UTXO whose verifier always returns ``False``\... and so it is our +responsibility to ensure that contracts do not fall into this +trap. Not only does nothing go wrong, but *something good is always +possible*. Not only does no-one steal your money, but you can always +recover it yourself. + +We call these properties "no locked funds" properties, because that is +usually what we want to test: that we can always reach a state in +which all tokens have been recovered from the contracts under test. Of +course, there is no *general* way to recover tokens held by a +contract, so we cannot expect QuickCheck to find a way to reach this +goal automatically; instead, we *specify a strategy* for recovering +funds, and what we test is that the given strategy always works. + +Writing and testing properties using Dynamic Logic +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We specify this kind of property using *dynamic logic*. +This part of the contract testing framework is inspired +by `dynamic logic for reasoning about programs +`_, but it +can be thought of just as a way of writing *test scenarios*, in +which we mix random generation, explicit actions, and assertions. We +write such scenarios in the ``DL`` monad; for example, here is a scenario +that first performs a random sequence of actions, then invokes a +finishing strategy, and finally asserts that no tokens remain locked +in contracts. + +.. literalinclude:: Escrow3.hs + :start-after: START finishEscrow + :end-before: END finishEscrow + +Here ``assertModel`` lets us include an assertion about the contract +model state, ``lockedValue`` is a function provided by the framework +that computes the total value held by contracts, and ``symIsZero`` +checks that this is zero. The value is returned here as a +``SymValue`` (and we will return to the need for this in a later +section), but for now it can be thought of just as a normal Plutus ``Value`` +with an extra type wrapper. + +This scenario just tests that the given finishing strategy always +succeeds in recovering all tokens from contracts, no matter what +actions have been performed beforehand. The finishing strategy itself +is written in the same monad. For example, if we think we should use a +``Redeem`` action to recover the tokens, then we can define + +.. literalinclude:: Escrow3.hs + :start-after: START badFinishingStrategy + :end-before: END badFinishingStrategy + +Of course, since the strategy must work in any state, including the +initial one, then we do have to check that the escrow has been +initialised before we attempt to ``Redeem``. + +.. note:: + + These test scenarios are very flexible, and can be used for other + purposes too. For example, we could write a test scenario that fixes + the escrow targets, thus undoing the generalization we made in the previous section: + + .. literalinclude:: Escrow3.hs + :start-after: START fixedTargets + :end-before: END fixedTargets + + Note that generated actions are always *appropriate for the current + state*, so here ``anyActions_`` will pick up generating the test case + from the point after the escrow targets are initialised. + + We can use dynamic logic to express everything from unit tests to full + random generation (by just specifying ``anyActions_`` as the + scenario). But for now, we focus on testing "no locked funds" properties. + +Now, dynamic logic just specifies a *generator* for tests to +perform; we still need to specify *how* to perform those +tests. Usually, we just reuse the existing property we have already +written, which runs the test case on the emulator and performs the +usual checks. In this case, we can define + +.. literalinclude:: Escrow3.hs + :start-after: START prop_FinishEscrow + :end-before: END prop_FinishEscrow + + +Then we can run the tests by passing the property to ``quickCheck``, as usual: + + .. code-block:: text + + > quickCheck prop_FinishEscrow + *** Failed! Falsified (after 1 test and 3 shrinks): + BadPrecondition + [Do $ Init [(Wallet 2,2)]] + [Action (Redeem (Wallet 1))] + (EscrowModel {_contributions = fromList [], + _targets = fromList [(Wallet 2,Value (Map [(,Map [("",2000000)])]))], + _phase = Running}) + + BadPrecondition + [Do $ Var 0 := Init [(Wallet 2,2)]] + Some (Redeem (Wallet 1)) + +The property fails, which is not surprising: our "finishing strategy" +is quite simplistic, and not yet expected to work. But let us inspect +the error message. The test failed because of a bad precondition, +after running the sequence + + .. code-block:: text + + Init [(Wallet 2,2)] + +So we set up a target to pay wallet 2 a sum of 2 Ada. Then we tried to +apply our finishing strategy, which is just for wallet 1 to issue a +``Redeem`` request: + + .. code-block:: text + + Redeem (Wallet 1) + +This wasn't possible, because the precondition of ``Redeem`` wasn't +satisfied. The message also shows us the model state--we have set up +the escrow targets successfully, but there are no contributions, and +the ``Redeem`` precondition says that the contributions must cover the +targets before ``Redeem`` is possible. So of course, it doesn't work. + +But the counterexample does show us what we need to do to *make* +``Redeem`` possible: we need to pay in sufficient contributions to +cover the targets. So that suggests a refined finishing strategy: + + +.. literalinclude:: Escrow3.hs + :start-after: START finishingStrategy + :end-before: END finishingStrategy + +We read the contributions and targets from the contract state, compute +the remaining deficit, and if the deficit is positive, then we make a +payment to cover it. After this, a ``Redeem`` should be +successful. And indeed, testing the property passes: this finishing +strategy works. + + .. code-block:: text + + > quickCheck . withMaxSuccess 1000 $ prop_FinishEscrow + +++ OK, passed 1000 tests. + + Actions (51925 in total): + 73.483% Pay + 14.278% Redeem + 10.315% WaitUntil + 1.924% Init + +.. _StrictRedeem: + +Digression: revisiting a design decision +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In section :ref:`DesignIssue` above, we discussed the situation in +which contributors pay in *more* to the escrow than is needed to meet +the targets. The actual contract allows that, and so do we in our +model; as a consequence we had to *specify* where the surplus funds +end up on redemption (in the wallet that invokes ``Redeem``). But +there is another approach we could have taken: we could simply have +said that ``Redeem`` *requires* the contributions and targets to match +exactly, by strengthening the precondition: + +.. literalinclude:: Escrow3.hs + :start-after: START strongPrecondition + :end-before: END strongPrecondition + +This does make ``prop_Escrow`` pass: + + .. code-block:: text + + > quickCheck prop_Escrow + +++ OK, passed 100 tests. + + Actions (2845 in total): + 82.81% Pay + 13.78% WaitUntil + 3.30% Init + 0.11% Redeem + + Actions rejected by precondition (870 in total): + 88.3% Redeem + 10.8% Pay + 0.9% Init + +But should we be satisfied with this? There are warning signs in the +statistics that QuickCheck collects: + +#. We have tested ``Redeem`` an extremely small number of times. +#. A high proportion of generated ``Redeem`` actions were *discarded* by the precondition. + +The explanation for this is that we can now only include ``Redeem`` in +a test case if the previous (random) payments have hit the target +*exactly*, and this is very unlikely. Moreover, once we have overshot +the target, then further random payments cannot help. + +We could add a stronger *precondition* to ``Pay``, that forbids +payments taking us over the target, and that would result in a better +distribution of actions. But it is not a *realistic* solution, because +at the end of the day, there is no way to *prevent* someone making a +payment to a script on the Cardano blockchain. *Making a payment to a +contract does not require the contract's approval*. + +So there is a problem here, but when we test ``prop_Escrow``, then it +is revealed only by careful inspection of the generated +statistics--the property does not *fail*. + +On the other hand, when we test ``prop_FinishEscrow``, then it fails immediately: + + .. code-block:: text + + > quickCheck prop_FinishEscrow + *** Failed! Falsified (after 5 tests and 6 shrinks): + BadPrecondition + [Do $ Init [], + Do $ Pay (Wallet 2) 2] + [Action (Redeem (Wallet 1))] + (EscrowModel {_contributions = fromList [(Wallet 2,Value (Map [(,Map [("",2000000)])]))], + _targets = fromList [], + _phase = Running}) + + BadPrecondition + [Do $ Var 0 := Init [],Do $ Var 3 := Pay (Wallet 2) 2] + Some (Redeem (Wallet 1)) + +The counterexample sets up an escrow with an empty list of targets +(which may seem odd, but is allowed, and tells us that no particular +targets are *needed* to make the property fail). Then it makes a +payment to the escrow, thus overshooting the targets. Finally, we try +to use the given finishing strategy--which just attempts to use +``Redeem``, and fails because the strong precondition we wrote does +not allow it. + +In this case, not only does the given finishing strategy fail, but the +bug cannot be fixed: *there is no possible finishing strategy that +works*. Once we have overshot the targets, there is no way to return +to a state in which ``Redeem`` is possible! And that is why the +contract authors did *not* follow this path: had they done so, then an +attacker would be able to 'brick' an escrow contract just by making an +unexpected payment to it. + +Fair's fair: Unilateral strategies +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We saw above how to test that a finishing strategy succeeds in +recovering all the tokens. But not all strategies are created +equal. For example, suppose you use an escrow contract to buy an +NFT. You place your funds in the escrow, but before the seller can +place the NFT there, they get a better offer. Now the seller will +never place the NFT in the escrow--and neither can the buyer--and so +the buyer's funds *will* be locked for ever, even though there is a +way (using the NFT) to recover them. + +This little story shows that there is a need for each wallet to be +able to recover their "fair share" of the funds in the contract, +without any other wallet's cooperation. And the contract model +framework provides a way of testing this too. + +The idea is to provide *two* strategies, one that recovers all the +tokens from contracts, and is also interpreted to define each wallet's +"fair share", and a second strategy *that can be followed by any +single wallet*, and recovers that wallet's tokens. We call this kind +of strategy a *unilateral* strategy; it is defined in the ``DL`` monad +in just the same way as the strategies we saw earlier, but only a +single wallet is allowed to perform actions. Indeed, this is why we +gave ``finishingStrategy`` a wallet as a parameter: it defines the +unilateral strategy for that wallet. Since the strategy uses +``Redeem``, which actually pays out *all* the targets, then we can +reuse it as the general strategy too, just by choosing a wallet to +perform it (and we chose wallet 1 above). + +The framework lets us package the general and unilateral strategies +together, into a "no locked funds proof": + +.. literalinclude:: Escrow3.hs + :start-after: START noLockProof + :end-before: END noLockProof + +.. note:: + + There are other components in a ``NoLockedFundsProof``, which we + will see later; we can ignore them for now, but we do need to take + suitable default values from ``defaultNLFP`` in the definition + above. + +and we can test them together using ``checkNoLockedFundsProof`` + +.. literalinclude:: Escrow3.hs + :start-after: START prop_NoLockedFunds + :end-before: END prop_NoLockedFunds + +.. code-block:: text + + > quickCheck prop_NoLockedFunds + *** Failed! Falsified (after 1 test and 5 shrinks): + DLScript + [Do $ Init [(Wallet 4,2)]] + + Unilateral strategy for Wallet 4 should have gotten it at least + SymValue {symValMap = fromList [], actualValPart = Value (Map [(,Map [("",2000000)])])} + but it got + SymValue {symValMap = fromList [], actualValPart = Value (Map [])} + +The property actually fails, because if all we do is create a target +that wallet 4 should receive 2 Ada, then wallet 4's unilateral +strategy is unable to recover that--even though, when wallet 1 follows +the strategy, then wallet 4 does receive the money. + +What happens here is that the *general* strategy, which is just the +same strategy followed by wallet 1, *does* pay out to wallet 4, and so +we *define* wallet 4's "fair share" to be 2 Ada. But this isn't really +right, because since no Ada have been paid into the contract, then +there are no tokens to disburse. Indeed, if anything, the "general" +strategy is *unfair* to wallet 1, which has to stump up 2 Ada in this +situation so that the escrow can be redeemed. So this test failure +does reveal a fairness problem, even if the victim is really wallet 1 +rather than wallet 4. + +We will see how to fix this problem in the next section. In the +meantime, to summarize, defining a ``NoLockedFundsProof`` requires us + +#. to define a general strategy that can recover *all* the tokens from + the contracts under test, and moreover implies a *fair share* of + the tokens for each wallet *in any state* (for example, a fair + share of the profits-so-far of any trading contract), + +#. to define a *unilateral strategy* for each wallet, that can recover + that wallet's fair share of the tokens from any state, without + cooperation from any other wallet. + + +Fixing the contract: refunds +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The fundamental problem with the finishing strategy we have developed +so far, is that in order to recover tokens already held by the +contract, we may need to pay in even more tokens! This seems a poor +design. It would make far more sense, in the event that the contract +is not followed to completion, to *refund* contributions to the +wallets that made them. And indeed, the actual implementation of the +contract supports a ``refund`` endpoint as well. + +To add refunds to our model, we need to add a new action + +.. literalinclude:: Escrow3.hs + :start-after: START EscrowModel + :end-before: END EscrowModel + +and add it to ``nextState``, ``precondition``, ``perform``, and ``arbitraryAction``: + +.. literalinclude:: Escrow3.hs + :start-after: START RefundModel + :end-before: END RefundModel + +(In the ``nextState`` clause, the first line uses a more complex lens +to extract the contributions, select the value for wallet ``w``, if +present, and then pass the resulting ``Maybe Value`` to ``fold``, thus +returning zero if there was no contribution, and the value itself if +there was). We also have to extend the ``testContract`` to include the +refund endpoint: + +.. literalinclude:: Escrow3.hs + :start-after: START testContract + :end-before: END testContract + + +With these additions, ``prop_Escrow`` still passes, but +now tests refunds as well: + +.. code-block:: text + + > quickCheck prop_Escrow + +++ OK, passed 100 tests. + + Actions (2625 in total): + 66.44% Pay + 12.46% WaitUntil + 9.64% Redeem + 7.96% Refund + 3.50% Init + + Actions rejected by precondition (478 in total): + 85.8% Refund + 12.6% Pay + 1.7% Init + +We can see that ``Refund`` is tested almost as often as ``Redeem``, +although many refunds are rejected by the precondition (which requires +that there actually *is* a contribution to refund). This isn't a big +deal, though, because the overall proportion of rejected actions is +low (15%), and sufficiently many ``Refund`` actions are being tested. + +The payoff, though, is that we can now define a far better finishing +strategy: the general strategy will just refund all the contributions, +and the unilateral strategies will claim a refund for the +wallet concerned. + +.. literalinclude:: Escrow3.hs + :start-after: START BetterStrategies + :end-before: END BetterStrategies + +.. note:: + + Here we use ``monitor`` to gather statistics on the number of + wallets receiving refunds during the finishing strategy, just to + make sure, for example, that it is not always zero. We place such + monitoring in the *general* strategy, not the wallet-specific ones, + because the general strategy is invoked exactly once per test, + while the wallet-specific ones may be invoked a variable--and + unpredictable--number of times. This makes statistics gathered in + the wallet-specific strategies harder to interpret. + +We put these strategies together into a ``NoLockedFundsProof``: + +.. literalinclude:: Escrow3.hs + :start-after: START BetterNoLockProof + :end-before: END BetterNoLockProof + +and run tests: + +.. code-block:: text + + > quickCheck prop_NoLockedFunds + +++ OK, passed 100 tests. + + Actions (31076 in total): + 65.211% Pay + 11.794% WaitUntil + 10.117% Redeem + 9.506% Refund + 1.847% Init + 1.525% Unilateral + + Refunded wallets (100 in total): + 30% 2 + 23% 1 + 17% 4 + 16% 3 + 13% 0 + 1% 5 + +Now the tests pass--each wallet can indeed recover its own fair share +of tokens--and moreover we test each action fairly often, and the +number of refunded wallets has a reasonable-looking distribution. + +Exercises +^^^^^^^^^ +You will find the code presented in this section in ``Spec.Tutorial.Escrow3``\. + +#. Strengthen the precondition of ``Redeem`` to require the + contributions and targets to match exactly, as discussed in + :ref:`StrictRedeem`. Verify that ``prop_Escrow`` passes and + ``prop_FinishEscrow`` fails. Now, *add a precondition to* ``Pay`` + to disallow payments that take the contributions over the target. + + #. Test ``prop_Escrow``, and make sure it passes; have you achieved + a better distribution of actions in tests? + + #. Test ``prop_FinishEscrow``; does it pass now? + +#. The code provided uses the poor finishing strategy based on + ``Redeem``. Verify that ``prop_NoLockedFunds`` fails, and replace + the strategy with the better one described above (you will find the + code in comments in the file). Verify that ``prop_NoLockedFunds`` + passes now. + + Do not be surprised if testing ``prop_NoLockedFunds`` is + considerably slower than testing ``prop_FinishEscrow``. The latter + runs the emulator only once per test, while the former must run it + repeatedly to test each wallet's unilateral strategy. + +#. Sometimes a wallet which is targetted to *receive* funds might do + better to complete the contributions and redeem the escrow, rather + than refund its own contribution. Implement this idea as a + per-wallet strategy, and see whether ``prop_NoLockedFunds`` still + passes. Add a call of ``monitor`` to your strategy to gather + statistics on how often ``Redeem`` is used instead of ``Refund``. + + +Taking Time into Account +------------------------ + +In the last section we added refunds to our tests; now a client can +pay into an escrow, and claim a refund of their contribution +freely--but this doesn't really correspond to the intention of an +escrow contract. In reality, an escrow contract should have a deadline +for payments and redemption, with refunds permitted only after the +deadline has passed. In fact, the *real* escrow contract, in +``Plutus.Contracts.Escrow``, provides such a deadline: the main +difference between this and the simplified contract we have tested so +far, ``Plutus.Contracts.Tutorial.Escrow``, is that the latter omits +the deadline and associated checks. + +In this section, we'll switch to testing the real contract, which we +can achieve just by changing the import in our model to be the real +contract. (As usual, you can find the code presented in this section +in ``Spec.Tutorial.Escrow4``\). + +Slots and POSIXTime +^^^^^^^^^^^^^^^^^^^ + +Just changing the import leads to a compiler warning: the +``EscrowParams`` type, which is passed to the contract under test, has +a new field ``escrowDeadline``, and so far, our code does not +initialise it. We will generate the deadlines, so that they vary from +test to test, but there is a slight mismatch to overcome first. In a +contract model we measure time in *slots*, but the ``escrowDeadline`` +field is not a slot number, it is a ``POSIXTime``. So while we shall +generate the deadline as a slot number (for convenience in the model), +we must convert it to a ``POSIXTime`` before we can pass it to the +contract under test. + +To do so, we need to know when slot 0 happens in POSIX time, and how +long the duration of each slot is. These are defined in a +``SlotConfig``, a type defined in ``Ledger.TimeSlot``. In principle +the slot configuration might vary, but we will use the default values +for testing (by using ``def`` from ``Data.Default`` as our +configuration. Putting all this together, we can add a deadline to our +``EscrowParams`` as follows: + +.. literalinclude:: Escrow4.hs + :start-after: START escrowParams + :end-before: END escrowParams + + +.. note:: + + If you are familiar with the ``POSIXTime`` type from + ``Data.Time.Clock.POSIX``, then beware that *this is not the same + type*. That type has a resolution of picoseconds, while Plutus uses + its own ``POSIXTime`` type with a resolution of milliseconds. + +Initialising the deadline +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The deadline, like the escrow targets, is fixed for each test, so it +makes sense to add the deadline as a new field to the ``Init`` +action--recall that it is the ``Init`` action that starts the contract +instances under test, and so must supply the deadline as part of the +``EscrowParams``. So we add the deadline slot to this action + +.. literalinclude:: Escrow4.hs + :start-after: START Action + :end-before: END Action + +and pass it to the contracts in the ``startInstances`` method: + +.. literalinclude:: Escrow4.hs + :start-after: START startInstances + :end-before: END startInstances + +Just as we record the escrow targets in the model state, so we will +need to include the deadline as part of the model, so we extend our +model type + +.. literalinclude:: Escrow4.hs + :start-after: START EscrowModel + :end-before: END EscrowModel + +and record the deadline in our model state transition: + +.. literalinclude:: Escrow4.hs + :start-after: START nextState + :end-before: END nextState + +It just remains to generate deadline slots (we choose positive +integers), and shrink them (by reusing integer shrinking): + +.. literalinclude:: Escrow4.hs + :start-after: START arbitraryAction + :end-before: END arbitraryAction + +.. literalinclude:: Escrow4.hs + :start-after: START shrinkAction + :end-before: END shrinkAction + +Now we are ready to run tests. + + .. _Timing: + +Modelling the passage of time +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We can now run tests, but they do not pass: + +.. code-block:: text + + > quickCheck prop_Escrow + *** Failed! Assertion failed (after 5 tests and 7 shrinks): + Actions + [Init (Slot {getSlot = 0}) [], + Pay (Wallet 1) 2] + Expected funds of W[1] to change by + Value (Map [(,Map [("",-2000000)])]) + but they did not change + Test failed. + Emulator log: + [INFO] Slot 0: TxnValidate ee3a44b98e0325e19bc6be1e6f25cdb269301666a3473758296e96cd7ea9a851 + [INFO] Slot 1: 00000000-0000-4000-8000-000000000000 {Wallet W[1]}: + Contract instance started + [INFO] Slot 1: 00000000-0000-4000-8000-000000000001 {Wallet W[2]}: + Contract instance started + ... + +We tried to pay 2 Ada from wallet 1, but the payment did not take +effect. Notice that the generated deadline is slot zero, though; in +other words, the deadline passed before we started the test. This +might seem surprising, since we *generated* the deadline as a positive +integer (and zero does not count as positive), but it is the result of +shrinking. If we don't want to test a deadline of slot zero, then we +must strengthen the precondition of ``Init`` to prevent it. + +Noting that the contract instances do not start until slot one, let us +require the deadline slot to be greater than that--at least slot +two. When we add this to the precondition then tests still fail, but +the shrunk counterexample is different: + +.. code-block:: text + + > quickCheck prop_Escrow + *** Failed! Assertion failed (after 2 tests and 5 shrinks): + Actions + [Init (Slot {getSlot = 2}) [], + WaitUntil (Slot {getSlot = 2}), + Pay (Wallet 3) 2] + Expected funds of W[3] to change by + Value (Map [(,Map [("",-2000000)])]) + but they did not change + Test failed. + +This test case makes the problem easier to see: it + +#. first, initializes the deadline to slot 2 + +#. then, *waits until* slot 2, + +#. and finally, attempts a payment, which does not go through. + +The second action, ``WaitUntil``, is one we have not seen in +counterexamples previously; it only appears when *timing is important* +to provoke a failure. In this case it's now clear what the problem is: +*the contract does not allow payments after the deadline*. So the next +step is to encode this in our model. + +.. note:: + + ``WaitUntil`` actions are inserted automatically into test cases by + the framework, to explore timing dependence. It is *possible* to + control the probability of a ``WaitUntil`` action, and the + distribution of the slots that we wait for, but it is often not + *necessary*--the default behaviour is often good enough. + +The contract model framework automatically keeps track of the current +slot number for us, so we *could* write a precondition for ``Pay`` +that refers explicitly to the slot number. However, all that really +matters is *whether or not the deadline has passed*--and probably +other parts of the model will depend on this too. So it is simpler to +check for this in one place, and then just refer to it elsewhere in +the model. + +Now we can benefit from our choice earlier to introduce a ``phase`` +field in the model: hitherto it has only distinguished initialization +from running the test, but now we can add a new phase: ``Refunding`` + +.. literalinclude:: Escrow4.hs + :start-after: START Phase + :end-before: END Phase + +The idea is that when the deadline passes, we move into the +``Refunding`` phase, and we can refer to the current phase in +preconditions. In fact, our preconditions *already* refer to the +phase, so with this change then ``Pay`` and ``Redeem`` will be +restricted to take place *before* the deadline. All we have to do is +to adjust the precondition for ``Refund``, which should of course be +restricted to *after* the deadline: + +.. literalinclude:: Escrow4.hs + :start-after: START precondition + :end-before: END precondition + +One question remains: *where do we change the phase?* Changing the +phase changes the model state, but not in response to an ``Action``: +it doesn't matter whether or not an action is performed on the +deadline, the phase must change anyway. This means that *we cannot +change the phase in the* ``nextState`` *function*, because this is +invoked only when actions are performed. We need to be able to *change +the contract state in response to the passage of time*. We can do this +by defining the ``nextReactiveState`` method of the ``ContractModel`` +class. + +This method is called every time the slot number advances in the model +(although not necessarily every slot--slot numbers can jump during a +test). In this case all we need to do is compare the new slot number +with the deadline, and move to the ``Refunding`` phase if appropriate: + +.. literalinclude:: Escrow4.hs + :start-after: START nextReactiveState + :end-before: END nextReactiveState + +Now ``prop_Escrow`` passes. + +Monitoring and the distribution of tests +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Testing ``prop_Escrow`` generates some interesting statistics: + +.. code-block:: text + + > quickCheck prop_Escrow + +++ OK, passed 100 tests. + + Actions (2291 in total): + 62.03% WaitUntil + 27.37% Pay + 3.71% Redeem + 3.62% Init + 3.27% Refund + + Actions rejected by precondition (11626 in total): + 70.437% Pay + 23.757% Refund + 5.746% Redeem + 0.060% Init + +In comparison with previous versions of this property, we can see from +the first table that there are *many* more ``WaitUntil`` actions in +these tests (previously they were around 10% of the tested +actions). Moreover, we can see that many more generated actions were +rejected by their precondition: we rejected over 11,000 actions, while +generating 2291 that were included in tests. Rejecting so many actions +is undesirable: not only does it waste testing time, but there is a +risk that the *distribution* of accepted actions is quite different +from that of generated actions, which can lead to ineffective testing. + +But why do we see this behaviour? It is because *once the deadline has +passed, then neither* ``Pay`` *nor* ``Redeem`` *is possible*; when we +generate these actions, then they will *always* be rejected by their +preconditions. Moreover, *after the deadline then we can* ``Refund`` +*each wallet at most once*. Once the deadline has passed, and all the +contributions have been refunded, then the preconditions allow no +further actions--except ``WaitUntil``. And so, test case generation +will choose ``WaitUntil``, over and over again, and this is why so +many of them appear in our tests. + +The following tables tell us more about the passage of time in our tests: + +.. code-block:: text + + Wait interval (1421 in total): + 28.85% <10 + 25.83% 10-19 + 23.15% 20-29 + 15.76% 30-39 + 5.77% 40-49 + 0.63% 50-59 + + Wait until (1421 in total): + 14.07% 100-199 + 12.03% 1000-1999 + 9.29% 200-299 + 8.94% 300-399 + 7.67% 400-499 + ... + 2.32% 2000-2999 + ... + +The first table shows us *how long* we waited at each individual +occurrence of ``WaitUntil``: mostly under 30 slots, but up to 59 slots +at a maximum. The second table shows us which slot numbers we waited +until: we can see that many tests ran for several hundred slots, and +indeed, some ran for over 2000 slots. + +Luckily, waiting is cheap, but since we are performing fewer useful +actions in each test, then we should probably run more tests overall +for the same level of confidence in our code. + +No locked funds? +^^^^^^^^^^^^^^^^ + +We still need to test that we can recover all tokens from the escrow, +and do so fairly. Recall our previous finishing strategy: + +.. literalinclude:: Escrow4.hs + :start-after: START oldFinishingStrategy + :end-before: END oldFinishingStrategy + +If we just use this as it is, it will fail. As before, we begin by +testing ``prop_FinishEscrow``, before we worry about unilateral +strategies for individual wallets: + + .. code-block:: text + + > quickCheck prop_FinishEscrow + *** Failed! Falsified (after 5 tests and 5 shrinks): + BadPrecondition + [Do $ Init (Slot {getSlot = 3}) [], + Do $ Pay (Wallet 3) 2] + [Action (Refund (Wallet 3))] + (EscrowModel {_contributions = fromList [(Wallet 3,Value (Map [(,Map [("",2000000)])]))], + _targets = fromList [], + _refundSlot = Slot {getSlot = 3}, + _phase = Running}) + +In this test we set the deadline to slot 3, make a payment, and then +the finishing strategy attempts to refund the payment... in slot +two. It doesn't work: the precondition forbids a refund in that +slot. So we have to adapt our finishing strategy, which must simply +wait until the deadline before refunding the contributions. + +.. literalinclude:: Escrow4.hs + :start-after: START finishingStrategy + :end-before: END finishingStrategy + +To wait until the deadline, we use ``waitUntilDL``; since this fails +if we try to wait until a slot in the past, then we have to check the +``currentSlot`` (maintained by the model) before we decide whether or +not to wait. + +.. literalinclude:: Escrow4.hs + :start-after: START waitUntilDeadline + :end-before: END waitUntilDeadline + +With this extended strategy, the property passes: + + .. code-block:: text + + > quickCheck prop_FinishEscrow + +++ OK, passed 100 tests. + + Actions (3588 in total): + 68.87% WaitUntil + 20.71% Pay + 4.77% Refund + 3.18% Redeem + 2.48% Init + + Refunded wallets (100 in total): + 67% 0 + 13% 2 + 7% 1 + 6% 3 + 6% 4 + 1% 5 + + +The strategy works, but the statistics we gathered on the number of +wallets to be refunded in each test are a little suspect. *In two +thirds of the tests, there were no refunds to be made!* This is not +ideal, given that we are testing whether or not our refund strategy +works. + +This leads us to wonder: *which phase of the test did we reach* before +testing our finishing strategy? To find out, we can just add a couple +of lines to the ``finishingStrategy`` code, to ``monitor`` the phase: + +.. literalinclude:: Escrow4.hs + :start-after: START monitoredFinishingStrategy + :end-before: END monitoredFinishingStrategy + +Testing the property again, we see + + .. code-block:: text + + Phase (100 in total): + 68% Refunding + 32% Running + +So in two thirds of our tests, we had already reached the +``Refunding`` phase before the finishing strategy was invoked--which +means, in many cases, that the addition we made to the strategy was +not needed. + +While we certainly want to run *some* tests of the finishing strategy +starting in the ``Refunding`` phase, two thirds seems far too +many. How can we ensure that more tests invoke the strategy in the +``Running`` phase? The simplest way is just to *choose longer +deadlines*. There is no particular reason why QuickCheck's default +positive integer distribution should be the right one for +deadlines. The simplest way to increase the values chosen is just to +apply QuickCheck's ``scale`` combinator to the generator concerned: + +.. literalinclude:: Escrow4.hs + :start-after: START weightedArbitraryAction + :end-before: END weightedArbitraryAction + +Here we scale the positive integer generator by multiplying the +QuickCheck size parameter by ten before generating; the effect is to +increase the range of values by a factor of ten. + +Is ten the right number? The only way to tell is to run tests and +measure how often we reach the refunding stage: + + .. code-block:: text + + > quickCheck . withMaxSuccess 1000 $ prop_FinishFast + +++ OK, passed 1000 tests. + + Phase (1000 in total): + 81.5% Running + 18.5% Refunding + + Refunded wallets (1000 in total): + 34.1% 0 + 18.5% 1 + 17.3% 2 + 13.5% 3 + 11.2% 4 + 5.4% 5 + +It seems that we reach the refunding stage in around 20% of tests, +which seems reasonable. Moreover the propertion of cases in which +there are no refunds to be made is now lower--one third instead of two +thirds. So this is a useful improvement. + +Finally, we also need to update the unilateral strategy for each +wallet in the same way. Once we have done so, then +``prop_NoLockedFunds`` passes again. + +Digression: testing the model alone for speed +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We ran a thousand tests to measure the proportion that reach the +refunding stage, because one hundred tests is rather few to estimate +this percentage from. In fact even a thousand tests is rather few to +get accurate results; repeating that thousand-test run ten times +yielded a refunding-percentage ranging from 17.4% to 21.6%. Ideally +one might run millions of tests to measure the distribution, so we can tune +the generation more accurately. Yet running a thousand tests is already +quite slow, because of the speed of the emulator. + +However, *it is not actually necessary to run the tests, to measure +their distribution*! The measurements we are making *depend only on +the model*, and so we can make them much more rapidly by taking the +emulator out of the test. This is simple to do: recall we defined +``prop_FinishEscrow`` by + +.. literalinclude:: Escrow4.hs + :start-after: START prop_FinishEscrow + :end-before: END prop_FinishEscrow + +which *generates* a test case from the dynamic logic test scenario +``finishEscrow``, and then *runs* it using ``prop_Escrow``. All we +have to do to take out the emulator is to replace ``prop_Escrow`` by +the property that is always ``True``: + +.. literalinclude:: Escrow4.hs + :start-after: START prop_FinishFast + :end-before: END prop_FinishFast + +This property generates tests in exactly the same way, and gathers +statistics in the same way (and checks preconditions in the same +way), but does not actually run the test on the emulator. In other +words, it's an excellent test of the *model*, and can be used to tune +it (or find bugs in it) without the cost of emulation. + +With this version, we can at least run 100,000 tests in a short time, +and obtain much more accurate statistics: + + .. code-block:: text + + > quickCheck . withMaxSuccess 100000 $ prop_FinishFast + +++ OK, passed 100000 tests. + + Phase (100000 in total): + 80.514% Running + 19.486% Refunding + + Refunded wallets (100000 in total): + 34.204% 0 + 18.387% 1 + 17.514% 2 + 14.877% 3 + 10.016% 4 + 5.002% 5 + +The results confirm that the distribution of test cases is reasonably good. + +Exercises +^^^^^^^^^ + +You will find the code discussed in this section in ``Spec.Tutorial.Escrow4``. + +#. Run ``quickCheck prop_Escrow`` and observe the distributions + reported. You will see that, even though we have extended the + escrow deadlines, many actions are still rejected by their + preconditions. Adapt the *generator* for actions, so that it only + generates each action during the correct phase. How does that + affect the proportion of rejected actions? + +#. The supplied code still has a buggy ``walletStrategy``. Verify this + by checking that ``prop_NoLockedFunds`` fails, and inspect the + counterexample. Correct the ``walletStrategy``, and veryify that + ``prop_NoLockedFunds`` now passes. + +#. The code also contains a fast version of ``prop_NoLockedFunds`` + that does not run the emulator. Use *this* property to test your + model, with and without the fix to the ``walletStrategy``. You + should find that the bug is found anyway (it is at the model + level), and that verifying that it has been fixed runs satisfyingly + faster. + +#. Modify the provided code to *remove* the scaling we applied to the + deadline generator, and test ``prop_FinishFast`` repeatedly to + judge the effect on the test case distribution. Reinsert the bug in + ``walletStrategy``, and use + + .. code-block:: text + + quickCheck . withMaxSuccess 10000 $ prop_NoLockedFundsFast + + to find it. Run this repeatedly, and make an estimate of the number + of tests needed to find the bug. Reinsert the scaling, and repeat + your estimate. Hopefully this will help persuade you of the value + of tuning your test case distributions! + + +Measuring coverage of on-chain code +----------------------------------- + +It is always good practice to measure the source-code coverage of +tests. Coverage information provides a sanity check that nothing has +been missed altogether: while covering a line of code is no guarantee +that a bug in that line will be revealed, *failing to cover* a line of +code *does* guarantee that any bug there will *not* be found. For +critical code, it is reasonable to aim for 100% coverage. + +Coverage of Haskell code can be measured using the `Haskell Program +Coverage `_ +toolkit; we will not discuss this further here. But while this works +well for measuring the coverage of *off-chain* code, it does not apply +to *on-chain* code, because this is compiled using the Plutus compiler +and executed on the blockchain, rather than by GHC. If we want to +measure the coverage of *on-chain* code--which is the most critical +code in a Plutus contract--then we need to use a separate tool. This +is what we cover in this section. + +Adding a coverage index +^^^^^^^^^^^^^^^^^^^^^^^ + +In order to generate a coverage report, the framework needs to know + +#. what was covered by tests, + +#. what should have been covered. + +Indeed, the most important part of a coverage report is often the +parts that were *not* covered by tests. This latter information--what +should be covered--is represented by a ``CoverageIndex`` that the +Plutus compiler constructs. Since the Plutus compiler is invoked using +Template Haskell in the code of the contract itself, then this is +where we have to save, and export, the coverage index. That is, we +must make additions to the code of a contract in order to enable +coverage measurement. + +To do so, we first inspect the code, and find all the +occurrences of ``PlutusTx.compile``. In the case of the escrow +contract, they are in the definition of ``typedValidator``: + + .. literalinclude:: EscrowImpl.hs + :start-after: START typedValidator + :end-before: END typedValidator + +The on-chain code consists of ``validate`` and ``wrap``. The latter is +a library function, whose coverage we do not need to measure, so we +just add (and export) a definition of a ``CoverageIndex`` that covers +``validate``: + + .. literalinclude:: EscrowImpl.hs + :start-after: START covIdx + :end-before: END covIdx + +It just remains to *import* the necessary types and functions + + .. literalinclude:: EscrowImpl.hs + :start-after: START imports + :end-before: END imports + +and to supply GHC options that cause the Plutus compiler to generate +coverage information: + +.. literalinclude:: EscrowImpl.hs + :start-after: START OPTIONS_GHC + :end-before: END OPTIONS_GHC + +With these additions, the contract implementation is ready for +coverage measurement. + +Measuring coverage +^^^^^^^^^^^^^^^^^^ + +Once we have created a suitable ``CoverageIndex``, we must create a +test that uses it. To do so, we need to + +#. Run the test using ``quickCheckWithCoverage``, and give it coverage options specifying the coverage index, + +#. Pass the (modified) coverage options that ``quickCheckWithCoverage`` constructs in to ``propRunActionsWithOptions`` (instead of ``propRunActions_``) when we run the action sequence, and + +#. (Ideally) visualize the resulting ``CoverageReport`` as annotated source code. + +Here is the code to do all this (we also need to import ``Plutus.Contract.Test.Coverage``): + + .. literalinclude:: Escrow5.hs + :start-after: START check_propEscrowWithCoverage + :end-before: END check_propEscrowWithCoverage + +First we call ``quickCheckWithCoverage`` with options containing +``covIdx``; it passes modified options to the rest of the property. We +test the property 1000 times, so that we are very likely to cover all +the reachable code in the tests. We cannot just reuse ``prop_Escrow``, +because we must pass in the modified coverage options ``covopts`` when +we run the actions, but otherwise this is just the same as +``prop_Escrow``. The result returned by ``quickCheckWithCoverage`` is +a ``CoverageReport``, which is difficult to interpret by itself, so we +bind it to ``cr`` and then generate an HTML file ``Escrow.html`` using +``writeCoverageReport``. + +Running this does take a little while, because we run a large number of +tests; on the other hand, diagnosing *why* a part of the code has not +been covered can be very time-consuming, and is wasted effort if the +reason is simply that we were unlucky when we ran the tests. It is +worth waiting a few minutes for more accurate coverage data, before +starting this kind of diagnosis. + +Quite a lot of output is generated, including lists of coverage items +that were covered respectively not covered. We shall ignore these for +now; the same information is presented much more readably in the +generated HTML file. But note that we do see statistics on endpoint +invocations: + + .. code-block:: text + + > check_propEscrowWithCoverage + +++ OK, passed 1000 tests: + 63.1% Contract instance for W[4] at endpoint pay-escrow + 62.5% Contract instance for W[1] at endpoint pay-escrow + 62.5% Contract instance for W[2] at endpoint pay-escrow + 61.2% Contract instance for W[3] at endpoint pay-escrow + 60.8% Contract instance for W[5] at endpoint pay-escrow + 29.1% Contract instance for W[5] at endpoint redeem-escrow + 28.2% Contract instance for W[1] at endpoint redeem-escrow + 27.4% Contract instance for W[3] at endpoint redeem-escrow + 25.8% Contract instance for W[2] at endpoint redeem-escrow + 25.6% Contract instance for W[4] at endpoint redeem-escrow + 4.5% Contract instance for W[1] at endpoint refund-escrow + 4.1% Contract instance for W[2] at endpoint refund-escrow + 3.9% Contract instance for W[4] at endpoint refund-escrow + 3.5% Contract instance for W[3] at endpoint refund-escrow + 3.3% Contract instance for W[5] at endpoint refund-escrow + + ... + +This table tells us what percentage of test cases made a call to each +endpoint from the given wallet; for example, 63.1% of test cases made +(somewhere) a call to the ``pay-escrow`` endpoint from wallet 4. As we +can see, the ``pay-escrow`` endpoint is called in most tests from each +wallet, ``redeem-escrow`` is a bit rarer, and ``refund-escrow`` is +used quite rarely. Most serious, of course, would be if one of the +endpoints doesn't appear in this table at all. + +It is possible to supply coverage goals for each wallet/endpoint +combination via an additional coverage option. We don't consider this +further here, except to note that by default the framework expects +each combination to appear in 20% of tests, and so we get warnings in +this case: + + .. code-block:: text + + Only 4.5% Contract instance for W[1] at endpoint refund-escrow, but expected 20.0% + Only 4.1% Contract instance for W[2] at endpoint refund-escrow, but expected 20.0% + Only 3.5% Contract instance for W[3] at endpoint refund-escrow, but expected 20.0% + Only 3.9% Contract instance for W[4] at endpoint refund-escrow, but expected 20.0% + Only 3.3% Contract instance for W[5] at endpoint refund-escrow, but expected 20.0% + +These warnings can be eliminated by specifying more appropriate (lower) +coverage goals for these endpoint calls. + +Interpreting the coverage annotations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Running the test above writes annotated source code to +``Escrow.html``. The entire contents of the file are reproduced +:ref:`here`. The report contains all of the +on-chain code provided in the ``CoverageIndex``, together with a few +lines of code around it for context. Off-chain code appears in grey, +so it can be distinguished. On-chain code on a white background was +covered by tests, and we know no more about it. Code on a red or green +background was also covered, but it is a boolean expression, and only +took one value (red for ``False``, green for ``True``). Orange code on +a black background is on-chain code that was not covered at all--and +thus may represent a gap in testing. + +Looking at the last section of code in the report, + + .. raw:: html + +
+      365    covIdx :: CoverageIndex
+      366    covIdx = getCovIdx $$(PlutusTx.compile [|| val
+   
+ +we see that it is the construction of the coverage index, and +parts of this code are labelled on-chain and uncovered. We can ignore +this, it's simply an artefact of the way the code labelling is done +(and could be avoided by putting the construction of the ``covIdx`` in a +different module, without coverage enabled). + +More interesting is the second section of the report: + + .. raw:: html + +
+      201    {-# INLINABLE validate #-}
+      202    validate :: EscrowParams DatumHash -> PaymentPubKeyHash -> Action -> ScriptContext -> Bool
+      203    validate EscrowParams{escrowDeadline, escrowTargets} contributor action ScriptContext{scriptContextTxInfo} =
+      204        case action of
+      205            Redeem ->
+      206                traceIfFalse "escrowDeadline-after" (escrowDeadline `after` txInfoValidRange scriptContextTxInfo)
+      207                && traceIfFalse "meetsTarget" (all (meetsTarget scriptContextTxInfo) escrowTargets)
+      208            Refund ->
+      209                traceIfFalse "escrowDeadline-before" ((escrowDeadline - 1) `before` txInfoValidRange scriptContextTxInfo)
+      210                && traceIfFalse "txSignedBy" (scriptContextTxInfo `txSignedBy` unPaymentPubKeyHash contributor)
+      211    
+   
+ +This is the main validator, and while some of its code is coloured +white, much of it is coloured green. This means the checks in this +function always returned ``True`` in our tests; we have not tested the +cases in which they return ``False``. + +This does indicate a weakness in our testing: since these checks +always passed in our tests, then those tests would *also* have passed +if the checks were removed completely (replaced by ``True``)--but the +contract would have been quite wrong. We will return to this point +later, when we discuss *negative testing*. For now, though, we just +note that *if the checks had returned* ``False``, *then the +transaction would have failed*--and the off-chain code is, of course, +designed not to submit failing transactions. So, in a sense, we should +expect this code to be coloured green--at least, when we test through +well-designed off-chain endpoints, as we have been doing. + +This code fragment also contains some entirely uncovered code--the +strings passed to ``traceIfFalse`` to be used as error messages if a +check fails. Since correct off-chain code never submits failing +transactions, then these error messages are never used--and hence the +code is labelled as 'uncovered'. Again, this is not really a problem. + +The most interesting part of the report is the first section: + + .. raw:: html + +
+      190    meetsTarget :: TxInfo -> EscrowTarget DatumHash -> Bool
+      191    meetsTarget ptx = \case
+      192        PaymentPubKeyTarget pkh vl ->
+      193            valuePaidTo ptx (unPaymentPubKeyHash pkh) `geq` vl
+      194        ScriptTarget validatorHash dataValue vl ->
+      195            case scriptOutputsAt validatorHash ptx of
+      196                [(dataValue', vl')] ->
+      197                    traceIfFalse "dataValue" (dataValue' == dataValue)
+      198                    && traceIfFalse "value" (vl' `geq` vl)
+      199                _ -> False
+   
+ +This is the function that is used to check that each target payment is +made when the escrow is redeemed, and as we see from the coverage +report, there are two cases, of which only one has been tested. In +fact the two cases handle payments to a wallet, and payments to a +script, and the second kind of payment is *not tested at all* by our +tests--yet it is handled by entirely different code in the on-chain +function. + +**This exposes a serious deficiency in the tests developed so far**: +they give us no evidence at all that target payments to a script work +as intended. To test this code as well, we would need to add 'proxy' +contracts to the tests, to act as recipients for such payments. We +leave making this extension as an exercise for the reader. + +.. _CoverageReport: + +The generated coverage report +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This is the generated coverage report in its entirety: + + .. raw:: html + :file: Escrow.html + +Crashes, and how to tolerate them +--------------------------------- + +One awkward possibility, that we cannot avoid, is that a contract +instance might crash during execution--for example, because of a power +failure to the machine it is running on. We don't want anything to be +lost permanently as a result--it should be possible to recover by +restarting the contract instance, perhaps in a different state, and +continue. Yet how should we test this? We need to deliberately crash +and restart contracts in tests, and check that they still behave as +the model says they should. + +The ``ContractModel`` framework provides a simple way to *extend* a +contract model, so that it can test crash-tolerance too. If ``m`` is a +``ContractModel`` instance, then so is ``WithCrashTolerance m``--and +testing the latter model will insert actions to crash and restart +contract instances at random. To define a property that runs these +tests, all we have to do is include ``WithCrashTolerance`` in the type +signature: + + .. literalinclude:: Escrow6.hs + :start-after: START prop_CrashTolerance + :end-before: END prop_CrashTolerance + +(The actual code here is the same as ``prop_Escrow``, only the type is different). + +We do have to provide a little more information before we can run +tests, though. + +#. Firstly, we cannot expect to include an action in a + test, when the contract(s) that should perform the action are not + running. We thus need to tell the framework whether or not an action + is *available*, given the contracts currently running. + +#. Secondly, when we restart a contract it may need to take some + recovery action, and so we must be able to give it the necessary + information to recover. We achieve this by specifying + possibly-different contract parameters to use, when a contract is + restarted. These parameters may depend on the model state. + +We provide this information by defining an instance of the +``CrashTolerance`` class: + + .. literalinclude:: Escrow6.hs + :start-after: START CrashTolerance + :end-before: END CrashTolerance + +The ``available`` method returns ``True`` if an action is available, +given a list of active contract keys ``alive``; since contract +instance keys have varying types, then the list actually contains keys +wrapped in an existential type, which is why the ``Key`` constructor +appears there. + +The ``restartArguments`` method provides the parameter for restarting +an escrow contract, which in this case can be just the same as when +the contract was first started. We need to recover the targets from +the model state, in which they are represented as a ``Map Wallet +Value``, so we convert them back to a list and refactor the +``escrowParams`` function so we can give ``escrowParams'`` a list of +``(Wallet,Value)`` pairs, rather than a list of ``(Wallet,Int)``: + + .. literalinclude:: Escrow6.hs + :start-after: START escrowParams + :end-before: END escrowParams + +It is possible to define the effect of crashing or restarting a +contract instance on the *model* too, if need be, by defining +additional methods in this class. In this case, though, crashing and +restarting ought to be entirely transparent, so we can omit them. + +Surprisingly, the tests do not pass! + + .. code-block:: text + + > quickCheck prop_CrashTolerance + *** Failed! Assertion failed (after 24 tests and 26 shrinks): + Actions + [Init (Slot {getSlot = 6}) [(Wallet 1,2),(Wallet 4,2)], + Crash (WalletKey (Wallet 4)), + Restart (WalletKey (Wallet 4)), + Pay (Wallet 4) 4, + Redeem (Wallet 1)] + Expected funds of W[4] to change by + Value (Map [(,Map [("",-2000000)])]) + but they changed by + Value (Map [(,Map [("",-4000000)])]) + a discrepancy of + Value (Map [(,Map [("",-2000000)])]) + Expected funds of W[1] to change by + Value (Map [(,Map [("",2000000)])]) + but they did not change + Contract instance log failed to validate: + ... + Slot 5: 00000000-0000-4000-8000-000000000000 {Wallet W[1]}: + Contract instance stopped with error: RedeemFailed NotEnoughFundsAtAddress + ... + +Here we simply set up targets with two beneficiaries, crash and +restart wallet 4, pay sufficient contributions to cover the targets, +and then try to redeem the escrow, which seems straightforward enough, +and yet the redemption thinks there are not enough funds in the +escrow, *even though we just paid them in*! + +This failure is a little tricky to debug. A clue is that the *payment* +was made by a contract instance that has been restarted, while the +*redemption* was made by a contract that has not. Do the payment and +redemption actually refer to the same escrow? In fact the targets +supplied to the contract instance are not necessarily exactly the +same: the contract receives a *list* of targets, but in the model we +represented them as a *map*--and converted the list of targets to a +map, and back again, when we restarted the contract. That means the +*order* of the targets might be different. + +Could that make a difference? To find out, we can just *sort* the +targets before passing them to the contract instance, thus +guaranteeing the same order every time: + + .. literalinclude:: Escrow6.hs + :start-after: START betterEscrowParams + :end-before: END betterEscrowParams + +Once we do this, the tests pass. We can also see from the resulting +statistics that quite a lot of crashing and restarting is going on: + + .. code-block:: text + + > quickCheck prop_CrashTolerance + +++ OK, passed 100 tests. + + Actions (2721 in total): + 42.48% Pay + 24.99% WaitUntil + 13.08% Crash + 9.52% Restart + 6.06% Redeem + 3.01% Init + 0.85% Refund + +Perhaps it's debatable whether or not the behaviour we uncovered here +is a *bug*, but it is certainly a feature--it was not obvious in +advance that specifying the same targets in a different order would +create an independent escrow, but that is what happens. So for +example, if a buyer and seller using an escrow contract to exchange an +NFT for Ada specify the two targets in different orders, then they +would place their assets in independent escrow that cannot be redeemed +until the refund deadline passes. Arguably a better designed contract +would sort the targets by wallet, as we have done here, before +creating any UTXOs, so that the problem cannot arise. + +Exercises +^^^^^^^^^ + +You will find the code discussed here in ``Spec.Tutorial.Escrow6``, *without* the addition of ``sortBy`` to ``escrowParams``. + +#. Run ``quickCheck prop_CrashTolerance`` to provoke a test + failure. Examine the counterexample and the test output, and make + sure you understand how the test fails. Run this test several + times: you will see the failure in several different forms, with + the same underlying cause. Make sure you understand how each + failure arises. + + Why does ``quickCheck`` always report a test case with *two* target + payments--why isn't one target enough to reveal the problem? + +#. Add sorting to the model, and verify that the tests now pass. + +#. An alternative way to fix the model is *not* to convert the targets + to a ``Map`` in the model state, but just keep them as a list of + pairs, so that exactly the same list can be supplied to the + contract instances when they are restarted. Implement this change, + and verify that the tests still pass. + + Which solution do you prefer? Arguably this one reflects the + *actual* design of the contract more closely, since the model makes + explicit that the order of the targets matters. + + +Debugging the Auction contract with model assertions +---------------------------------------------------- + +In this section, we'll apply the techniques we have seen so far to +test another contract, and we'll see how they reveal some surprising +behaviour. The contract we take this time is the auction contract in +``Plutus.Contracts.Auction``. This module actually defines *two* +contracts, a seller contract and a buyer contract. The seller puts up +a ``Value`` for sale, creating an auction UTXO containing the value, +and buyers can then bid Ada for it. When the auction deadline is +reached, the highest bidder receives the auctioned value, and the +seller receives the bid. + +Modelling the Auction contract +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``Spec.Auction`` contains a contract model for testing this +contract. The value for sale is a custom token, wallet 1 is the +seller, and the deadline used for testing is fixed at slot 101; the +generated tests just consist of an ``Init`` action to start the +auction, and a series of ``Bid`` actions by the other wallets. + + .. literalinclude:: Auction.hs + :start-after: START Action + :end-before: END Action + +The model keeps track of the highest bid and bidder, and the current +phase the auction is in: + + .. literalinclude:: Auction.hs + :start-after: START model + :end-before: END model + +It is updated by the ``nextState`` method on each bid: + + .. literalinclude:: Auction.hs + :start-after: START nextState + :end-before: END nextState + +Note that when a higher bid is received, the previous bid is returned +to the bidder. + +We only allow bids that are larger than the previous one (which is why +``nextState`` doesn't need to check this): + + .. literalinclude:: Auction.hs + :start-after: START precondition + :end-before: END precondition + +The most interesting part of the model covers what happens when the +auction deadline is reached: in contrast to the ``Escrow`` contract, +the highest bid is paid to the seller automatically, and the buyer +receives the token. We model this using the ``nextReactiveState`` +method introduced in section :ref:`Timing` + + .. literalinclude:: Auction.hs + :start-after: START nextReactiveState + :end-before: END nextReactiveState + +Finally we can define the property to test; in this case we have to +supply some options to initialize wallet 1 with the token to be +auctioned: + + .. literalinclude:: Auction.hs + :start-after: START prop_Auction + :end-before: END prop_Auction + +The only important part here is ``options``, which is defined as follows: + + .. literalinclude:: Auction.hs + :start-after: START options + :end-before: END options + +Unsurprisingly, the tests pass. + + .. code-block:: text + + > quickCheck prop_Auction + +++ OK, passed 100 tests. + + Actions (2348 in total): + 85.82% Bid + 10.35% WaitUntil + 3.83% Init + +No locked funds? +^^^^^^^^^^^^^^^^ + +Now we have a basic working model of the auction contract, we can +begin to test more subtle properties. To begin with, can we recover +the funds held by the contract? The strategy to try is obvious: all we +have to do is wait for the deadline to pass. So ``prop_FinishAuction`` +is very simple: + + .. literalinclude:: Auction.hs + :start-after: START prop_FinishAuction + :end-before: END prop_FinishAuction + +This property passes too: + + .. code-block:: text + + > quickCheck prop_FinishAuction + +++ OK, passed 100 tests. + + Actions (3152 in total): + 84.77% Bid + 12.25% WaitUntil + 2.98% Init + +Now, to supply a ``NoLockedFundsProof`` we need a general strategy for +fund recovery, and a wallet-specific one. Since all we have to do is +wait, we can use the *same* strategy as both. + + .. literalinclude:: Auction.hs + :start-after: START noLockProof + :end-before: END noLockProof + +Surprisingly, *these tests fail*! + + .. code-block:: text + + > quickCheck prop_NoLockedFunds + *** Failed! Assertion failed (after 2 tests and 1 shrink): + DLScript + [Do $ Init, + Do $ Bid (Wallet 3) 2000000] + + The ContractModel's Unilateral behaviour for Wallet 3 does not match the + actual behaviour for actions: + Actions + [Var 0 := Init, + Var 1 := Bid (Wallet 3) 2000000, + Var 2 := Unilateral (Wallet 3), + Var 3 := WaitUntil (Slot {getSlot = 101})] + Expected funds of W[1] to change by + Value (Map [(363d...,Map [("token",-1)])]) + but they changed by + Value (Map [(,Map [("",-2000000)]),(363d...,Map [("token",-1)])]) + a discrepancy of + Value (Map [(,Map [("",-2000000)])]) + Expected funds of W[3] to change by + Value (Map [(363d...,Map [("token",1)])]) + but they changed by + Value (Map [(,Map [("",-2000000)])]) + a discrepancy of + Value (Map [(,Map [("",-2000000)]),(363d...,Map [("token",-1)])]) + Test failed. + +This test just started the auction and submitted a bid from wallet 3, +then *stopped all the other wallets* (this is what ``Unilateral +(Wallet 3)`` does), before waiting until the auction deadline. This +resulted in a different distribution of funds from the one the model +predicts. Looking at the last part of the message, we see that we +expected wallet 3 to get the token, *but it did not*; neither did it +get its bid back. Wallet 1 did lose the token, though, and in addition +lost the 2 Ada required to create the auction UTXO in the first place. + +What is going on? The strategy worked in the general case, but failed +in the unilateral case, which tells us that *the buyer requires the +cooperation of the seller* in order to recover the auctioned +token. Why? Well, our description of the contract above was a little +misleading: the proceeds of the auction *cannot* be paid out +automatically just because the deadline passes; the Cardano blockchain +won't do that. Instead, *someone must submit the payout +transaction*. In the case of this contract, it's the seller: even +though there is no *endpoint call* at the deadline, the seller's +off-chain code continues running throughout the auction, and when the +deadline comes it submits the payout transaction. So if the seller's +contract is stopped, then no payout occurs. + +This is not a *very* serious bug, because the *on-chain* code allows +anyone to submit the payout transaction, so the buyer could in +principle do so. However, the existing off-chain code does not provide +an endpoint for this, and so recovering the locked funds would require +writing a new version of the off-chain code (or rolling a suitable +transaction by hand). + + .. _AuctionAssertion: + +Model assertions, and unexpected expectations. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Looking back at the failed test again, the *expected* wallet contents +are actually a little *unexpected*: + + .. code-block:: text + + Actions + [Var 0 := Init, + Var 1 := Bid (Wallet 3) 2000000, + Var 2 := Unilateral (Wallet 3), + Var 3 := WaitUntil (Slot {getSlot = 101})] + Expected funds of W[1] to change by + Value (Map [(363d...,Map [("token",-1)])]) + but they changed by + Value (Map [(,Map [("",-2000000)]),(363d...,Map [("token",-1)])]) + a discrepancy of + Value (Map [(,Map [("",-2000000)])]) + +Notice that, even though wallet 3 made a bid of 2 Ada, we *expected* +the seller to end up without the token, but *with no extra +money*. Wouldn't we expect the seller to end up with 2 Ada? + +Because ``prop_Auction`` passed, then we know that in the absence of a +``Unilateral`` then the model and the contract implementation agree on +fund transfers. But does the model actually predict that the seller +gets the winning bid? This can be a little hard to infer from the +state transitions themselves; we can check that each action appears to +do the right thing, but whether the end result is as expected is not +necessarily immediately obvious. + +We can address this kind of problem by *adding assertions to the +model*. The model tracks the change in each wallet's balance since the +beginning of the test, so we can add an assertion, at the point where +the auction ends, that checks that the seller loses the token and +gains the winning bid. We just a little code to ``nextReactiveState``: + + .. literalinclude:: Auction.hs + :start-after: START extendedNextReactiveState + :end-before: END extendedNextReactiveState + +If the boolean passed to ``assertSpec`` is ``False``, then the test +fails with the first argument in the error message. + + .. note:: + + We do have to allow for the possibility that the auction never + started, which is why we include in the assertion the possibility + that wallet 1's balance remains unchanged. Without this, the tests + fail. + +Now ``prop_Auction`` fails! + + .. code-block:: text + + > quickCheck prop_Auction + *** Failed! Falsified (after 27 tests and 24 shrinks): + Actions + [Init, + Bid (Wallet 3) 2000000, + WaitUntil (Slot {getSlot = 100})] + assertSpec failed: w1 final balance is wrong: + SymValue {symValMap = fromList [], actualValPart = Value (Map [(363d...,Map [("token",-1)])])} + + .. note:: + + The balance change is actually a ``SymValue``, not a ``Value``, + but as you can see it *contains* a ``Value``, which is all we care + about right now. We will return to the purpose of the + ``symValMap`` in a later section. + +Even in this simple case, the seller does not receive the right +amount: wallet 1 lost the token, but received no payment! + +The reason has to do with the minimum Ada in each UTXO. When the +auction UTXO is created, the seller has to put in 2 Ada along with the +token. When the auction ends, one might expect that 2 Ada to be +returned to the seller. But it can't be: *it is needed to create the +UTXO that delivers the token to the buyer*! Thus the seller receives 2 +Ada (from the buyer's bid) in this example, but this only makes up for +the 2 Ada deposited in the auction UTXO, and the seller ends up giving +away the token for nothing. + +This is quite surprising behaviour, and arguably, the contract should +require that the buyer pay the seller 2 Ada *plus* the winning bid, so that +the stated bid is equal to the seller's net receipts. + + .. note:: + + Model assertions can be tested without running the emulator, by + using ``propSanityCheckAssertions`` instead of + ``propRunActions_``. This is very much faster, and enables very + thorough testing of the model. Since other tests check that the + implementation correponds to the model, then this still gives us + valuable information about the implementation. + +Crashing the auction +^^^^^^^^^^^^^^^^^^^^ + +Is the auction crash tolerant? To find out, we just declare that +``Bid`` actions are available when the corresponding buyer contract is +running, define the restart arguments, and the crash-tolerant property +(which just replicates the definition of ``prop_Auction`` with a +different type). + + .. literalinclude:: Auction.hs + :start-after: START crashTolerance + :end-before: END crashTolerance + +Perhaps unsurprisingly, this property fails: + + .. code-block:: text + + > quickCheck prop_CrashTolerance + *** Failed! Assertion failed (after 17 tests and 11 shrinks): + Actions + [Init, + Crash SellerH, + WaitUntil (Slot {getSlot = 100})] + Expected funds of W[1] to change by + Value (Map []) + but they changed by + Value (Map [(,Map [("",-2000000)]),(363d3944282b3d16b239235a112c0f6e2f1195de5067f61c0dfc0f5f,Map [("token",-1)])]) + a discrepancy of + Value (Map [(,Map [("",-2000000)]),(363d3944282b3d16b239235a112c0f6e2f1195de5067f61c0dfc0f5f,Map [("token",-1)])]) + Test failed. + +We already know that the auction payout is initiated by the seller +contract, so if that contract is not running, then no payout takes +place. (Although there are no bids in this counterexample, a payout is +still needed--to return the token to the seller). That is why this test fails. + +But this is actually not the only way the property can fail. The other +failure (which generates some rather long contract logs) looks like +this: + + .. code-block:: text + + > quickCheck prop_CrashTolerance + *** Failed! Assertion failed (after 13 tests and 9 shrinks): + Actions + [Init, + Crash SellerH, + Restart SellerH] + Contract instance log failed to validate: + ... half a megabyte of output ... + Slot 6: 00000000-0000-4000-8000-000000000004 {Wallet W[1]}: + Contract instance stopped with error: StateMachineContractError (SMCContractError (WalletContractError (InsufficientFunds "Total: Value (Map [(,Map [(\"\",9999999997645750)])]) expected: Value (Map [(363d3944282b3d16b239235a112c0f6e2f1195de5067f61c0dfc0f5f,Map [(\"token\",1)])])"))) + Test failed. + +In other words, after a crash, *the seller contract fails to +restart*. This is simply because the seller tries to put the token up +for auction when it starts, and *wallet 1 no longer holds the +token*--it is already in an auction UTXO on the blockchain. So the +seller contract fails with an ``InsufficientFunds`` error. To continue +the auction, we would really need another endpoint to resume the +seller, which the contract does not provide, or a parameter to the +seller contract which specifies whether to start or continue an +auction. + +Coverage of the Auction contract +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We can generate a coverage report for the ``Auction`` contract just as +we did for the ``Escrow`` one. The interesting part of the report is: + + .. raw:: html + :file: Auction.html + +The auction is defined as a Plutus state machine, which just repeats +an ``auctionTransition`` over and over again. We can see that the +state machine itself, and most of the transition code, is +covered. However, the ``Bid`` transition has only been tested in the +case where new bid is higher than the old one. Indeed, the tests are +designed to respect that precondition. Moroever, the last clause in +the ``case`` expression has not been tested at all--but this is quite +OK, because it returns ``Nothing`` which the state machine library +interprets to mean "reject the transaction". So the uncovered code +*could* only be covered by failing transactions, which the off-chain +code is designed not to submit. + +Exercises +^^^^^^^^^ + +The code discussed here is in ``Spec.Auction``. + +#. Test the failing properties (``prop_NoLockedFunds`` and + ``prop_CrashTolerance``) and observe the failures. + +#. Add the model assertion discussed in :ref:`AuctionAssertion` to the + code, and ``quickCheck prop_SanityCheckAssertions`` to verify that + it fails. Change the assertion to say that the seller receives 2 + Ada *less* than the bid, and verify that it now passes. + +Becoming Level 1 Certification Ready +------------------------------------ + +Level 1 certification of plutus smart contracts relies on the machinery +we have discussed in this tutorial. First things first we are going to +have a look at the :hsmod:`Plutus.Contract.Test.Certification` module. + +This module defines a type ``Certification`` paramtereized over a type +``m`` that should be a ``ContractModel``. This is a record type that has +fields for: + +#. a ``CoverageIndex``, +#. two different types of ``NoLockedFundsProof`` + (a standard full proof and a light proof that does not require you to provide + a per-wallet unilateral strategy), +#. the ability to provide a specialized error whitelist, +#. a way to specify that we have an instance of ``CrashTolerance`` for ``m``, +#. unit tests in the form of a function from a ``CoverageRef`` to a ``TestTree`` + (see :hsobj:`Plutus.Contract.Test.checkPredicateCoverage` + for how to construct one of these), and +#. named dynamic logic unit tests. + +Fortunately, understanding what we need to do to get certification-ready +at this stage is simple. We just need to build a ``Certification`` object. +For example of how to do this, check out ``Spec.GameStateMachine.certification`` +and ``Spec.Uniswap.certification``. + +You can run level 1 certification locally using the +:hsobj:`Plutus.Contract.Test.Certification.Run.certify` function - but at +this stage you may find it difficult to read the output of this function. +Don't worry! A certification dashboard is on the way! + +Exercises +^^^^^^^^^ + +#. Build a certification object for the ``Auction`` and ``Escrow`` contracts. diff --git a/doc/plutus/tutorials/index.rst b/doc/plutus/tutorials/index.rst index 62f0962562..6c8d171460 100644 --- a/doc/plutus/tutorials/index.rst +++ b/doc/plutus/tutorials/index.rst @@ -13,3 +13,5 @@ Tutorials basic-validators basic-minting-policies contract-testing + contract-models + diff --git a/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-contract.nix b/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-contract.nix index 640543c40a..88af4b9c6f 100644 --- a/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-contract.nix +++ b/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-contract.nix @@ -57,6 +57,7 @@ (hsPkgs."freer-simple" or (errorHandler.buildDepError "freer-simple")) (hsPkgs."hashable" or (errorHandler.buildDepError "hashable")) (hsPkgs."hedgehog" or (errorHandler.buildDepError "hedgehog")) + (hsPkgs."html-entities" or (errorHandler.buildDepError "html-entities")) (hsPkgs."lens" or (errorHandler.buildDepError "lens")) (hsPkgs."memory" or (errorHandler.buildDepError "memory")) (hsPkgs."mmorph" or (errorHandler.buildDepError "mmorph")) @@ -151,6 +152,7 @@ ] ++ (pkgs.lib).optionals (!(compiler.isGhcjs && true || system.isGhcjs || system.isWindows)) [ "Plutus/Contract/Test" "Plutus/Contract/Test/Coverage" + "Plutus/Contract/Test/Coverage/ReportCoverage" "Plutus/Contract/Test/ContractModel" "Plutus/Contract/Test/ContractModel/Internal" "Plutus/Contract/Test/ContractModel/Symbolics" diff --git a/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-doc.nix b/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-doc.nix index f889dababe..f6b7f83368 100644 --- a/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-doc.nix +++ b/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-doc.nix @@ -57,6 +57,8 @@ (hsPkgs."random" or (errorHandler.buildDepError "random")) (hsPkgs."text" or (errorHandler.buildDepError "text")) (hsPkgs."aeson" or (errorHandler.buildDepError "aeson")) + (hsPkgs."tasty" or (errorHandler.buildDepError "tasty")) + (hsPkgs."tasty-quickcheck" or (errorHandler.buildDepError "tasty-quickcheck")) ] ++ (pkgs.lib).optional (!(compiler.isGhcjs && true || system.isGhcjs)) (hsPkgs."plutus-tx-plugin" or (errorHandler.buildDepError "plutus-tx-plugin")); build-tools = [ (hsPkgs.buildPackages.doctest.components.exes.doctest or (pkgs.buildPackages.doctest or (errorHandler.buildToolDepError "doctest:doctest"))) @@ -73,6 +75,13 @@ "HandlingBlockchainEvents" "HelloWorldApp" "WriteScriptsTo" + "Escrow" + "Escrow2" + "Escrow3" + "Escrow4" + "Escrow5" + "Escrow6" + "EscrowImpl" ]; hsSourceDirs = [ "plutus/tutorials" "plutus/howtos" ]; mainPath = (([ diff --git a/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-use-cases.nix b/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-use-cases.nix index b5cd38a6bc..f80d86479e 100644 --- a/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-use-cases.nix +++ b/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-use-cases.nix @@ -66,6 +66,8 @@ "Plutus/Contracts/ErrorHandling" "Plutus/Contracts/Escrow" "Plutus/Contracts/SimpleEscrow" + "Plutus/Contracts/Tutorial/Escrow" + "Plutus/Contracts/Tutorial/EscrowStrict" "Plutus/Contracts/Future" "Plutus/Contracts/Game" "Plutus/Contracts/GameStateMachine" @@ -159,8 +161,10 @@ (hsPkgs."plutus-contract" or (errorHandler.buildDepError "plutus-contract")) (hsPkgs."plutus-contract-certification" or (errorHandler.buildDepError "plutus-contract-certification")) (hsPkgs."plutus-ledger" or (errorHandler.buildDepError "plutus-ledger")) + (hsPkgs."plutus-ledger-api" or (errorHandler.buildDepError "plutus-ledger-api")) (hsPkgs."plutus-ledger-constraints" or (errorHandler.buildDepError "plutus-ledger-constraints")) (hsPkgs."plutus-use-cases" or (errorHandler.buildDepError "plutus-use-cases")) + (hsPkgs."playground-common" or (errorHandler.buildDepError "playground-common")) (hsPkgs."base" or (errorHandler.buildDepError "base")) (hsPkgs."bytestring" or (errorHandler.buildDepError "bytestring")) (hsPkgs."cardano-crypto-class" or (errorHandler.buildDepError "cardano-crypto-class")) @@ -191,6 +195,13 @@ "Spec/Escrow" "Spec/Escrow/Endpoints" "Spec/SimpleEscrow" + "Spec/Tutorial/Escrow" + "Spec/Tutorial/Escrow1" + "Spec/Tutorial/Escrow2" + "Spec/Tutorial/Escrow3" + "Spec/Tutorial/Escrow4" + "Spec/Tutorial/Escrow5" + "Spec/Tutorial/Escrow6" "Spec/Future" "Spec/Game" "Spec/GameStateMachine" @@ -203,6 +214,7 @@ "Spec/Rollup" "Spec/Stablecoin" "Spec/Uniswap" + "Spec/Uniswap/Endpoints" "Spec/TokenAccount" "Spec/Vesting" ]; diff --git a/nix/pkgs/haskell/materialized-darwin/.plan.nix/quickcheck-dynamic.nix b/nix/pkgs/haskell/materialized-darwin/.plan.nix/quickcheck-dynamic.nix index 305cd0cb25..87158f9e77 100644 --- a/nix/pkgs/haskell/materialized-darwin/.plan.nix/quickcheck-dynamic.nix +++ b/nix/pkgs/haskell/materialized-darwin/.plan.nix/quickcheck-dynamic.nix @@ -44,6 +44,7 @@ "Test/QuickCheck/DynamicLogic/Monad" "Test/QuickCheck/DynamicLogic/Quantify" "Test/QuickCheck/DynamicLogic/SmartShrinking" + "Test/QuickCheck/DynamicLogic/Utils" "Test/QuickCheck/StateModel" ]; hsSourceDirs = [ "src" ]; diff --git a/nix/pkgs/haskell/materialized-darwin/default.nix b/nix/pkgs/haskell/materialized-darwin/default.nix index 721254ab68..e7fa31333d 100644 --- a/nix/pkgs/haskell/materialized-darwin/default.nix +++ b/nix/pkgs/haskell/materialized-darwin/default.nix @@ -380,6 +380,7 @@ "blockfrost-api".revision = (((hackage."blockfrost-api")."0.3.1.0").revisions).default; "blockfrost-api".flags.production = false; "blockfrost-api".flags.buildfast = true; + "html-entities".revision = (((hackage."html-entities")."1.1.4.5").revisions).default; "cryptohash-sha1".revision = (((hackage."cryptohash-sha1")."0.11.101.0").revisions).default; "warp-tls".revision = (((hackage."warp-tls")."3.3.2").revisions).default; "digest".revision = (((hackage."digest")."0.0.1.3").revisions).default; @@ -1692,6 +1693,7 @@ "servant-websockets".components.library.planned = lib.mkOverride 900 true; "ouroboros-network-framework".components.exes."demo-connection-manager".planned = lib.mkOverride 900 true; "sqlite-simple".components.library.planned = lib.mkOverride 900 true; + "html-entities".components.library.planned = lib.mkOverride 900 true; "constraints-extras".components.library.planned = lib.mkOverride 900 true; "monad-logger".components.library.planned = lib.mkOverride 900 true; "OneTuple".components.library.planned = lib.mkOverride 900 true; diff --git a/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-contract.nix b/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-contract.nix index 640543c40a..88af4b9c6f 100644 --- a/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-contract.nix +++ b/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-contract.nix @@ -57,6 +57,7 @@ (hsPkgs."freer-simple" or (errorHandler.buildDepError "freer-simple")) (hsPkgs."hashable" or (errorHandler.buildDepError "hashable")) (hsPkgs."hedgehog" or (errorHandler.buildDepError "hedgehog")) + (hsPkgs."html-entities" or (errorHandler.buildDepError "html-entities")) (hsPkgs."lens" or (errorHandler.buildDepError "lens")) (hsPkgs."memory" or (errorHandler.buildDepError "memory")) (hsPkgs."mmorph" or (errorHandler.buildDepError "mmorph")) @@ -151,6 +152,7 @@ ] ++ (pkgs.lib).optionals (!(compiler.isGhcjs && true || system.isGhcjs || system.isWindows)) [ "Plutus/Contract/Test" "Plutus/Contract/Test/Coverage" + "Plutus/Contract/Test/Coverage/ReportCoverage" "Plutus/Contract/Test/ContractModel" "Plutus/Contract/Test/ContractModel/Internal" "Plutus/Contract/Test/ContractModel/Symbolics" diff --git a/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-doc.nix b/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-doc.nix index f889dababe..f6b7f83368 100644 --- a/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-doc.nix +++ b/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-doc.nix @@ -57,6 +57,8 @@ (hsPkgs."random" or (errorHandler.buildDepError "random")) (hsPkgs."text" or (errorHandler.buildDepError "text")) (hsPkgs."aeson" or (errorHandler.buildDepError "aeson")) + (hsPkgs."tasty" or (errorHandler.buildDepError "tasty")) + (hsPkgs."tasty-quickcheck" or (errorHandler.buildDepError "tasty-quickcheck")) ] ++ (pkgs.lib).optional (!(compiler.isGhcjs && true || system.isGhcjs)) (hsPkgs."plutus-tx-plugin" or (errorHandler.buildDepError "plutus-tx-plugin")); build-tools = [ (hsPkgs.buildPackages.doctest.components.exes.doctest or (pkgs.buildPackages.doctest or (errorHandler.buildToolDepError "doctest:doctest"))) @@ -73,6 +75,13 @@ "HandlingBlockchainEvents" "HelloWorldApp" "WriteScriptsTo" + "Escrow" + "Escrow2" + "Escrow3" + "Escrow4" + "Escrow5" + "Escrow6" + "EscrowImpl" ]; hsSourceDirs = [ "plutus/tutorials" "plutus/howtos" ]; mainPath = (([ diff --git a/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-use-cases.nix b/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-use-cases.nix index b5cd38a6bc..f80d86479e 100644 --- a/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-use-cases.nix +++ b/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-use-cases.nix @@ -66,6 +66,8 @@ "Plutus/Contracts/ErrorHandling" "Plutus/Contracts/Escrow" "Plutus/Contracts/SimpleEscrow" + "Plutus/Contracts/Tutorial/Escrow" + "Plutus/Contracts/Tutorial/EscrowStrict" "Plutus/Contracts/Future" "Plutus/Contracts/Game" "Plutus/Contracts/GameStateMachine" @@ -159,8 +161,10 @@ (hsPkgs."plutus-contract" or (errorHandler.buildDepError "plutus-contract")) (hsPkgs."plutus-contract-certification" or (errorHandler.buildDepError "plutus-contract-certification")) (hsPkgs."plutus-ledger" or (errorHandler.buildDepError "plutus-ledger")) + (hsPkgs."plutus-ledger-api" or (errorHandler.buildDepError "plutus-ledger-api")) (hsPkgs."plutus-ledger-constraints" or (errorHandler.buildDepError "plutus-ledger-constraints")) (hsPkgs."plutus-use-cases" or (errorHandler.buildDepError "plutus-use-cases")) + (hsPkgs."playground-common" or (errorHandler.buildDepError "playground-common")) (hsPkgs."base" or (errorHandler.buildDepError "base")) (hsPkgs."bytestring" or (errorHandler.buildDepError "bytestring")) (hsPkgs."cardano-crypto-class" or (errorHandler.buildDepError "cardano-crypto-class")) @@ -191,6 +195,13 @@ "Spec/Escrow" "Spec/Escrow/Endpoints" "Spec/SimpleEscrow" + "Spec/Tutorial/Escrow" + "Spec/Tutorial/Escrow1" + "Spec/Tutorial/Escrow2" + "Spec/Tutorial/Escrow3" + "Spec/Tutorial/Escrow4" + "Spec/Tutorial/Escrow5" + "Spec/Tutorial/Escrow6" "Spec/Future" "Spec/Game" "Spec/GameStateMachine" @@ -203,6 +214,7 @@ "Spec/Rollup" "Spec/Stablecoin" "Spec/Uniswap" + "Spec/Uniswap/Endpoints" "Spec/TokenAccount" "Spec/Vesting" ]; diff --git a/nix/pkgs/haskell/materialized-linux/.plan.nix/quickcheck-dynamic.nix b/nix/pkgs/haskell/materialized-linux/.plan.nix/quickcheck-dynamic.nix index 305cd0cb25..87158f9e77 100644 --- a/nix/pkgs/haskell/materialized-linux/.plan.nix/quickcheck-dynamic.nix +++ b/nix/pkgs/haskell/materialized-linux/.plan.nix/quickcheck-dynamic.nix @@ -44,6 +44,7 @@ "Test/QuickCheck/DynamicLogic/Monad" "Test/QuickCheck/DynamicLogic/Quantify" "Test/QuickCheck/DynamicLogic/SmartShrinking" + "Test/QuickCheck/DynamicLogic/Utils" "Test/QuickCheck/StateModel" ]; hsSourceDirs = [ "src" ]; diff --git a/nix/pkgs/haskell/materialized-linux/default.nix b/nix/pkgs/haskell/materialized-linux/default.nix index ddf561fc38..94247bf388 100644 --- a/nix/pkgs/haskell/materialized-linux/default.nix +++ b/nix/pkgs/haskell/materialized-linux/default.nix @@ -380,6 +380,7 @@ "blockfrost-api".revision = (((hackage."blockfrost-api")."0.3.1.0").revisions).default; "blockfrost-api".flags.production = false; "blockfrost-api".flags.buildfast = true; + "html-entities".revision = (((hackage."html-entities")."1.1.4.5").revisions).default; "cryptohash-sha1".revision = (((hackage."cryptohash-sha1")."0.11.101.0").revisions).default; "warp-tls".revision = (((hackage."warp-tls")."3.3.2").revisions).default; "digest".revision = (((hackage."digest")."0.0.1.3").revisions).default; @@ -1692,6 +1693,7 @@ "servant-websockets".components.library.planned = lib.mkOverride 900 true; "ouroboros-network-framework".components.exes."demo-connection-manager".planned = lib.mkOverride 900 true; "sqlite-simple".components.library.planned = lib.mkOverride 900 true; + "html-entities".components.library.planned = lib.mkOverride 900 true; "constraints-extras".components.library.planned = lib.mkOverride 900 true; "monad-logger".components.library.planned = lib.mkOverride 900 true; "OneTuple".components.library.planned = lib.mkOverride 900 true; diff --git a/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-contract.nix b/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-contract.nix index 640543c40a..88af4b9c6f 100644 --- a/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-contract.nix +++ b/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-contract.nix @@ -57,6 +57,7 @@ (hsPkgs."freer-simple" or (errorHandler.buildDepError "freer-simple")) (hsPkgs."hashable" or (errorHandler.buildDepError "hashable")) (hsPkgs."hedgehog" or (errorHandler.buildDepError "hedgehog")) + (hsPkgs."html-entities" or (errorHandler.buildDepError "html-entities")) (hsPkgs."lens" or (errorHandler.buildDepError "lens")) (hsPkgs."memory" or (errorHandler.buildDepError "memory")) (hsPkgs."mmorph" or (errorHandler.buildDepError "mmorph")) @@ -151,6 +152,7 @@ ] ++ (pkgs.lib).optionals (!(compiler.isGhcjs && true || system.isGhcjs || system.isWindows)) [ "Plutus/Contract/Test" "Plutus/Contract/Test/Coverage" + "Plutus/Contract/Test/Coverage/ReportCoverage" "Plutus/Contract/Test/ContractModel" "Plutus/Contract/Test/ContractModel/Internal" "Plutus/Contract/Test/ContractModel/Symbolics" diff --git a/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-doc.nix b/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-doc.nix index f889dababe..f6b7f83368 100644 --- a/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-doc.nix +++ b/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-doc.nix @@ -57,6 +57,8 @@ (hsPkgs."random" or (errorHandler.buildDepError "random")) (hsPkgs."text" or (errorHandler.buildDepError "text")) (hsPkgs."aeson" or (errorHandler.buildDepError "aeson")) + (hsPkgs."tasty" or (errorHandler.buildDepError "tasty")) + (hsPkgs."tasty-quickcheck" or (errorHandler.buildDepError "tasty-quickcheck")) ] ++ (pkgs.lib).optional (!(compiler.isGhcjs && true || system.isGhcjs)) (hsPkgs."plutus-tx-plugin" or (errorHandler.buildDepError "plutus-tx-plugin")); build-tools = [ (hsPkgs.buildPackages.doctest.components.exes.doctest or (pkgs.buildPackages.doctest or (errorHandler.buildToolDepError "doctest:doctest"))) @@ -73,6 +75,13 @@ "HandlingBlockchainEvents" "HelloWorldApp" "WriteScriptsTo" + "Escrow" + "Escrow2" + "Escrow3" + "Escrow4" + "Escrow5" + "Escrow6" + "EscrowImpl" ]; hsSourceDirs = [ "plutus/tutorials" "plutus/howtos" ]; mainPath = (([ diff --git a/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-use-cases.nix b/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-use-cases.nix index b5cd38a6bc..f80d86479e 100644 --- a/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-use-cases.nix +++ b/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-use-cases.nix @@ -66,6 +66,8 @@ "Plutus/Contracts/ErrorHandling" "Plutus/Contracts/Escrow" "Plutus/Contracts/SimpleEscrow" + "Plutus/Contracts/Tutorial/Escrow" + "Plutus/Contracts/Tutorial/EscrowStrict" "Plutus/Contracts/Future" "Plutus/Contracts/Game" "Plutus/Contracts/GameStateMachine" @@ -159,8 +161,10 @@ (hsPkgs."plutus-contract" or (errorHandler.buildDepError "plutus-contract")) (hsPkgs."plutus-contract-certification" or (errorHandler.buildDepError "plutus-contract-certification")) (hsPkgs."plutus-ledger" or (errorHandler.buildDepError "plutus-ledger")) + (hsPkgs."plutus-ledger-api" or (errorHandler.buildDepError "plutus-ledger-api")) (hsPkgs."plutus-ledger-constraints" or (errorHandler.buildDepError "plutus-ledger-constraints")) (hsPkgs."plutus-use-cases" or (errorHandler.buildDepError "plutus-use-cases")) + (hsPkgs."playground-common" or (errorHandler.buildDepError "playground-common")) (hsPkgs."base" or (errorHandler.buildDepError "base")) (hsPkgs."bytestring" or (errorHandler.buildDepError "bytestring")) (hsPkgs."cardano-crypto-class" or (errorHandler.buildDepError "cardano-crypto-class")) @@ -191,6 +195,13 @@ "Spec/Escrow" "Spec/Escrow/Endpoints" "Spec/SimpleEscrow" + "Spec/Tutorial/Escrow" + "Spec/Tutorial/Escrow1" + "Spec/Tutorial/Escrow2" + "Spec/Tutorial/Escrow3" + "Spec/Tutorial/Escrow4" + "Spec/Tutorial/Escrow5" + "Spec/Tutorial/Escrow6" "Spec/Future" "Spec/Game" "Spec/GameStateMachine" @@ -203,6 +214,7 @@ "Spec/Rollup" "Spec/Stablecoin" "Spec/Uniswap" + "Spec/Uniswap/Endpoints" "Spec/TokenAccount" "Spec/Vesting" ]; diff --git a/nix/pkgs/haskell/materialized-windows/.plan.nix/quickcheck-dynamic.nix b/nix/pkgs/haskell/materialized-windows/.plan.nix/quickcheck-dynamic.nix index 305cd0cb25..87158f9e77 100644 --- a/nix/pkgs/haskell/materialized-windows/.plan.nix/quickcheck-dynamic.nix +++ b/nix/pkgs/haskell/materialized-windows/.plan.nix/quickcheck-dynamic.nix @@ -44,6 +44,7 @@ "Test/QuickCheck/DynamicLogic/Monad" "Test/QuickCheck/DynamicLogic/Quantify" "Test/QuickCheck/DynamicLogic/SmartShrinking" + "Test/QuickCheck/DynamicLogic/Utils" "Test/QuickCheck/StateModel" ]; hsSourceDirs = [ "src" ]; diff --git a/nix/pkgs/haskell/materialized-windows/default.nix b/nix/pkgs/haskell/materialized-windows/default.nix index 513b4e6c54..9d6ef23dc4 100644 --- a/nix/pkgs/haskell/materialized-windows/default.nix +++ b/nix/pkgs/haskell/materialized-windows/default.nix @@ -376,6 +376,7 @@ "blockfrost-api".revision = (((hackage."blockfrost-api")."0.3.1.0").revisions).default; "blockfrost-api".flags.production = false; "blockfrost-api".flags.buildfast = true; + "html-entities".revision = (((hackage."html-entities")."1.1.4.5").revisions).default; "cryptohash-sha1".revision = (((hackage."cryptohash-sha1")."0.11.101.0").revisions).default; "warp-tls".revision = (((hackage."warp-tls")."3.3.2").revisions).default; "digest".revision = (((hackage."digest")."0.0.1.3").revisions).default; @@ -1672,6 +1673,7 @@ "servant-websockets".components.library.planned = lib.mkOverride 900 true; "ouroboros-network-framework".components.exes."demo-connection-manager".planned = lib.mkOverride 900 true; "sqlite-simple".components.library.planned = lib.mkOverride 900 true; + "html-entities".components.library.planned = lib.mkOverride 900 true; "constraints-extras".components.library.planned = lib.mkOverride 900 true; "monad-logger".components.library.planned = lib.mkOverride 900 true; "OneTuple".components.library.planned = lib.mkOverride 900 true; diff --git a/plutus-contract-certification/src/Plutus/Contract/Test/Certification.hs b/plutus-contract-certification/src/Plutus/Contract/Test/Certification.hs index 44b7ecabce..a9e0582acf 100644 --- a/plutus-contract-certification/src/Plutus/Contract/Test/Certification.hs +++ b/plutus-contract-certification/src/Plutus/Contract/Test/Certification.hs @@ -16,17 +16,19 @@ data Certification m = Certification { certCoverageIndex :: CoverageIndex, -- ^ Coverage locations for on-chain test coverage. certNoLockedFunds :: Maybe (NoLockedFundsProof m), certNoLockedFundsLight :: Maybe (NoLockedFundsProofLight m), - certUnitTests :: Maybe (CoverageRef -> TestTree), -- ^ Unit tests using "Test.Tasty". See e.g. 'Plutus.Contract.Test.checkPredicateCoverage'. certCrashTolerance :: Maybe (Instance CrashTolerance m), -- ^ Contract model for testing robustness against off-chain code crashes. certWhitelist :: Maybe Whitelist, -- ^ List of allowed exceptions from on-chain code. Usually `Just 'defaultWhiteList'`. + certUnitTests :: Maybe (CoverageRef -> TestTree), -- ^ Unit tests using "Test.Tasty". See e.g. 'Plutus.Contract.Test.checkPredicateCoverage'. certDLTests :: [(String, DL m ())] -- ^ Unit tests using 'Plutus.Contract.Test.ContractModel.DL'. } defaultCertification :: Certification m -defaultCertification = Certification { certCoverageIndex = mempty - , certNoLockedFunds = Nothing - , certNoLockedFundsLight = Nothing - , certUnitTests = Nothing - , certCrashTolerance = Nothing - , certWhitelist = Just defaultWhitelist - , certDLTests = [] } +defaultCertification = Certification + { certCoverageIndex = mempty + , certNoLockedFunds = Nothing + , certNoLockedFundsLight = Nothing + , certUnitTests = Nothing + , certCrashTolerance = Nothing + , certWhitelist = Just defaultWhitelist + , certDLTests = [] + } diff --git a/plutus-contract-certification/src/Plutus/Contract/Test/Certification/Run.hs b/plutus-contract-certification/src/Plutus/Contract/Test/Certification/Run.hs index b1847d81b1..48de3b1c11 100644 --- a/plutus-contract-certification/src/Plutus/Contract/Test/Certification/Run.hs +++ b/plutus-contract-certification/src/Plutus/Contract/Test/Certification/Run.hs @@ -130,12 +130,12 @@ runStandardProperty opts covIdx = liftIORep $ quickCheckWithCoverageAndResult @m defaultCheckOptionsContractModel covopts - $ const (pure True) + (\ _ -> pure True) checkNoLockedFunds :: ContractModel m => CertificationOptions -> NoLockedFundsProof m -> CertMonad QC.Result checkNoLockedFunds opts prf = lift $ quickCheckWithResult (mkQCArgs opts) - $ checkNoLockedFundsProof defaultCheckOptionsContractModel prf + $ checkNoLockedFundsProof prf checkNoLockedFundsLight :: ContractModel m => CertificationOptions -> NoLockedFundsProofLight m -> CertMonad QC.Result checkNoLockedFundsLight opts prf = diff --git a/plutus-contract/plutus-contract.cabal b/plutus-contract/plutus-contract.cabal index 0a2be11e9b..281d0591cd 100644 --- a/plutus-contract/plutus-contract.cabal +++ b/plutus-contract/plutus-contract.cabal @@ -127,6 +127,7 @@ library freer-simple -any, hashable -any, hedgehog -any, + html-entities -any, lens -any, memory -any, mmorph -any, @@ -161,6 +162,7 @@ library exposed-modules: Plutus.Contract.Test Plutus.Contract.Test.Coverage + Plutus.Contract.Test.Coverage.ReportCoverage Plutus.Contract.Test.ContractModel Plutus.Contract.Test.ContractModel.Internal Plutus.Contract.Test.ContractModel.Symbolics diff --git a/plutus-contract/src/Plutus/Contract/Test/ContractModel.hs b/plutus-contract/src/Plutus/Contract/Test/ContractModel.hs index f84029a113..b9f5f4f28a 100644 --- a/plutus-contract/src/Plutus/Contract/Test/ContractModel.hs +++ b/plutus-contract/src/Plutus/Contract/Test/ContractModel.hs @@ -135,6 +135,8 @@ module Plutus.Contract.Test.ContractModel , checkNoLockedFundsProofFast , NoLockedFundsProofLight(..) , checkNoLockedFundsProofLight + , checkNoLockedFundsProofWithOptions + , checkNoLockedFundsProofFastWithOptions -- $checkNoPartiality , Whitelist , whitelistOk diff --git a/plutus-contract/src/Plutus/Contract/Test/ContractModel/CrashTolerance.hs b/plutus-contract/src/Plutus/Contract/Test/ContractModel/CrashTolerance.hs index c3af0f5526..3f4343564b 100644 --- a/plutus-contract/src/Plutus/Contract/Test/ContractModel/CrashTolerance.hs +++ b/plutus-contract/src/Plutus/Contract/Test/ContractModel/CrashTolerance.hs @@ -140,8 +140,11 @@ instance forall state. -- An action may start its own contract instances and we need to keep track of them aliveContractInstances %= ([Key k | StartContract (UnderlyingContractInstanceKey k) _ <- startInstances s (UnderlyingAction a)] ++) where - embed :: Spec state a -> Spec (WithCrashTolerance state) a - embed (Spec comp) = Spec (zoom (liftL _contractState underlyingModelState) comp) + + nextReactiveState slot = embed $ nextReactiveState slot + + monitoring (s,s') (UnderlyingAction a) = monitoring (_underlyingModelState <$> s,_underlyingModelState <$> s') a + monitoring _ _ = id arbitraryAction s = frequency [ (10, UnderlyingAction <$> arbitraryAction (_underlyingModelState <$> s)) , (1, Crash <$> QC.elements (s ^. contractState . aliveContractInstances)) @@ -153,3 +156,5 @@ instance forall state. liftL :: Functor t => (forall a. t a -> a) -> Lens' s a -> Lens' (t s) (t a) liftL extr l ft ts = getCompose . l (Compose . ft . (<$ ts)) $ extr ts +embed :: Spec state a -> Spec (WithCrashTolerance state) a +embed (Spec comp) = Spec (zoom (liftL _contractState underlyingModelState) comp) diff --git a/plutus-contract/src/Plutus/Contract/Test/ContractModel/Internal.hs b/plutus-contract/src/Plutus/Contract/Test/ContractModel/Internal.hs index daaa04f485..d05f1e158e 100644 --- a/plutus-contract/src/Plutus/Contract/Test/ContractModel/Internal.hs +++ b/plutus-contract/src/Plutus/Contract/Test/ContractModel/Internal.hs @@ -93,6 +93,7 @@ module Plutus.Contract.Test.ContractModel.Internal , assertModel , stopping , weight + , getSize , monitor -- * Properties @@ -147,6 +148,8 @@ module Plutus.Contract.Test.ContractModel.Internal , checkNoLockedFundsProofFast , NoLockedFundsProofLight(..) , checkNoLockedFundsProofLight + , checkNoLockedFundsProofWithOptions + , checkNoLockedFundsProofFastWithOptions -- $checkNoPartiality , Whitelist , whitelistOk @@ -199,7 +202,7 @@ import Ledger.Slot import Ledger.Value (AssetClass) import Plutus.Contract (Contract, ContractError, ContractInstanceId, Endpoint, endpoint) import Plutus.Contract.Schema (Input) -import Plutus.Contract.Test +import Plutus.Contract.Test hiding (not) import Plutus.Contract.Test.ContractModel.Symbolics import Plutus.Contract.Test.Coverage import Plutus.Trace.Effects.EmulatorControl (discardWallets) @@ -212,11 +215,11 @@ import PlutusTx.Coverage import PlutusTx.ErrorCodes import Streaming qualified as S import Test.QuickCheck.DynamicLogic.Monad qualified as DL -import Test.QuickCheck.StateModel hiding (Action, Actions (..), arbitraryAction, initialState, monitoring, nextState, - pattern Actions, perform, precondition, shrinkAction, stateAfter) +import Test.QuickCheck.StateModel hiding (Action, Actions (..), actionName, arbitraryAction, initialState, monitoring, + nextState, pattern Actions, perform, precondition, shrinkAction, stateAfter) import Test.QuickCheck.StateModel qualified as StateModel -import Test.QuickCheck hiding (ShrinkState, checkCoverage, (.&&.), (.||.)) +import Test.QuickCheck hiding (ShrinkState, checkCoverage, getSize, (.&&.), (.||.)) import Test.QuickCheck qualified as QC import Test.QuickCheck.Monadic (PropertyM, monadic) import Test.QuickCheck.Monadic qualified as QC @@ -456,6 +459,10 @@ class ( Typeable state -- `anyActions`. arbitraryAction :: ModelState state -> Gen (Action state) + -- | The name of an Action, used to report statistics. + actionName :: Action state -> String + actionName = head . words . show + -- | The probability that we will generate a `WaitUntil` in a given state waitProbability :: ModelState state -> Double waitProbability _ = 0.1 @@ -814,6 +821,10 @@ instance ContractModel state => StateModel (ModelState state) where type ActionMonad (ModelState state) = ContractMonad state + actionName (ContractAction _ act) = actionName act + actionName (Unilateral _) = "Unilateral" + actionName (WaitUntil _) = "WaitUntil" + arbitraryAction s = -- TODO: do we need some way to control the distribution -- between actions and waits here? @@ -834,7 +845,7 @@ instance ContractModel state => StateModel (ModelState state) where shrinkAction _ _ = [] initialState = ModelState { _currentSlot = 1 - , _balanceChanges = Map.empty + , _balanceChanges = Map.fromList [(w,mempty) | w <- knownWallets] , _minted = mempty , _assertions = mempty , _assertionsOk = True @@ -1199,7 +1210,7 @@ anyActions :: Int -> DL state () anyActions = DL.anyActions -- | Generate a sequence of random actions using `arbitraryAction`. All actions satisfy their --- `precondition`s. Actions are generated until the `stopping` stage is reached. +-- `precondition`s. Actions may be generated until the `stopping` stage is reached; the expected length is size/2. anyActions_ :: DL state () anyActions_ = DL.anyActions_ @@ -1213,15 +1224,16 @@ anyActions_ = DL.anyActions_ -- Conversely, before the stopping phase, branches starting with `stopping` -- are avoided unless there are no other possible choices. -- --- For example, here is the definition of `anyActions_`: +-- For example, here is the definition of `anyActions`: -- -- @ --- `anyActions_` = `stopping` `Control.Applicative.<|>` (`anyAction` >> `anyActions_`) +-- `anyActions` n = `stopping` `Control.Applicative.<|>` pure () +-- `Control.Applicative.<|>` (`weight` (fromIntegral n) >> `anyAction` >> `anyActions` n) -- @ -- --- The effect of this definition is that the second branch will be taken until the desired number +-- The effect of this definition is that the second or third branch will be taken until the desired number -- of actions have been generated, at which point the `stopping` branch will be taken and --- generation stops (or continues with whatever comes after the `anyActions_` call). +-- generation stops (or continues with whatever comes after the `anyActions` call). -- -- Now, it might not be possible, or too hard, to find a way to terminate a scenario. For -- instance, this scenario has no finite test cases: @@ -1259,6 +1271,20 @@ stopping = DL.stopping weight :: Double -> DL state () weight = DL.weight +-- | Sometimes test case generation should depend on QuickCheck's size +-- parameter. This can be accessed using @getSize@. For example, @anyActions_@ is defined by +-- +-- @ +-- anyActions_ = do n <- getSize +-- anyActions (n `div` 2 + 1) +-- @ +-- +-- so that we generate a random number of actions, but on average half the size (which is about the same as +-- the average random positive integer, or length of a list). + +getSize :: DL state Int +getSize = DL.getSize + -- | The `monitor` function allows you to collect statistics of your testing using QuickCheck -- functions like `Test.QuickCheck.label`, `Test.QuickCheck.collect`, `Test.QuickCheck.classify`, -- and `Test.QuickCheck.tabulate`. See also the `monitoring` method of `ContractModel` which is @@ -1706,21 +1732,33 @@ defaultNLFP = NoLockedFundsProof { nlfpMainStrategy = return () -- are killed and their private keys are deleted from the emulator state. checkNoLockedFundsProof + :: (ContractModel model) + => NoLockedFundsProof model + -> Property +checkNoLockedFundsProof = checkNoLockedFundsProofWithOptions defaultCheckOptionsContractModel + +checkNoLockedFundsProofFast + :: (ContractModel model) + => NoLockedFundsProof model + -> Property +checkNoLockedFundsProofFast = checkNoLockedFundsProofFastWithOptions defaultCheckOptionsContractModel + +checkNoLockedFundsProofWithOptions :: (ContractModel model) => CheckOptions -> NoLockedFundsProof model -> Property -checkNoLockedFundsProof options = +checkNoLockedFundsProofWithOptions options = checkNoLockedFundsProof' prop where prop = propRunActionsWithOptions' options defaultCoverageOptions (\ _ -> TracePredicate $ pure True) -checkNoLockedFundsProofFast +checkNoLockedFundsProofFastWithOptions :: (ContractModel model) => CheckOptions -> NoLockedFundsProof model -> Property -checkNoLockedFundsProofFast _ = checkNoLockedFundsProof' (const $ property True) +checkNoLockedFundsProofFastWithOptions _ = checkNoLockedFundsProof' (const $ property True) checkNoLockedFundsProof' :: (ContractModel model) @@ -1736,7 +1774,9 @@ checkNoLockedFundsProof' run NoLockedFundsProof{nlfpMainStrategy = mainStrat, let s0 = (stateAfter $ Actions as) s = stateAfter $ Actions (as ++ as') in foldl (QC..&&.) (counterexample "Main run prop" (run (toStateModelActions $ Actions $ as ++ as')) QC..&&. (counterexample "Main strategy" . counterexample (show . Actions $ as ++ as') $ prop s0 s)) - [ walletProp s0 as w bal | (w, bal) <- Map.toList (s ^. balanceChanges) ] + [ walletProp s0 as w bal | (w, bal) <- Map.toList (s ^. balanceChanges) + , not $ bal `symLeq` (s0 ^. balanceChange w) ] + -- if the main strategy leaves w with <= the starting value, then doing nothing is a good wallet strategy. where nextVarIdx as = 1 + maximum ([0] ++ [ i | i <- varNumOf <$> as ]) prop s0 s = diff --git a/plutus-contract/src/Plutus/Contract/Test/ContractModel/Symbolics.hs b/plutus-contract/src/Plutus/Contract/Test/ContractModel/Symbolics.hs index 0e4e6900ad..de1de9c724 100644 --- a/plutus-contract/src/Plutus/Contract/Test/ContractModel/Symbolics.hs +++ b/plutus-contract/src/Plutus/Contract/Test/ContractModel/Symbolics.hs @@ -37,7 +37,7 @@ newtype AssetKey = AssetKey Int deriving (Ord, Eq, Show, Num, JSON.FromJSONKey, data SymToken = SymToken { symVar :: Var AssetKey, symVarIdx :: String } deriving (Ord, Eq, Data) -- | A symbolic value is a combination of a real value and a value associating symbolic -- tokens with an amount -data SymValue = SymValue { symValMap :: Map SymToken Integer, actualValPart :: Value } deriving (Show) +data SymValue = SymValue { symValMap :: Map SymToken Integer, actualValPart :: Value } deriving (Show, Data) instance Show SymToken where show (SymToken (Var i) n) = "tok" ++ show i ++ "." ++ n diff --git a/plutus-contract/src/Plutus/Contract/Test/Coverage.hs b/plutus-contract/src/Plutus/Contract/Test/Coverage.hs index d4d8f158e4..d03e0c6c1d 100644 --- a/plutus-contract/src/Plutus/Contract/Test/Coverage.hs +++ b/plutus-contract/src/Plutus/Contract/Test/Coverage.hs @@ -8,6 +8,7 @@ module Plutus.Contract.Test.Coverage , CoverageRef(..) , newCoverageRef , readCoverageRef + , writeCoverageReport ) where import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey) @@ -34,9 +35,10 @@ import Wallet.Emulator.MultiAgent (EmulatorEvent, EmulatorEvent' (..), EmulatorT import Wallet.Types import Data.IORef +import Plutus.Contract.Test.Coverage.ReportCoverage qualified as ReportCoverage --- | Get every endpoint name that has been invoced in the emulator events in `es` +-- | Get every endpoint name that has been invoked in the emulator events in `es` -- indexed by `ContractInstanceTag` getInvokedEndpoints :: [EmulatorEvent] -> Map ContractInstanceTag (Set String) getInvokedEndpoints es = @@ -72,6 +74,10 @@ newCoverageRef = CoverageRef <$> newIORef mempty readCoverageRef :: CoverageRef -> IO CoverageReport readCoverageRef (CoverageRef ioref) = readIORef ioref +-- | Write a coverage report to name.html for the given index. +writeCoverageReport :: String -> CoverageIndex -> CoverageReport -> IO () +writeCoverageReport = ReportCoverage.writeCoverageReport + -- TODO: Move this to plutus core to avoid orhpan instance instance NFData CovLoc where rnf (CovLoc f sl el sc ec) = diff --git a/plutus-contract/src/Plutus/Contract/Test/Coverage/ReportCoverage.hs b/plutus-contract/src/Plutus/Contract/Test/Coverage/ReportCoverage.hs new file mode 100644 index 0000000000..63c4ed87de --- /dev/null +++ b/plutus-contract/src/Plutus/Contract/Test/Coverage/ReportCoverage.hs @@ -0,0 +1,271 @@ +module Plutus.Contract.Test.Coverage.ReportCoverage(writeCoverageReport) where + +import Control.Exception +import Data.Function +import Data.List +import Data.Map qualified as Map +import Data.Ord +import Data.Set (Set) +import Data.Set qualified as Set +import Data.Text (pack, unpack) +import HTMLEntities.Text (text) + +import PlutusTx.Coverage + +-- Position (in a file), and status (of a character) + +type Pos = (Int,Int) -- line, column + +predPos, succPos :: Pos -> Pos + +predPos (l,1) = (l-1,maxBound) +predPos (l,c) = (l,c-1) + +succPos (l,c) = (l,c+1) + +data Status = AlwaysTrue | AlwaysFalse | Uncovered | OffChain | Covered + -- Covered comes last, because this means that all of the other status + -- take precedence when there are two swipes for the same interval + -- (one from the base coverage, and the other from the "uncovered" set) + deriving (Eq, Ord, Show) + +statusStyle :: Status -> String +statusStyle Covered = "background-color:white;color:black" +statusStyle AlwaysTrue = "background-color:lightgreen;color:black" +statusStyle AlwaysFalse = "background-color:lightpink;color:black" +statusStyle Uncovered = "background-color:black;color:orangered" +statusStyle OffChain = "background-color:lightgray;color:gray" + +-- A "swipe" represents colouring a region of a file with a +-- status. Our overall approach is to convert coverage information +-- into a collection of non-overlapping, but possibly nested swipes, +-- and then converting this into an orderedlist of disjoint swipes +-- which can be used for generating colours. + +data Swipe = Swipe { swipeStart :: Pos, + swipeEnd :: Pos, + swipeStatus :: Status } + deriving (Eq, Show) + +-- This surprising ordering on swipes has the property that if s1 is +-- nested within s2, then s1 <= s2. Given that no two swipes overlap, +-- then s1 <= s2 precisely if s1 precedes s2 entirely, or s1 is nested +-- within s2. It follow that, in a sorted list of swipes, the first +-- one has no other swipes nested within it, and therefore its colour +-- takes priority over all other swipes. We make use of this in +-- converting a set of swipes to a set of disjoint swipes with the +-- same coloration. + +instance Ord Swipe where + (<=) = (<=) `on` \(Swipe start end status) -> (end, Down start, status) + +precedes :: Swipe -> Swipe -> Bool +precedes sw sw' = swipeEnd sw < swipeStart sw' + +-- Is the first swipe nested within the second? + +nested :: Swipe -> Swipe -> Bool +nested (Swipe from to _) (Swipe from' to' _) = from >= from' && to <= to' + +-- Let the first swipe "swipe over" part of the second. The resulting +-- swipes do not overlap. The first swipe must be nested within the +-- second. + +combineNestedSwipes :: Swipe -> Swipe -> [Swipe] +combineNestedSwipes (Swipe from to s) (Swipe from' to' s') = + [Swipe from' (predPos from) s' | from /= from'] ++ + [Swipe from to s] ++ + [Swipe (succPos to) to' s' | to /= to'] + +-- Flatten an ordered list of swipes, to get a non-overlapping list. +-- Nested swipes "swipe over" the outer swipe. Because of the custom +-- ordering on swipes, the first swipe in the list cannot have any +-- other swipe in the listed nested within it, which means that its +-- colour "wins" over all others. + +flattenSwipes :: [Swipe] -> [Swipe] +flattenSwipes [] = [] +flattenSwipes (sw:swipes) = swipeOver sw . flattenSwipes $ swipes + +swipeOver :: Swipe -> [Swipe] -> [Swipe] +swipeOver sw [] = [sw] +swipeOver sw (sw':swipes) + | swipeEnd sw < swipeStart sw' = sw:sw':swipes + | swipeEnd sw' < swipeStart sw = sw':swipeOver sw swipes + | nested sw sw' = combineNestedSwipes sw sw' ++ swipes + | otherwise = error . unlines $ + "swipeOver: precondition violated; swipes are not nested or disjoint.": + map show (sw:sw':take 8 swipes) + +-- Convert an ordered list of non-intersecting swipes that may swipe +-- any region in a file, into a list of swipes applied to each line. + +type SwipesPerLine = [(Int,[Swipe])] + +swipesByLine :: [Swipe] -> SwipesPerLine +swipesByLine = map addLine . groupBy ((==) `on` (fst.swipeStart)) . concatMap splitSwipe + where splitSwipe s@(Swipe (fromLine,_) (toLine,_) _) + | fromLine == toLine = [s] + | otherwise = s{swipeEnd = (fromLine,maxBound)}: + splitSwipe s{swipeStart = (fromLine+1,1)} + addLine swipes = (fst . swipeStart . head $ swipes, swipes) + +-- Extend a list of swipes-per-line by including non-swiped lines that +-- are within windowLines of a swiped line. + +windowLines :: Int +windowLines = 5 + +includeNearby :: SwipesPerLine -> SwipesPerLine +includeNearby swipes = excluding 1 swipes + where excluding _ [] = [] + excluding n nSwipes + | n > nextSwiped = error . unlines $ ("Bad excluding: "++show n):map show (take 10 nSwipes) + | n == nextSwiped = including n nSwipes + | n+windowLines < nextSwiped = excluding (nextSwiped-windowLines) nSwipes + | otherwise = (n,[]):excluding (n+1) nSwipes + where nextSwiped = fst (head nSwipes) + including _ [] = [] + including n ((next,swipe):nSwipes) + | n > next = error . unlines $ ("Bad including: "++show n):map show (take 10 ((next,swipe):nSwipes)) + | n == next = + (next,swipe):including (n+1) nSwipes + | n+windowLines >= next = + (n,[]):including (n+1) ((next,swipe):nSwipes) + | n+windowLines < next = + [(i,[]) | i <- [n..n-1+windowLines]] ++ excluding (n+windowLines) ((next,swipe):nSwipes) + | otherwise = error "impossible" + +-- Extend a list of swipes-per-line to include non-swiped lines that +-- form a small gap between swiped blocks. Gaps are replaced by three +-- vertical dots; there is no sense in replacing a gap of three or +-- fewer lines this way. + +fillSmallGaps :: SwipesPerLine -> SwipesPerLine +fillSmallGaps ((n,swipes):(n',swipes'):nSwipes) + | n+4 >= n' = (n,swipes):[(i,[]) | i <- [n+1..n'-1]] ++ fillSmallGaps ((n',swipes'):nSwipes) + | otherwise = (n,swipes):fillSmallGaps ((n',swipes'):nSwipes) +fillSmallGaps swipes = swipes + +-- Generate HTML elements + +element :: String -> [(String,String)] -> String -> String +element name attrs body = + "<"++name++" "++ + concat [a++"="++b++" " | (a,b) <- attrs]++ + ">"++body++"" + +quote :: String -> String +quote s = q++s++q + where q = "\"" + +encode :: String -> String +encode = unpack . text . pack + +-- Read source files and extract coverage information. + +type FileInfo = (String, [String], Set CoverageAnnotation, Set CoverageAnnotation) + +files :: CoverageIndex -> CoverageReport -> IO [FileInfo] +files ci@(CoverageIndex metadataMap) (CoverageReport annots) = sequence [file n | n <- fileNames ci] + where file name = do + body <- either (const "" :: IOException -> String) id <$> + try (readFile name) + return (name, lines body, covx name, covs name) + covx name = Set.filter ((==name) . _covLocFile . getCovLoc) . Map.keysSet $ metadataMap + covs name = Set.filter ((==name) . _covLocFile . getCovLoc) annots + +fileNames :: CoverageIndex -> [String] +fileNames (CoverageIndex metadataMap) = + Set.toList . Set.map (_covLocFile . getCovLoc) . Map.keysSet $ metadataMap + +getCovLoc :: CoverageAnnotation -> CovLoc +getCovLoc (CoverLocation c) = c +getCovLoc (CoverBool c _) = c + +locSwipe :: CovLoc -> Status -> Swipe +locSwipe loc status = + Swipe (_covLocStartLine loc, _covLocStartCol loc) + (_covLocEndLine loc, _covLocEndCol loc) + status + +-- Generate the coverage report and write to an HTML file. + +writeCoverageReport :: String -> CoverageIndex -> CoverageReport -> IO () +writeCoverageReport name ci cr = do + fs <- files ci cr + writeFile (name++".html") . coverageReportHtml $ fs + +coverageReportHtml :: [FileInfo] -> String +coverageReportHtml fs = element "body" [] $ report + where + report = header ++ concat ["
"++file name body covx annots | (name, body, covx, annots) <- fs] + header = + element "h1" [] "Files" ++ + element "ul" [] + (concat [element "li" [] . element "a" [("href",quote ("#"++name))] $ name + | (name,_,_,_) <- fs]) + file name body covx annots = + let uncovered = covx Set.\\ annots + swipes = [ Swipe (_covLocStartLine loc,_covLocStartCol loc) + (_covLocEndLine loc,_covLocEndCol loc) $ + case (CoverBool loc True `Set.member` uncovered, + CoverBool loc False `Set.member` uncovered) of + (True, True) -> Uncovered + (False, True) -> AlwaysTrue + (True, False) -> AlwaysFalse + (False, False) -> Uncovered + | loc <- Set.toList . Set.map getCovLoc $ uncovered ] + base = baseCoverage covx + swipes' = flattenSwipes . sort $ swipes ++ base + in + element "h2" [("id",quote name)] name ++ + element "pre" [] (unlines + (annotateLines + (zip [1..] body) + (fillSmallGaps . includeNearby . swipesByLine $ swipes'))) + +-- Convert a coverage index into a list of swipes that colour all the +-- text in the index "Covered". This is the starting point for adding +-- colours indicating *lack* of coverage. At this point we discard +-- nested coverage locations. + +baseCoverage :: Set CoverageAnnotation -> [Swipe] +baseCoverage covs = + outermost . sort . map (`locSwipe` Covered) . Set.toList . Set.map getCovLoc $ covs + where outermost = reverse . removeNested . reverse + removeNested (sw:sw':swipes) + | nested sw' sw = removeNested (sw:swipes) + | precedes sw' sw = sw:removeNested (sw':swipes) + removeNested swipes = swipes + +-- Apply swipes to the selected contents of a file + +annotateLines :: [(Int,String)] -> SwipesPerLine -> [String] +annotateLines _ [] = [] +annotateLines [] _ = [element "div" [("style","color:red")] "Source code not available"] +annotateLines ((n,line):nLines) ((n',swipes):nSwipes) + | n < n' = replicate 3 "." ++ + annotateLines (dropWhile (( String -> [Swipe] -> String +annotateLine n line swipes = + showLineNo n++" "++swipeLine 1 line swipes + +swipeLine :: Int -> String -> [Swipe] -> String +swipeLine _ line [] = element "span" [("style",statusStyle OffChain)] $ encode line +swipeLine c line s@(Swipe (_,from) (_,to) stat:swipes) + | c < from = element "span" [("style",statusStyle OffChain)] (encode $ take (from-c) line) ++ + swipeLine from (drop (from-c) line) s + | otherwise = element "span" [("style",statusStyle stat)] (encode $ take (to+1-from) line) ++ + swipeLine (to+1) (drop (to+1-from) line) swipes + +showLineNo :: Int -> String +showLineNo n = reverse . take 6 . reverse $ replicate 6 ' ' ++ show n + diff --git a/plutus-use-cases/Auction.html b/plutus-use-cases/Auction.html new file mode 100644 index 0000000000..2a18a1c7a2 --- /dev/null +++ b/plutus-use-cases/Auction.html @@ -0,0 +1,91 @@ +

Files


src/Plutus/Contracts/Auction.hs

.
+.
+.
+    71            , highestBidder :: PaymentPubKeyHash
+    72            }
+    73        deriving stock (Haskell.Eq, Haskell.Show, Generic)
+    74        deriving anyclass (ToJSON, FromJSON)
+    75    
+    76    PlutusTx.unstableMakeIsData ''HighestBid
+    77    
+    78    -- | The states of the auction
+    79    data AuctionState
+    80        = Ongoing HighestBid -- Bids can be submitted.
+    81        | Finished HighestBid -- The auction is finished
+.
+.
+.
+   104    --   highest bidder is seller of the asset. So if nobody submits
+   105    --   any bids, the seller gets the asset back after the auction has ended.
+   106    initialState :: PaymentPubKeyHash -> AuctionState
+   107    initialState self = Ongoing HighestBid{highestBid = 0, highestBidder = self}
+   108    
+   109    PlutusTx.unstableMakeIsData ''AuctionState
+   110    
+   111    -- | Transition between auction states
+   112    data AuctionInput
+   113        = Bid { newBid :: Ada, newBidder :: PaymentPubKeyHash } -- Increase the price
+   114        | Payout
+.
+.
+.
+   124    auctionTransition
+   125        :: AuctionParams
+   126        -> State AuctionState
+   127        -> AuctionInput
+   128        -> Maybe (TxConstraints Void Void, State AuctionState)
+   129    auctionTransition AuctionParams{apOwner, apAsset, apEndTime} State{stateData=oldStateData, stateValue=oldStateValue} input =
+   130        case (oldStateData, input) of
+   131    
+   132            (Ongoing HighestBid{highestBid, highestBidder}, Bid{newBid, newBidder}) | newBid > highestBid -> -- if the new bid is higher,
+   133                let constraints = if highestBid == 0 then mempty else
+   134                        Constraints.mustPayToPubKey highestBidder (Ada.toValue highestBid) -- we pay back the previous highest bid
+   135                        <> Constraints.mustValidateIn (Interval.to $ apEndTime - 1) -- but only if we haven't gone past 'apEndTime'
+   136                    newState =
+   137                        State
+   138                            { stateData = Ongoing HighestBid{highestBid = newBid, highestBidder = newBidder}
+   139                            , stateValue = Value.noAdaValue oldStateValue
+   140                                        <> Ada.toValue (Ada.fromValue oldStateValue - highestBid)
+   141                                        <> Ada.toValue newBid -- and lock the new bid in the script output
+   142                            }
+   143                in Just (constraints, newState)
+   144    
+   145            (Ongoing h@HighestBid{highestBidder, highestBid}, Payout) ->
+   146                let constraints =
+   147                        Constraints.mustValidateIn (Interval.from apEndTime) -- When the auction has ended,
+   148                        <> Constraints.mustPayToPubKey apOwner (Ada.toValue highestBid) -- the owner receives the payment
+   149                        <> Constraints.mustPayToPubKey highestBidder apAsset -- and the highest bidder the asset
+   150                    newState = State { stateData = Finished h, stateValue = mempty }
+   151                in Just (constraints, newState)
+   152    
+   153            -- Any other combination of 'AuctionState' and 'AuctionInput' is disallowed.
+   154            -- This rules out new bids that don't go over the current highest bid.
+   155            _ -> Nothing
+   156    
+   157    
+   158    {-# INLINABLE auctionStateMachine #-}
+   159    auctionStateMachine :: (ThreadToken, AuctionParams) -> AuctionMachine
+   160    auctionStateMachine (threadToken, auctionParams) =
+   161        SM.mkStateMachine (Just threadToken) (auctionTransition auctionParams) isFinal
+   162      where
+   163        isFinal Finished{} = True
+   164        isFinal _          = False
+   165    
+   166    {-# INLINABLE mkValidator #-}
+   167    mkValidator :: (ThreadToken, AuctionParams) -> Scripts.ValidatorType AuctionMachine
+   168    mkValidator = SM.mkValidator . auctionStateMachine
+   169    
+   170    -- | The script instance of the auction state machine. It contains the state
+   171    --   machine compiled to a Plutus core validator script.
+   172    typedValidator :: (ThreadToken, AuctionParams) -> Scripts.TypedValidator AuctionMachine
+   173    typedValidator = Scripts.mkTypedValidatorParam @AuctionMachine
+.
+.
+.
+   365                Transition _ (Ongoing s) -> loop s
+   366                InitialState (Ongoing s) -> loop s
+   367                _                        -> logWarn CurrentStateNotFound
+   368    
+   369    covIdx :: CoverageIndex
+   370    covIdx = getCovIdx $$(PlutusTx.compile [|| mkValidator ||])
+
\ No newline at end of file diff --git a/plutus-use-cases/plutus-use-cases.cabal b/plutus-use-cases/plutus-use-cases.cabal index eba8502bc8..30e0a18cf4 100644 --- a/plutus-use-cases/plutus-use-cases.cabal +++ b/plutus-use-cases/plutus-use-cases.cabal @@ -34,6 +34,8 @@ library Plutus.Contracts.ErrorHandling Plutus.Contracts.Escrow Plutus.Contracts.SimpleEscrow + Plutus.Contracts.Tutorial.Escrow + Plutus.Contracts.Tutorial.EscrowStrict Plutus.Contracts.Future Plutus.Contracts.Game Plutus.Contracts.GameStateMachine @@ -110,6 +112,13 @@ test-suite plutus-use-cases-test Spec.Escrow Spec.Escrow.Endpoints Spec.SimpleEscrow + Spec.Tutorial.Escrow + Spec.Tutorial.Escrow1 + Spec.Tutorial.Escrow2 + Spec.Tutorial.Escrow3 + Spec.Tutorial.Escrow4 + Spec.Tutorial.Escrow5 + Spec.Tutorial.Escrow6 Spec.Future Spec.Game Spec.GameStateMachine @@ -122,6 +131,7 @@ test-suite plutus-use-cases-test Spec.Rollup Spec.Stablecoin Spec.Uniswap + Spec.Uniswap.Endpoints Spec.TokenAccount Spec.Vesting default-language: Haskell2010 @@ -136,8 +146,10 @@ test-suite plutus-use-cases-test plutus-contract -any, plutus-contract-certification -any, plutus-ledger -any, + plutus-ledger-api, plutus-ledger-constraints -any, - plutus-use-cases -any + plutus-use-cases -any, + playground-common -any build-depends: base >=4.9 && <5, bytestring -any, diff --git a/plutus-use-cases/src/Plutus/Contracts/Auction.hs b/plutus-use-cases/src/Plutus/Contracts/Auction.hs index 81ec6f82d0..3d4b8bf763 100644 --- a/plutus-use-cases/src/Plutus/Contracts/Auction.hs +++ b/plutus-use-cases/src/Plutus/Contracts/Auction.hs @@ -11,6 +11,7 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} +{-# OPTIONS_GHC -g -fplugin-opt PlutusTx.Plugin:coverage-all #-} module Plutus.Contracts.Auction( AuctionState(..), AuctionInput(..), @@ -23,7 +24,8 @@ module Plutus.Contracts.Auction( AuctionOutput(..), AuctionError(..), ThreadToken, - SM.getThreadToken + SM.getThreadToken, + covIdx ) where import Control.Lens (makeClassyPrisms) @@ -45,6 +47,8 @@ import Plutus.Contract.StateMachine (State (..), StateMachine (..), StateMachine import Plutus.Contract.StateMachine qualified as SM import Plutus.Contract.Util (loopM) import PlutusTx qualified +import PlutusTx.Code +import PlutusTx.Coverage import PlutusTx.Prelude import Prelude qualified as Haskell @@ -361,3 +365,6 @@ auctionBuyer currency params = do Transition _ (Ongoing s) -> loop s InitialState (Ongoing s) -> loop s _ -> logWarn CurrentStateNotFound + +covIdx :: CoverageIndex +covIdx = getCovIdx $$(PlutusTx.compile [|| mkValidator ||]) diff --git a/plutus-use-cases/src/Plutus/Contracts/Escrow.hs b/plutus-use-cases/src/Plutus/Contracts/Escrow.hs index 939a8bcc26..5f66e1be02 100644 --- a/plutus-use-cases/src/Plutus/Contracts/Escrow.hs +++ b/plutus-use-cases/src/Plutus/Contracts/Escrow.hs @@ -13,6 +13,7 @@ {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:debug-context #-} +{-# OPTIONS_GHC -g -fplugin-opt PlutusTx.Plugin:coverage-all #-} -- | A general-purpose escrow contract in Plutus module Plutus.Contracts.Escrow( -- $escrow @@ -40,6 +41,8 @@ module Plutus.Contracts.Escrow( , EscrowSchema -- * Exposed for test endpoints , Action(..) + -- * Coverage + , covIdx ) where import Control.Lens (makeClassyPrisms, review, view) @@ -64,6 +67,8 @@ import Ledger.Value (Value, geq, lt) import Plutus.Contract import Plutus.Contract.Typed.Tx qualified as Typed import PlutusTx qualified +import PlutusTx.Code +import PlutusTx.Coverage import PlutusTx.Prelude hiding (Applicative (..), Semigroup (..), check, foldMap) import Prelude (Semigroup (..), foldMap) @@ -356,3 +361,6 @@ payRedeemRefund params vl = do -- Pay the value 'vl' into the contract _ <- pay inst params vl go + +covIdx :: CoverageIndex +covIdx = getCovIdx $$(PlutusTx.compile [|| validate ||]) diff --git a/plutus-use-cases/src/Plutus/Contracts/Tutorial/Escrow.hs b/plutus-use-cases/src/Plutus/Contracts/Tutorial/Escrow.hs new file mode 100644 index 0000000000..e1038306a8 --- /dev/null +++ b/plutus-use-cases/src/Plutus/Contracts/Tutorial/Escrow.hs @@ -0,0 +1,321 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:debug-context #-} +{-# OPTIONS_GHC -g -fplugin-opt PlutusTx.Plugin:coverage-all #-} + +-- | A general-purpose escrow contract in Plutus +module Plutus.Contracts.Tutorial.Escrow( + -- $escrow + Escrow + , EscrowError(..) + , AsEscrowError(..) + , EscrowParams(..) + , EscrowTarget(..) + , payToPaymentPubKeyTarget + , targetTotal + , escrowContract + , typedValidator + -- * Actions + , pay + , payEp + , redeem + , redeemEp + , refund + , refundEp + , RedeemFailReason(..) + , RedeemSuccess(..) + , RefundSuccess(..) + , EscrowSchema + -- * Exposed for test endpoints + , Action(..) + -- * Coverage + , covIdx + ) where + +import Control.Lens (makeClassyPrisms, review, view) +import Control.Monad (void) +import Control.Monad.Error.Lens (throwing) +import Data.Aeson (FromJSON, ToJSON) +import GHC.Generics (Generic) + +import Ledger (Datum (..), DatumHash, PaymentPubKeyHash (unPaymentPubKeyHash), TxId, getCardanoTxId, txSignedBy, + valuePaidTo) +import Ledger qualified +import Ledger.Constraints (TxConstraints) +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts (ScriptContext (..), TxInfo (..)) +import Ledger.Tx qualified as Tx +import Ledger.Typed.Scripts (TypedValidator) +import Ledger.Typed.Scripts qualified as Scripts +import Ledger.Value (Value, geq, lt) + +import Plutus.Contract +import Plutus.Contract.Typed.Tx qualified as Typed +import PlutusTx qualified +import PlutusTx.Code +import PlutusTx.Coverage +import PlutusTx.Prelude hiding (Applicative (..), Semigroup (..), check, foldMap) + +import Prelude (Semigroup (..), foldMap) +import Prelude qualified as Haskell + +type EscrowSchema = + Endpoint "pay-escrow" Value + .\/ Endpoint "redeem-escrow" () + .\/ Endpoint "refund-escrow" () + +data RedeemFailReason = DeadlinePassed | NotEnoughFundsAtAddress + deriving stock (Haskell.Eq, Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +data EscrowError = + RedeemFailed RedeemFailReason + | RefundFailed + | EContractError ContractError + deriving stock (Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +makeClassyPrisms ''EscrowError + +instance AsContractError EscrowError where + _ContractError = _EContractError + +-- This is a simplified version of the Escrow contract, which does not +-- enforce a deadline on payments or redemption, and also allows +-- Refund actions at any time. + +-- $escrow +-- The escrow contract implements the exchange of value between multiple +-- parties. It is defined by a list of targets (public keys and script +-- addresses, each associated with a value). It works similar to the +-- crowdfunding contract in that the contributions can be made independently, +-- and the funds can be unlocked only by a transaction that pays the correct +-- amount to each target. A refund is possible if the outputs locked by the +-- contract have not been spent by the deadline. (Compared to the crowdfunding +-- contract, the refund policy is simpler because here because there is no +-- "collection period" during which the outputs may be spent after the deadline +-- has passed. This is because we're assuming that the participants in the +-- escrow contract will make their deposits as quickly as possible after +-- agreeing on a deal) +-- +-- The contract supports two modes of operation, manual and automatic. In +-- manual mode, all actions are driven by endpoints that exposed via 'payEp' +-- 'redeemEp' and 'refundEp'. In automatic mode, the 'pay', 'redeem' and +-- 'refund'actions start immediately. This mode is useful when the escrow is +-- called from within another contract, for example during setup (collection of +-- the initial deposits). + +-- | Defines where the money should go. Usually we have `d = Datum` (when +-- defining `EscrowTarget` values in off-chain code). Sometimes we have +-- `d = DatumHash` (when checking the hashes in on-chain code) +data EscrowTarget d = + PaymentPubKeyTarget PaymentPubKeyHash Value + deriving (Haskell.Functor) + +PlutusTx.makeLift ''EscrowTarget + +-- | An 'EscrowTarget' that pays the value to a public key address. +payToPaymentPubKeyTarget :: PaymentPubKeyHash -> Value -> EscrowTarget d +payToPaymentPubKeyTarget = PaymentPubKeyTarget + +-- | Definition of an escrow contract, consisting of a deadline and a list of targets +data EscrowParams d = + EscrowParams + { escrowTargets :: [EscrowTarget d] + -- ^ Where the money should go. For each target, the contract checks that + -- the output 'mkTxOutput' of the target is present in the spending + -- transaction. + } deriving (Haskell.Functor) + +PlutusTx.makeLift ''EscrowParams + +-- | The total 'Value' that must be paid into the escrow contract +-- before it can be unlocked +targetTotal :: EscrowParams d -> Value +targetTotal = foldl (\vl tgt -> vl + targetValue tgt) mempty . escrowTargets + +-- | The 'Value' specified by an 'EscrowTarget' +targetValue :: EscrowTarget d -> Value +targetValue = \case + PaymentPubKeyTarget _ vl -> vl + +-- | Create a 'Ledger.TxOut' value for the target +mkTx :: EscrowTarget Datum -> TxConstraints Action PaymentPubKeyHash +mkTx = \case + PaymentPubKeyTarget pkh vl -> + Constraints.mustPayToPubKey pkh vl + +data Action = Redeem | Refund + +data Escrow +instance Scripts.ValidatorTypes Escrow where + type instance RedeemerType Escrow = Action + type instance DatumType Escrow = PaymentPubKeyHash + +PlutusTx.unstableMakeIsData ''Action +PlutusTx.makeLift ''Action + +{-# INLINABLE meetsTarget #-} +-- | @ptx `meetsTarget` tgt@ if @ptx@ pays at least @targetValue tgt@ to the +-- target address. +-- +-- The reason why this does not require the target amount to be equal +-- to the actual amount is to enable any excess funds consumed by the +-- spending transaction to be paid to target addresses. This may happen if +-- the target address is also used as a change address for the spending +-- transaction, and allowing the target to be exceed prevents outsiders from +-- poisoning the contract by adding arbitrary outputs to the script address. +meetsTarget :: TxInfo -> EscrowTarget DatumHash -> Bool +meetsTarget ptx = \case + PaymentPubKeyTarget pkh vl -> + valuePaidTo ptx (unPaymentPubKeyHash pkh) `geq` vl + +{-# INLINABLE validate #-} +validate :: EscrowParams DatumHash -> PaymentPubKeyHash -> Action -> ScriptContext -> Bool +validate EscrowParams{escrowTargets} contributor action ScriptContext{scriptContextTxInfo} = + case action of + Redeem -> + traceIfFalse "meetsTarget" (all (meetsTarget scriptContextTxInfo) escrowTargets) + Refund -> + traceIfFalse "txSignedBy" (scriptContextTxInfo `txSignedBy` unPaymentPubKeyHash contributor) + +typedValidator :: EscrowParams Datum -> Scripts.TypedValidator Escrow +typedValidator escrow = go (Haskell.fmap Ledger.datumHash escrow) where + go = Scripts.mkTypedValidatorParam @Escrow + $$(PlutusTx.compile [|| validate ||]) + $$(PlutusTx.compile [|| wrap ||]) + wrap = Scripts.wrapValidator + +escrowContract + :: EscrowParams Datum + -> Contract () EscrowSchema EscrowError () +escrowContract escrow = + let inst = typedValidator escrow + payAndRefund = endpoint @"pay-escrow" $ \vl -> do + _ <- pay inst escrow vl + refund inst escrow + in selectList + [ void payAndRefund + , void $ redeemEp escrow + ] + +-- | 'pay' with an endpoint that gets the owner's public key and the +-- contribution. +payEp :: + forall w s e. + ( HasEndpoint "pay-escrow" Value s + , AsEscrowError e + ) + => EscrowParams Datum + -> Promise w s e TxId +payEp escrow = promiseMap + (mapError (review _EContractError)) + (endpoint @"pay-escrow" $ pay (typedValidator escrow) escrow) + +-- | Pay some money into the escrow contract. +pay :: + forall w s e. + ( AsContractError e + ) + => TypedValidator Escrow + -- ^ The instance + -> EscrowParams Datum + -- ^ The escrow contract + -> Value + -- ^ How much money to pay in + -> Contract w s e TxId +pay inst _escrow vl = do + pk <- ownPaymentPubKeyHash + let tx = Constraints.mustPayToTheScript pk vl + utx <- mkTxConstraints (Constraints.typedValidatorLookups inst) tx + getCardanoTxId <$> submitUnbalancedTx (Constraints.adjustUnbalancedTx utx) + +newtype RedeemSuccess = RedeemSuccess TxId + deriving (Haskell.Eq, Haskell.Show) + +-- | 'redeem' with an endpoint. +redeemEp :: + forall w s e. + ( HasEndpoint "redeem-escrow" () s + , AsEscrowError e + ) + => EscrowParams Datum + -> Promise w s e RedeemSuccess +redeemEp escrow = promiseMap + (mapError (review _EscrowError)) + (endpoint @"redeem-escrow" $ \() -> redeem (typedValidator escrow) escrow) + +-- | Redeem all outputs at the contract address using a transaction that +-- has all the outputs defined in the contract's list of targets. +redeem :: + forall w s e. + ( AsEscrowError e + ) + => TypedValidator Escrow + -> EscrowParams Datum + -> Contract w s e RedeemSuccess +redeem inst escrow = mapError (review _EscrowError) $ do + let addr = Scripts.validatorAddress inst + unspentOutputs <- utxosAt addr + let + tx = Typed.collectFromScript unspentOutputs Redeem + <> foldMap mkTx (escrowTargets escrow) + if foldMap (view Tx.ciTxOutValue) unspentOutputs `lt` targetTotal escrow + then throwing _RedeemFailed NotEnoughFundsAtAddress + else do + utx <- mkTxConstraints ( Constraints.typedValidatorLookups inst + <> Constraints.unspentOutputs unspentOutputs + ) tx + RedeemSuccess . getCardanoTxId <$> submitUnbalancedTx (Constraints.adjustUnbalancedTx utx) + +newtype RefundSuccess = RefundSuccess TxId + deriving newtype (Haskell.Eq, Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +-- | 'refund' with an endpoint. +refundEp :: + forall w s. + ( HasEndpoint "refund-escrow" () s + ) + => EscrowParams Datum + -> Promise w s EscrowError RefundSuccess +refundEp escrow = endpoint @"refund-escrow" $ \() -> refund (typedValidator escrow) escrow + +-- | Claim a refund of the contribution. +refund :: + forall w s. + TypedValidator Escrow + -> EscrowParams Datum + -> Contract w s EscrowError RefundSuccess +refund inst _escrow = do + pk <- ownPaymentPubKeyHash + unspentOutputs <- utxosAt (Scripts.validatorAddress inst) + let flt _ ciTxOut = either id Ledger.datumHash (Tx._ciTxOutDatum ciTxOut) == Ledger.datumHash (Datum (PlutusTx.toBuiltinData pk)) + tx' = Typed.collectFromScriptFilter flt unspentOutputs Refund + if Constraints.modifiesUtxoSet tx' + then do + utx <- mkTxConstraints ( Constraints.typedValidatorLookups inst + <> Constraints.unspentOutputs unspentOutputs + ) tx' + RefundSuccess . getCardanoTxId <$> submitUnbalancedTx (Constraints.adjustUnbalancedTx utx) + else throwing _RefundFailed () + +covIdx :: CoverageIndex +covIdx = getCovIdx $$(PlutusTx.compile [|| validate ||]) + <> getCovIdx $$(PlutusTx.compile [|| wrap ||]) + where + wrap :: (PaymentPubKeyHash -> Action -> ScriptContext -> Bool) -> + Scripts.WrappedValidatorType + wrap = Scripts.wrapValidator diff --git a/plutus-use-cases/src/Plutus/Contracts/Tutorial/EscrowStrict.hs b/plutus-use-cases/src/Plutus/Contracts/Tutorial/EscrowStrict.hs new file mode 100644 index 0000000000..8ba3c9a382 --- /dev/null +++ b/plutus-use-cases/src/Plutus/Contracts/Tutorial/EscrowStrict.hs @@ -0,0 +1,321 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:debug-context #-} +-- | A general-purpose escrow contract in Plutus +module Plutus.Contracts.Tutorial.EscrowStrict( + -- $escrow + Escrow + , EscrowError(..) + , AsEscrowError(..) + , EscrowParams(..) + , EscrowTarget(..) + , payToScriptTarget + , payToPaymentPubKeyTarget + , targetTotal + , escrowContract + , typedValidator + -- * Actions + , pay + , payEp + , redeem + , redeemEp + , refund + , refundEp + , RedeemFailReason(..) + , RedeemSuccess(..) + , RefundSuccess(..) + , EscrowSchema + -- * Exposed for test endpoints + , Action(..) + ) where + +import Control.Lens (makeClassyPrisms, review, view) +import Control.Monad (void) +import Control.Monad.Error.Lens (throwing) +import Data.Aeson (FromJSON, ToJSON) +import GHC.Generics (Generic) + +import Ledger (Datum (..), DatumHash, PaymentPubKeyHash (unPaymentPubKeyHash), TxId, ValidatorHash, getCardanoTxId, + scriptOutputsAt, txSignedBy, valuePaidTo) +import Ledger qualified +import Ledger.Constraints (TxConstraints) +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts (ScriptContext (..), TxInfo (..)) +import Ledger.Tx qualified as Tx +import Ledger.Typed.Scripts (TypedValidator) +import Ledger.Typed.Scripts qualified as Scripts +import Ledger.Value (Value, geq, lt) + +import Plutus.Contract +import Plutus.Contract.Typed.Tx qualified as Typed +import PlutusTx qualified +import PlutusTx.Prelude hiding (Applicative (..), Semigroup (..), check, foldMap) + +import Prelude (Semigroup (..), foldMap) +import Prelude qualified as Haskell + +type EscrowSchema = + Endpoint "pay-escrow" Value + .\/ Endpoint "redeem-escrow" () + .\/ Endpoint "refund-escrow" () + +data RedeemFailReason = DeadlinePassed | NotEnoughFundsAtAddress + deriving stock (Haskell.Eq, Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +data EscrowError = + RedeemFailed RedeemFailReason + | RefundFailed + | EContractError ContractError + deriving stock (Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +makeClassyPrisms ''EscrowError + +instance AsContractError EscrowError where + _ContractError = _EContractError + +-- This is a simplified version of the Escrow contract, which does not +-- enforce a deadline on payments or redemption, and also allows +-- Refund actions at any time. + +-- In addition, this version only allows redeem when the contract +-- contains *exactly* the right value. + +-- $escrow +-- The escrow contract implements the exchange of value between multiple +-- parties. It is defined by a list of targets (public keys and script +-- addresses, each associated with a value). It works similar to the +-- crowdfunding contract in that the contributions can be made independently, +-- and the funds can be unlocked only by a transaction that pays the correct +-- amount to each target. A refund is possible if the outputs locked by the +-- contract have not been spent by the deadline. (Compared to the crowdfunding +-- contract, the refund policy is simpler because here because there is no +-- "collection period" during which the outputs may be spent after the deadline +-- has passed. This is because we're assuming that the participants in the +-- escrow contract will make their deposits as quickly as possible after +-- agreeing on a deal) +-- +-- The contract supports two modes of operation, manual and automatic. In +-- manual mode, all actions are driven by endpoints that exposed via 'payEp' +-- 'redeemEp' and 'refundEp'. In automatic mode, the 'pay', 'redeem' and +-- 'refund'actions start immediately. This mode is useful when the escrow is +-- called from within another contract, for example during setup (collection of +-- the initial deposits). + +-- | Defines where the money should go. Usually we have `d = Datum` (when +-- defining `EscrowTarget` values in off-chain code). Sometimes we have +-- `d = DatumHash` (when checking the hashes in on-chain code) +data EscrowTarget d = + PaymentPubKeyTarget PaymentPubKeyHash Value + | ScriptTarget ValidatorHash d Value + deriving (Haskell.Functor) + +PlutusTx.makeLift ''EscrowTarget + +-- | An 'EscrowTarget' that pays the value to a public key address. +payToPaymentPubKeyTarget :: PaymentPubKeyHash -> Value -> EscrowTarget d +payToPaymentPubKeyTarget = PaymentPubKeyTarget + +-- | An 'EscrowTarget' that pays the value to a script address, with the +-- given data script. +payToScriptTarget :: ValidatorHash -> Datum -> Value -> EscrowTarget Datum +payToScriptTarget = ScriptTarget + +-- | Definition of an escrow contract, consisting of a deadline and a list of targets +data EscrowParams d = + EscrowParams + { escrowTargets :: [EscrowTarget d] + -- ^ Where the money should go. For each target, the contract checks that + -- the output 'mkTxOutput' of the target is present in the spending + -- transaction. + } deriving (Haskell.Functor) + +PlutusTx.makeLift ''EscrowParams + +-- | The total 'Value' that must be paid into the escrow contract +-- before it can be unlocked +targetTotal :: EscrowParams d -> Value +targetTotal = foldl (\vl tgt -> vl + targetValue tgt) mempty . escrowTargets + +-- | The 'Value' specified by an 'EscrowTarget' +targetValue :: EscrowTarget d -> Value +targetValue = \case + PaymentPubKeyTarget _ vl -> vl + ScriptTarget _ _ vl -> vl + +-- | Create a 'Ledger.TxOut' value for the target +mkTx :: EscrowTarget Datum -> TxConstraints Action PaymentPubKeyHash +mkTx = \case + PaymentPubKeyTarget pkh vl -> + Constraints.mustPayToPubKey pkh vl + ScriptTarget vs ds vl -> + Constraints.mustPayToOtherScript vs ds vl + +data Action = Redeem | Refund + +data Escrow +instance Scripts.ValidatorTypes Escrow where + type instance RedeemerType Escrow = Action + type instance DatumType Escrow = PaymentPubKeyHash + +PlutusTx.unstableMakeIsData ''Action +PlutusTx.makeLift ''Action + +{-# INLINABLE meetsTarget #-} +-- | @ptx `meetsTarget` tgt@ if @ptx@ pays exactly @targetValue tgt@ to the +-- target address. This is buggy behaviour: see Spec.Escrow for an explanation. +-- +meetsTarget :: TxInfo -> EscrowTarget DatumHash -> Bool +meetsTarget ptx = \case + PaymentPubKeyTarget pkh vl -> + valuePaidTo ptx (unPaymentPubKeyHash pkh) `geq` vl + ScriptTarget validatorHash dataValue vl -> + case scriptOutputsAt validatorHash ptx of + [(dataValue', vl')] -> + traceIfFalse "dataValue" (dataValue' == dataValue) + && traceIfFalse "value" (vl' == vl) + _ -> False + +{-# INLINABLE validate #-} +validate :: EscrowParams DatumHash -> PaymentPubKeyHash -> Action -> ScriptContext -> Bool +validate EscrowParams{escrowTargets} contributor action ScriptContext{scriptContextTxInfo} = + case action of + Redeem -> + traceIfFalse "meetsTarget" (all (meetsTarget scriptContextTxInfo) escrowTargets) + Refund -> + traceIfFalse "txSignedBy" (scriptContextTxInfo `txSignedBy` unPaymentPubKeyHash contributor) + +typedValidator :: EscrowParams Datum -> Scripts.TypedValidator Escrow +typedValidator escrow = go (Haskell.fmap Ledger.datumHash escrow) where + go = Scripts.mkTypedValidatorParam @Escrow + $$(PlutusTx.compile [|| validate ||]) + $$(PlutusTx.compile [|| wrap ||]) + wrap = Scripts.wrapValidator + +escrowContract + :: EscrowParams Datum + -> Contract () EscrowSchema EscrowError () +escrowContract escrow = + let inst = typedValidator escrow + payAndRefund = endpoint @"pay-escrow" $ \vl -> do + _ <- pay inst escrow vl + refund inst escrow + in selectList + [ void payAndRefund + , void $ redeemEp escrow + ] + +-- | 'pay' with an endpoint that gets the owner's public key and the +-- contribution. +payEp :: + forall w s e. + ( HasEndpoint "pay-escrow" Value s + , AsEscrowError e + ) + => EscrowParams Datum + -> Promise w s e TxId +payEp escrow = promiseMap + (mapError (review _EContractError)) + (endpoint @"pay-escrow" $ pay (typedValidator escrow) escrow) + +-- | Pay some money into the escrow contract. +pay :: + forall w s e. + ( AsContractError e + ) + => TypedValidator Escrow + -- ^ The instance + -> EscrowParams Datum + -- ^ The escrow contract + -> Value + -- ^ How much money to pay in + -> Contract w s e TxId +pay inst _escrow vl = do + pk <- ownPaymentPubKeyHash + let tx = Constraints.mustPayToTheScript pk vl + utx <- mkTxConstraints (Constraints.typedValidatorLookups inst) tx + getCardanoTxId <$> submitUnbalancedTx (Constraints.adjustUnbalancedTx utx) + +newtype RedeemSuccess = RedeemSuccess TxId + deriving (Haskell.Eq, Haskell.Show) + +-- | 'redeem' with an endpoint. +redeemEp :: + forall w s e. + ( HasEndpoint "redeem-escrow" () s + , AsEscrowError e + ) + => EscrowParams Datum + -> Promise w s e RedeemSuccess +redeemEp escrow = promiseMap + (mapError (review _EscrowError)) + (endpoint @"redeem-escrow" $ \() -> redeem (typedValidator escrow) escrow) + +-- | Redeem all outputs at the contract address using a transaction that +-- has all the outputs defined in the contract's list of targets. +redeem :: + forall w s e. + ( AsEscrowError e + ) + => TypedValidator Escrow + -> EscrowParams Datum + -> Contract w s e RedeemSuccess +redeem inst escrow = mapError (review _EscrowError) $ do + let addr = Scripts.validatorAddress inst + unspentOutputs <- utxosAt addr + let + tx = Typed.collectFromScript unspentOutputs Redeem + <> foldMap mkTx (escrowTargets escrow) + if foldMap (view Tx.ciTxOutValue) unspentOutputs `lt` targetTotal escrow + then throwing _RedeemFailed NotEnoughFundsAtAddress + else do + utx <- mkTxConstraints ( Constraints.typedValidatorLookups inst + <> Constraints.unspentOutputs unspentOutputs + ) tx + RedeemSuccess . getCardanoTxId <$> submitUnbalancedTx (Constraints.adjustUnbalancedTx utx) + +newtype RefundSuccess = RefundSuccess TxId + deriving newtype (Haskell.Eq, Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +-- | 'refund' with an endpoint. +refundEp :: + forall w s. + ( HasEndpoint "refund-escrow" () s + ) + => EscrowParams Datum + -> Promise w s EscrowError RefundSuccess +refundEp escrow = endpoint @"refund-escrow" $ \() -> refund (typedValidator escrow) escrow + +-- | Claim a refund of the contribution. +refund :: + forall w s. + TypedValidator Escrow + -> EscrowParams Datum + -> Contract w s EscrowError RefundSuccess +refund inst _escrow = do + pk <- ownPaymentPubKeyHash + unspentOutputs <- utxosAt (Scripts.validatorAddress inst) + let flt _ ciTxOut = either id Ledger.datumHash (Tx._ciTxOutDatum ciTxOut) == Ledger.datumHash (Datum (PlutusTx.toBuiltinData pk)) + tx' = Typed.collectFromScriptFilter flt unspentOutputs Refund + if Constraints.modifiesUtxoSet tx' + then do + utx <- mkTxConstraints ( Constraints.typedValidatorLookups inst + <> Constraints.unspentOutputs unspentOutputs + ) tx' + RefundSuccess . getCardanoTxId <$> submitUnbalancedTx (Constraints.adjustUnbalancedTx utx) + else throwing _RefundFailed () + diff --git a/plutus-use-cases/src/Plutus/Contracts/Uniswap/OffChain.hs b/plutus-use-cases/src/Plutus/Contracts/Uniswap/OffChain.hs index 650c788a61..577da6789a 100644 --- a/plutus-use-cases/src/Plutus/Contracts/Uniswap/OffChain.hs +++ b/plutus-use-cases/src/Plutus/Contracts/Uniswap/OffChain.hs @@ -29,6 +29,10 @@ module Plutus.Contracts.Uniswap.OffChain , start, create, add, remove, close, swap, pools , ownerEndpoint, userEndpoints , findSwapA, findSwapB, covIdx + -- exported for defining test endpoints + , findUniswapFactoryAndPool, uniswapInstance, liquidityPolicy + , uniswapScript, poolStateCoin, liquidityCurrency, lpTicker + , calculateRemoval, funds ) where import Control.Lens (view) diff --git a/plutus-use-cases/test/Spec/Auction.hs b/plutus-use-cases/test/Spec/Auction.hs index bb771a677f..7fe8a10d15 100644 --- a/plutus-use-cases/test/Spec/Auction.hs +++ b/plutus-use-cases/test/Spec/Auction.hs @@ -17,6 +17,11 @@ module Spec.Auction , prop_Auction , prop_FinishAuction , prop_NoLockedFunds + , prop_NoLockedFundsFast + , prop_SanityCheckAssertions + , prop_Whitelist + , prop_CrashTolerance + , check_propAuctionWithCoverage ) where import Control.Lens hiding (elements) @@ -41,6 +46,8 @@ import Ledger qualified import Ledger.TimeSlot (SlotConfig) import Ledger.TimeSlot qualified as TimeSlot import Plutus.Contract.Test.ContractModel +import Plutus.Contract.Test.ContractModel.CrashTolerance +import Plutus.Contract.Test.Coverage import Plutus.Contracts.Auction hiding (Bid) import Plutus.Trace.Emulator qualified as Trace import PlutusTx.Monoid (inv) @@ -177,7 +184,7 @@ instance ContractModel AuctionModel where SellerH :: ContractInstanceKey AuctionModel AuctionOutput SellerSchema AuctionError () BuyerH :: Wallet -> ContractInstanceKey AuctionModel AuctionOutput BuyerSchema AuctionError () - data Action AuctionModel = Init Wallet | Bid Wallet Integer + data Action AuctionModel = Init | Bid Wallet Integer deriving (Eq, Show, Data) initialState = AuctionModel @@ -187,7 +194,10 @@ instance ContractModel AuctionModel where , _phase = NotStarted } - initialInstances = StartContract SellerH () : [ StartContract (BuyerH w) () | w <- [w2, w3, w4] ] + initialInstances = [ StartContract (BuyerH w) () | w <- [w2, w3, w4] ] + + startInstances _ Init = [StartContract SellerH ()] + startInstances _ _ = [] instanceWallet SellerH = w1 instanceWallet (BuyerH w) = w @@ -197,27 +207,22 @@ instance ContractModel AuctionModel where arbitraryAction s | p /= NotStarted = do - oneof [ Bid w <$> chooseBid (lo,hi) - | w <- [w2, w3, w4] - , let (lo,hi) = validBidRange s w - , lo <= hi ] - | otherwise = pure $ Init w1 + oneof [ Bid w <$> validBid + | w <- [w2, w3, w4] ] + | otherwise = pure $ Init where p = s ^. contractState . phase - - waitProbability s - | s ^. contractState . phase /= NotStarted - , all (uncurry (>) . validBidRange s) [w2, w3, w4] = 1 - | otherwise = 0.1 - - precondition s (Init _) = s ^. contractState . phase == NotStarted - precondition s cmd = s ^. contractState . phase /= NotStarted && - case cmd of - -- In order to place a bid, we need to satisfy the constraint where - -- each tx output must have at least N Ada. - Bid w bid -> let (lo,hi) = validBidRange s w in - lo <= bid && bid <= hi - _ -> True + b = s ^. contractState . currentBid + validBid = choose ((b+1) `max` Ada.getLovelace Ledger.minAdaTxOut, + b + Ada.getLovelace (Ada.adaOf 100)) + + precondition s Init = s ^. contractState . phase == NotStarted + precondition s (Bid _ bid) = + -- In order to place a bid, we need to satisfy the constraint where + -- each tx output must have at least N Ada. + s ^. contractState . phase /= NotStarted && + bid >= Ada.getLovelace (Ledger.minAdaTxOut) && + bid > s ^. contractState . currentBid nextReactiveState slot' = do end <- viewContractState endSlot @@ -228,27 +233,31 @@ instance ContractModel AuctionModel where phase .= AuctionOver deposit w $ Ada.toValue Ledger.minAdaTxOut <> theToken deposit w1 $ Ada.lovelaceValueOf bid + {- + w1change <- viewModelState $ balanceChange w1 -- since the start of the test + assertSpec ("w1 final balance is wrong:\n "++show w1change) $ + w1change == toSymValue (inv theToken <> Ada.lovelaceValueOf bid) || + w1change == mempty + -} - -- This command is only for setting up the model state with theToken nextState cmd = do - slot <- viewModelState currentSlot - end <- viewContractState endSlot case cmd of - Init _ -> do + Init -> do phase .= Bidding withdraw w1 $ Ada.toValue Ledger.minAdaTxOut <> theToken wait 3 Bid w bid -> do - current <- viewContractState currentBid - leader <- viewContractState winner - when (slot < end) $ do + currentPhase <- viewContractState phase + when (currentPhase == Bidding) $ do + current <- viewContractState currentBid + leader <- viewContractState winner withdraw w $ Ada.lovelaceValueOf bid deposit leader $ Ada.lovelaceValueOf current currentBid .= bid winner .= w wait 2 - perform _ _ _ (Init _) = delay 3 + perform _ _ _ Init = delay 3 perform handle _ _ (Bid w bid) = do -- FIXME: You cannot bid in certain slots when the off-chain code is busy, so to make the -- tests pass we send two identical bids in consecutive slots. The off-chain code is @@ -260,41 +269,9 @@ instance ContractModel AuctionModel where Trace.callEndpoint @"bid" (handle $ BuyerH w) (Ada.lovelaceOf bid) delay 1 - shrinkAction _ (Init _) = [] + shrinkAction _ Init = [] shrinkAction _ (Bid w v) = [ Bid w v' | v' <- shrink v ] - monitoring _ (Bid _ bid) = - classify (Ada.lovelaceOf bid == Ada.adaOf 100 - (Ledger.minAdaTxOut <> Ledger.maxFee)) - "Maximum bid reached" - monitoring _ _ = id - --- In order to place a bid, we need to satisfy the constraint where --- each tx output must have at least N Ada. --- --- When we bid, we must make sure that we don't bid too high such --- that: --- - we can't pay for fees anymore --- - we have a tx output of less than N Ada. --- --- We suppose the initial balance is 100 Ada. Needs to be changed if --- the emulator initialises the wallets with a different value. -validBidRange :: ModelState AuctionModel -> Wallet -> (Integer,Integer) -validBidRange s _w = - let currentWalletBalance = Ada.adaOf 100 -- this is approximate - current = s ^. contractState . currentBid - in ( (current+1) `max` Ada.getLovelace Ledger.minAdaTxOut, - Ada.getLovelace (currentWalletBalance - (Ledger.minAdaTxOut <> Ledger.maxFee)) - ) - --- When we choose a bid, we prefer a lower bid to a higher --- one. Otherwise longer tests very often reach the maximum possible --- bid, which makes little sense. -chooseBid :: (Integer,Integer) -> Gen Integer -chooseBid (lo,hi) - | lo==hi = pure lo - | lo 0) (iterate (`div` 400) (hi-lo))] - | otherwise = error $ "chooseBid "++show (lo,hi) - prop_Auction :: Actions AuctionModel -> Property prop_Auction script = propRunActionsWithOptions (set minLogLevel Info options) defaultCoverageOptions @@ -303,12 +280,16 @@ prop_Auction script = finishAuction :: DL AuctionModel () finishAuction = do - action $ Init w1 anyActions_ - slot <- viewModelState currentSlot - when (slot < 101) $ waitUntilDL 101 + finishingStrategy assertModel "Locked funds are not zero" (symIsZero . lockedValue) +finishingStrategy :: DL AuctionModel () +finishingStrategy = do + slot <- viewModelState currentSlot + end <- viewContractState endSlot + when (slot < end) $ waitUntilDL end + prop_FinishAuction :: Property prop_FinishAuction = forAllDL finishAuction prop_Auction @@ -317,17 +298,40 @@ prop_FinishAuction = forAllDL finishAuction prop_Auction -- and building a Payout transaction manually). noLockProof :: NoLockedFundsProof AuctionModel noLockProof = defaultNLFP - { nlfpMainStrategy = strat - , nlfpWalletStrategy = const strat } - where - strat = do - p <- viewContractState phase - when (p == NotStarted) $ action $ Init w1 - slot <- viewModelState currentSlot - when (slot < 101) $ waitUntilDL 101 + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = const finishingStrategy } prop_NoLockedFunds :: Property -prop_NoLockedFunds = checkNoLockedFundsProof (set minLogLevel Critical options) noLockProof +prop_NoLockedFunds = checkNoLockedFundsProofWithOptions (set minLogLevel Critical options) noLockProof + +prop_NoLockedFundsFast :: Property +prop_NoLockedFundsFast = checkNoLockedFundsProofFast noLockProof + +prop_SanityCheckAssertions :: Actions AuctionModel -> Property +prop_SanityCheckAssertions = propSanityCheckAssertions + +prop_Whitelist :: Actions AuctionModel -> Property +prop_Whitelist = checkErrorWhitelist defaultWhitelist + +instance CrashTolerance AuctionModel where + available (Bid w _) alive = (Key $ BuyerH w) `elem` alive + available Init _ = True + + restartArguments _ BuyerH{} = () + restartArguments _ SellerH{} = () + +prop_CrashTolerance :: Actions (WithCrashTolerance AuctionModel) -> Property +prop_CrashTolerance = + propRunActionsWithOptions (set minLogLevel Critical options) defaultCoverageOptions + (\ _ -> pure True) + +check_propAuctionWithCoverage :: IO () +check_propAuctionWithCoverage = do + cr <- quickCheckWithCoverage stdArgs (set coverageIndex covIdx $ defaultCoverageOptions) $ \covopts -> + withMaxSuccess 1000 $ + propRunActionsWithOptions @AuctionModel + (set minLogLevel Critical options) covopts (const (pure True)) + writeCoverageReport "Auction" covIdx cr tests :: TestTree tests = diff --git a/plutus-use-cases/test/Spec/Escrow.hs b/plutus-use-cases/test/Spec/Escrow.hs index afd720b3f5..4ff248d0b2 100644 --- a/plutus-use-cases/test/Spec/Escrow.hs +++ b/plutus-use-cases/test/Spec/Escrow.hs @@ -166,7 +166,6 @@ finishingStrategy walletAlive = do slot <- viewContractState refundSlot when (now < slot+1) $ waitUntilDL $ slot+1 contribs <- viewContractState contributions - monitor (classify (Map.null contribs) "no need for extra refund to recover funds") sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs, walletAlive w] prop_FinishEscrow :: Property @@ -178,7 +177,7 @@ noLockProof = defaultNLFP , nlfpWalletStrategy = finishingStrategy . (==) } prop_NoLockedFunds :: Property -prop_NoLockedFunds = checkNoLockedFundsProof defaultCheckOptionsContractModel noLockProof +prop_NoLockedFunds = checkNoLockedFundsProof noLockProof tests :: TestTree diff --git a/plutus-use-cases/test/Spec/GameStateMachine.hs b/plutus-use-cases/test/Spec/GameStateMachine.hs index 929465c660..a987500475 100644 --- a/plutus-use-cases/test/Spec/GameStateMachine.hs +++ b/plutus-use-cases/test/Spec/GameStateMachine.hs @@ -27,6 +27,7 @@ module Spec.GameStateMachine , prop_SanityCheckAssertions , prop_GameCrashTolerance , certification + , covIndex ) where import Control.Exception hiding (handle) @@ -152,12 +153,15 @@ instance ContractModel GameModel where -- To generate a random test case we need to know how to generate a random -- command given the current model state. arbitraryAction s = oneof $ - [ genLockAction ] ++ + [ genLockAction | Nothing <- [tok] ] ++ [ Guess w <$> genGuess <*> genGuess <*> genGuessAmount - | val > Ada.getLovelace Ledger.minAdaTxOut, Just w <- [tok] ] ++ + | val > minOut, Just w <- [tok] ] ++ [ GiveToken <$> genWallet | isJust tok ] where - genGuessAmount = frequency [(1, pure val), (1, pure $ Ada.getLovelace Ledger.minAdaTxOut), (8, choose (Ada.getLovelace Ledger.minAdaTxOut, val))] + genGuessAmount = frequency $ [(1, pure val)] ++ + [(1, pure $ minOut) | 2*minOut <= val] ++ + [(8, choose (minOut, val-minOut)) | minOut <= val-minOut] + minOut = Ada.getLovelace Ledger.minAdaTxOut tok = s ^. contractState . hasToken val = s ^. contractState . gameValue genLockAction :: Gen (Action GameModel) @@ -208,12 +212,16 @@ prop_SanityCheckModel = propSanityCheckModel @GameModel prop_SanityCheckAssertions :: Actions GameModel -> Property prop_SanityCheckAssertions = propSanityCheckAssertions -check_prop_Game_with_coverage :: IO CoverageReport -check_prop_Game_with_coverage = - quickCheckWithCoverage stdArgs (set coverageIndex (covIdx gameParam) defaultCoverageOptions) $ \covopts -> +check_prop_Game_with_coverage :: IO () +check_prop_Game_with_coverage = do + cr <- quickCheckWithCoverage stdArgs (set coverageIndex covIndex defaultCoverageOptions) $ \covopts -> propRunActionsWithOptions @GameModel defaultCheckOptionsContractModel covopts (const (pure True)) + writeCoverageReport "GameStateMachine" covIndex cr + +covIndex :: CoverageIndex +covIndex = covIdx gameParam propGame' :: LogLevel -> Actions GameModel -> Property propGame' l = propRunActionsWithOptions @@ -299,7 +307,7 @@ noLockProof = defaultNLFP { when hasTok $ action (Guess w secret "" val) prop_CheckNoLockedFundsProof :: Property -prop_CheckNoLockedFundsProof = checkNoLockedFundsProof defaultCheckOptionsContractModel noLockProof +prop_CheckNoLockedFundsProof = checkNoLockedFundsProof noLockProof -- * Unit tests @@ -458,8 +466,7 @@ certification = defaultCertification { certNoLockedFunds = Just noLockProof, certUnitTests = Just unitTest, certCoverageIndex = covIdx gameParam, - certCrashTolerance = Just Instance, - certWhitelist = Just defaultWhitelist + certCrashTolerance = Just Instance } where unitTest ref = diff --git a/plutus-use-cases/test/Spec/Prism.hs b/plutus-use-cases/test/Spec/Prism.hs index adb357cca9..eaea9f9eed 100644 --- a/plutus-use-cases/test/Spec/Prism.hs +++ b/plutus-use-cases/test/Spec/Prism.hs @@ -22,6 +22,7 @@ import Control.Monad import Data.Data import Data.Map (Map) import Data.Map qualified as Map +import Ledger (minAdaTxOut) import Ledger.Ada qualified as Ada import Ledger.Value (TokenName) import Plutus.Contract.Test hiding (not) @@ -162,9 +163,13 @@ instance ContractModel PrismModel where nextState cmd = do wait waitSlots case cmd of - Revoke w -> isIssued w %= doRevoke + Revoke w -> do + issued <- use (isIssued w) + when (issued == Issued) $ deposit mirror minAdaTxOut + isIssued w %= doRevoke Issue w -> do wait 1 + withdraw mirror minAdaTxOut isIssued w .= Issued Call w -> do iss <- (== Issued) <$> viewContractState (isIssued w) @@ -201,7 +206,7 @@ noLockProof :: NoLockedFundsProof PrismModel noLockProof = defaultNLFP prop_NoLock :: Property -prop_NoLock = checkNoLockedFundsProof defaultCheckOptionsContractModel noLockProof +prop_NoLock = checkNoLockedFundsProof noLockProof tests :: TestTree tests = testGroup "PRISM" diff --git a/plutus-use-cases/test/Spec/SealedBidAuction.hs b/plutus-use-cases/test/Spec/SealedBidAuction.hs index 7aca4d4219..654981fd28 100644 --- a/plutus-use-cases/test/Spec/SealedBidAuction.hs +++ b/plutus-use-cases/test/Spec/SealedBidAuction.hs @@ -243,7 +243,7 @@ noLockProof = defaultNLFP , nlfpWalletStrategy = finishingStrategy } prop_NoLockedFunds :: Property -prop_NoLockedFunds = checkNoLockedFundsProof options noLockProof +prop_NoLockedFunds = checkNoLockedFundsProofWithOptions options noLockProof tests :: TestTree tests = diff --git a/plutus-use-cases/test/Spec/Tutorial/Escrow.hs b/plutus-use-cases/test/Spec/Tutorial/Escrow.hs new file mode 100644 index 0000000000..e4feba20cc --- /dev/null +++ b/plutus-use-cases/test/Spec/Tutorial/Escrow.hs @@ -0,0 +1,244 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} +module Spec.Tutorial.Escrow(tests, prop_Escrow, + prop_FinishEscrow, prop_NoLockedFunds, + prop_CrashTolerance, prop_Whitelist, + check_propEscrowWithCoverage, + EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void, when) +import Data.Data +import Data.Foldable +import Data.Function +import Data.List (sortBy) +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.Value +import Plutus.Contract hiding (currentSlot) +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel +import Plutus.Contract.Test.ContractModel.CrashTolerance +import Plutus.Contract.Test.Coverage + +import Plutus.Contracts.Tutorial.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck as QC hiding ((.&&.)) +import Test.Tasty +import Test.Tasty.QuickCheck hiding ((.&&.)) + +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _phase :: Phase + } deriving (Eq, Show, Data) + +data Phase = Initial | Running deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where + data Action EscrowModel = Init [(Wallet, Integer)] + | Redeem Wallet + | Pay Wallet Integer + | Refund Wallet + deriving (Eq, Show, Data) + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _phase = Initial + } +{- , _targets = Map.fromList [ (w1, Ada.adaValueOf 10) + , (w2, Ada.adaValueOf 20) + ] + } +-} + + --initialInstances = [StartContract (WalletKey w) () | w <- testWallets] + initialInstances = [] + + startInstances _ (Init wns) = + [StartContract (WalletKey w) (escrowParams wns) | w <- testWallets] + startInstances _ _ = [] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} params = testContract + where + testContract = selectList [ void $ payEp params + , void $ redeemEp params + , void $ refundEp params + ] >> testContract + + nextState a = case a of + Init wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + -- omit next two lines to disable disbursement of the surplus + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + Refund w -> do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 + + + + precondition s a = case a of + Init tgts-> currentPhase == Initial + && and [Ada.adaValueOf (fromInteger n) `geq` Ada.toValue minAdaTxOut | (_,n) <- tgts] +-- && and [Ada.adaValueOf (fromInteger n) `gt` Ada.toValue 0 | (_,n) <- tgts] + Redeem _ -> currentPhase == Running + && (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) +-- && (s ^. contractState . contributions . to fold) == (s ^. contractState . targets . to fold) + Refund w -> Nothing /= (s ^. contractState . contributions . at w) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + -- disallow payments that take us over the targets + -- && ((s ^. contractState . contributions . to fold) <> Ada.adaValueOf (fromInteger v)) `leq` (s ^. contractState . targets . to fold) + where currentPhase = s ^. contractState . phase + + perform h _ _ a = case a of + Init _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + Refund w -> do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 + + + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> arbitraryTargets + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] + ++ + [ (1, Refund <$> elements (s ^. contractState . contributions . to Map.keys)) + | Prelude.not . null $ s ^. contractState . contributions . to Map.keys ] + + + shrinkAction _ (Init tgts) = map Init (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +{- + monitoring _ (Redeem _) = classify True "Contains Redeem" + monitoring (s,s') _ = classify (redeemable s' && Prelude.not (redeemable s)) "Redeemable" + where redeemable s = precondition s (Redeem undefined) +-} + +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] + +walletStrategy :: Wallet -> DL EscrowModel () +walletStrategy w = do + contribs <- viewContractState contributions + when (w `Map.member` contribs) $ action $ Refund w + +prop_FinishEscrow :: Property +prop_FinishEscrow = forAllDL finishEscrow prop_Escrow + +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = walletStrategy } + +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProof noLockProof + +instance CrashTolerance EscrowModel where + available (Init _) _ = True + available a alive = (Key $ WalletKey w) `elem` alive + where w = case a of + Pay w _ -> w + Redeem w -> w + Refund w -> w + _ -> error "This case is unreachable" + + restartArguments s WalletKey{} = escrowParams' $ Map.toList (s ^. contractState . targets) + +prop_CrashTolerance :: Actions (WithCrashTolerance EscrowModel) -> Property +prop_CrashTolerance = propRunActions_ + +prop_Whitelist :: Actions EscrowModel -> Property +prop_Whitelist = checkErrorWhitelist defaultWhitelist + +tests :: TestTree +tests = testGroup "escrow" + [ testProperty "QuickCheck ContractModel" $ withMaxSuccess 10 prop_Escrow + , testProperty "QuickCheck NoLockedFunds" $ withMaxSuccess 10 prop_NoLockedFunds + ] + +escrowParams :: [(Wallet, Integer)] -> EscrowParams d +escrowParams tgts = escrowParams' [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- tgts] + +escrowParams' :: [(Wallet,Value)] -> EscrowParams d +escrowParams' tgts' = + EscrowParams + { escrowTargets = [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) v + | (w,v) <- sortBy (compare `on` fst) tgts' ] } + +check_propEscrowWithCoverage :: IO () +check_propEscrowWithCoverage = do + cr <- quickCheckWithCoverage stdArgs (set coverageIndex covIdx $ defaultCoverageOptions) $ \covopts -> + withMaxSuccess 1000 $ propRunActionsWithOptions @EscrowModel defaultCheckOptionsContractModel + covopts (const (pure True)) + writeCoverageReport "Escrow" covIdx cr diff --git a/plutus-use-cases/test/Spec/Tutorial/Escrow1.hs b/plutus-use-cases/test/Spec/Tutorial/Escrow1.hs new file mode 100644 index 0000000000..0a509a36d4 --- /dev/null +++ b/plutus-use-cases/test/Spec/Tutorial/Escrow1.hs @@ -0,0 +1,119 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in +-- Plutus.Contracts.Tutorial.Escrow. The code is explained in the +-- 'Basic Contract Models' section of the contract model tutorial. + +module Spec.Tutorial.Escrow1(prop_Escrow, EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void) +import Data.Data +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.Value +import Plutus.Contract +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel + +import Plutus.Contracts.Tutorial.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + } deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where + data Action EscrowModel = Pay Wallet Integer + | Redeem Wallet + deriving (Eq, Show, Data) + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError () + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.fromList [ (w1, Ada.adaValueOf 10) + , (w2, Ada.adaValueOf 20) + ] + } + + initialInstances = [StartContract (WalletKey w) () | w <- testWallets] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} _ = testContract + where + testContract = selectList [ void $ payEp escrowParams + , void $ redeemEp escrowParams + ] >> testContract + + nextState a = case a of + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + -- omit next two lines to disable disbursement of the surplus + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + + precondition s a = case a of + Redeem _ -> (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + Pay _ v -> Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + + perform h _ _ a = case a of + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + + arbitraryAction s = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] + + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + +escrowParams :: EscrowParams d +escrowParams = + EscrowParams + { escrowTargets = + [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w1) (Ada.adaValueOf 10) + , payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w2) (Ada.adaValueOf 20) + ] + } diff --git a/plutus-use-cases/test/Spec/Tutorial/Escrow2.hs b/plutus-use-cases/test/Spec/Tutorial/Escrow2.hs new file mode 100644 index 0000000000..11d393838b --- /dev/null +++ b/plutus-use-cases/test/Spec/Tutorial/Escrow2.hs @@ -0,0 +1,151 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in Plutus.Contracts.Tutorial.Escrow, +-- with generated escrow targets. See the "Parameterising Models and +-- Dynamic Contract Instances" section of the tutorial. + +module Spec.Tutorial.Escrow2(prop_Escrow, EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void) +import Data.Data +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.Value +import Plutus.Contract +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel + +import Plutus.Contracts.Tutorial.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _phase :: Phase + } deriving (Eq, Show, Data) + +data Phase = Initial | Running deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where + data Action EscrowModel = Init [(Wallet, Integer)] + | Redeem Wallet + | Pay Wallet Integer + deriving (Eq, Show, Data) + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _phase = Initial + } + + initialInstances = [] + + startInstances _ (Init wns) = + [StartContract (WalletKey w) (escrowParams wns) | w <- testWallets] + startInstances _ _ = [] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} params = testContract params + + nextState a = case a of + Init wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + -- omit next two lines to disable disbursement of the surplus + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + + precondition s a = case a of + Init _ -> currentPhase == Initial + Redeem _ -> currentPhase == Running + && (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + where currentPhase = s ^. contractState . phase + + + perform h _ _ a = case a of + Init _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> arbitraryTargets + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] + + shrinkAction _ (Init tgts) = map Init (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +testContract :: EscrowParams Datum -> Contract () EscrowSchema EscrowError () +testContract params = selectList [ void $ payEp params + , void $ redeemEp params + ] >> testContract params + + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + + +escrowParams :: [(Wallet, Integer)] -> EscrowParams d +escrowParams tgts = + EscrowParams + { escrowTargets = + [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) (Ada.adaValueOf (fromInteger n)) + | (w,n) <- tgts + ] + } diff --git a/plutus-use-cases/test/Spec/Tutorial/Escrow3.hs b/plutus-use-cases/test/Spec/Tutorial/Escrow3.hs new file mode 100644 index 0000000000..c89dd302a3 --- /dev/null +++ b/plutus-use-cases/test/Spec/Tutorial/Escrow3.hs @@ -0,0 +1,227 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in Plutus.Contracts.Tutorial.Escrow, +-- with generated escrow targets. See the "Parameterising Models and +-- Dynamic Contract Instances" section of the tutorial. + +module Spec.Tutorial.Escrow3(prop_Escrow, prop_FinishEscrow, prop_NoLockedFunds, prop_FixedTargets, EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void, when) +import Data.Data +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.Value +import Plutus.Contract +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel + +import Plutus.Contracts.Tutorial.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _phase :: Phase + } deriving (Eq, Show, Data) + +data Phase = Initial | Running deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where + data Action EscrowModel = Init [(Wallet, Integer)] + | Redeem Wallet + | Pay Wallet Integer + | Refund Wallet + deriving (Eq, Show, Data) + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _phase = Initial + } + + initialInstances = [] + + startInstances _ (Init wns) = + [StartContract (WalletKey w) (escrowParams wns) | w <- testWallets] + startInstances _ _ = [] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} params = testContract params + + nextState a = case a of + Init wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + Refund w -> do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 + + precondition s a = case a of + Init tgts -> currentPhase == Initial + && and [Ada.adaValueOf (fromInteger n) `geq` Ada.toValue minAdaTxOut | (_,n) <- tgts] + Redeem _ -> currentPhase == Running + && fold (s ^. contractState . contributions) `geq` fold (s ^. contractState . targets) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + Refund w -> currentPhase == Running + && w `Map.member` (s ^. contractState . contributions) + where currentPhase = s ^. contractState . phase + + + perform h _ _ a = case a of + Init _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + Refund w -> do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 + + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> arbitraryTargets + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] ++ + [ (1, Refund <$> elements testWallets) ] + + + shrinkAction _ (Init tgts) = map Init (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +testContract :: EscrowParams Datum -> Contract () EscrowSchema EscrowError () +testContract params = selectList [ void $ payEp params + , void $ redeemEp params + , void $ refundEp params + ] >> testContract params + + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + + +escrowParams :: [(Wallet, Integer)] -> EscrowParams d +escrowParams tgts = + EscrowParams + { escrowTargets = + [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) (Ada.adaValueOf (fromInteger n)) + | (w,n) <- tgts + ] + } + +-- This is the first--bad--approach to recovering locked funds. +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy w1 + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +finishingStrategy :: Wallet -> DL EscrowModel () +finishingStrategy w = do + currentPhase <- viewContractState phase + when (currentPhase /= Initial) $ do + currentTargets <- viewContractState targets + currentContribs <- viewContractState contributions + let deficit = fold currentTargets <> inv (fold currentContribs) + when (deficit `gt` Ada.adaValueOf 0) $ + action $ Pay w $ round $ Ada.getAda $ max minAdaTxOut $ Ada.fromValue deficit + action $ Redeem w + +-- This unilateral strategy fails. +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy w1 + , nlfpWalletStrategy = finishingStrategy } + +{- this is the better strategy based on refunds +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] + +walletStrategy :: Wallet -> DL EscrowModel () +walletStrategy w = do + contribs <- viewContractState contributions + when (w `Map.member` contribs) $ action $ Refund w + +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = walletStrategy } +-} + +prop_FinishEscrow :: Property +prop_FinishEscrow = forAllDL finishEscrow prop_Escrow + +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProof noLockProof + +fixedTargets :: DL EscrowModel () +fixedTargets = do + action $ Init [(w1,10),(w2,20)] + anyActions_ + +prop_FixedTargets :: Property +prop_FixedTargets = forAllDL fixedTargets prop_Escrow diff --git a/plutus-use-cases/test/Spec/Tutorial/Escrow4.hs b/plutus-use-cases/test/Spec/Tutorial/Escrow4.hs new file mode 100644 index 0000000000..015633c509 --- /dev/null +++ b/plutus-use-cases/test/Spec/Tutorial/Escrow4.hs @@ -0,0 +1,225 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in Plutus.Contracts.Tutorial.Escrow, +-- with generated escrow targets. See the "Parameterising Models and +-- Dynamic Contract Instances" section of the tutorial. + +module Spec.Tutorial.Escrow4(prop_Escrow, prop_FinishEscrow, prop_FinishFast, prop_NoLockedFunds, prop_NoLockedFundsFast, EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void, when) +import Data.Data +import Data.Default +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, Slot (..), minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.TimeSlot (SlotConfig (..)) +import Ledger.Value (Value, geq) +import Plutus.Contract (Contract, selectList) +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel +import Plutus.V1.Ledger.Time + +import Plutus.Contracts.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _refundSlot :: Slot + , _phase :: Phase + } deriving (Eq, Show, Data) + +data Phase = Initial | Running | Refunding deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where + data Action EscrowModel = Init Slot [(Wallet, Integer)] + | Redeem Wallet + | Pay Wallet Integer + | Refund Wallet + deriving (Eq, Show, Data) + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _refundSlot = 0 + , _phase = Initial + } + + initialInstances = [] + + startInstances _ (Init s wns) = + [StartContract (WalletKey w) (escrowParams s wns) | w <- testWallets] + startInstances _ _ = [] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} params = testContract params + + nextState a = case a of + Init s wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + refundSlot .= s + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + Refund w -> do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 + + + nextReactiveState slot = do + deadline <- viewContractState refundSlot + when (slot >= deadline) $ phase .= Refunding + + + precondition s a = case a of + Init s tgts -> currentPhase == Initial + && s > 1 + && and [Ada.adaValueOf (fromInteger n) `geq` Ada.toValue minAdaTxOut | (_,n) <- tgts] + Redeem _ -> currentPhase == Running + && fold (s ^. contractState . contributions) `geq` fold (s ^. contractState . targets) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + Refund w -> currentPhase == Refunding + && w `Map.member` (s ^. contractState . contributions) + where currentPhase = s ^. contractState . phase + + + perform h _ _ a = case a of + Init _ _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + Refund w -> do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 + + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> (Slot . getPositive <$> scale (*10) arbitrary) <*> arbitraryTargets + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] ++ + [ (1, Refund <$> elements testWallets) ] + + + shrinkAction _ (Init s tgts) = map (Init s) (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) + ++ map (`Init` tgts) (map Slot . shrink . getSlot $ s) + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +testContract :: EscrowParams Datum -> Contract () EscrowSchema EscrowError () +testContract params = selectList [ void $ payEp params + , void $ redeemEp params + , void $ refundEp params + ] >> testContract params + + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + + +escrowParams :: Slot -> [(Wallet, Integer)] -> EscrowParams d +escrowParams s tgts = + EscrowParams + { escrowTargets = + [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) (Ada.adaValueOf (fromInteger n)) + | (w,n) <- tgts + ] + , escrowDeadline = scSlotZeroTime def + POSIXTime (getSlot s * scSlotLength def) + } + +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + phase <- viewContractState phase + monitor $ tabulate "Phase" [show phase] + waitUntilDeadline + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] + +walletStrategy :: Wallet -> DL EscrowModel () +walletStrategy w = do + contribs <- viewContractState contributions + --waitUntilDeadline + when (w `Map.member` contribs) $ do + action $ Refund w + +waitUntilDeadline :: DL EscrowModel () +waitUntilDeadline = do + deadline <- viewContractState refundSlot + slot <- viewModelState currentSlot + when (slot < deadline) $ waitUntilDL deadline + +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = walletStrategy } + +prop_FinishEscrow :: Property +prop_FinishEscrow = forAllDL finishEscrow prop_Escrow + +prop_FinishFast :: Property +prop_FinishFast = forAllDL finishEscrow $ const True + +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProof noLockProof + +prop_NoLockedFundsFast :: Property +prop_NoLockedFundsFast = checkNoLockedFundsProofFast noLockProof diff --git a/plutus-use-cases/test/Spec/Tutorial/Escrow5.hs b/plutus-use-cases/test/Spec/Tutorial/Escrow5.hs new file mode 100644 index 0000000000..395a8231de --- /dev/null +++ b/plutus-use-cases/test/Spec/Tutorial/Escrow5.hs @@ -0,0 +1,233 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in Plutus.Contracts.Tutorial.Escrow, +-- with generated escrow targets. See the "Parameterising Models and +-- Dynamic Contract Instances" section of the tutorial. + +module Spec.Tutorial.Escrow5(prop_Escrow, prop_FinishEscrow, prop_FinishFast, prop_NoLockedFunds, prop_NoLockedFundsFast, + check_propEscrowWithCoverage, EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void, when) +import Data.Data +import Data.Default +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, Slot (..), minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.TimeSlot (SlotConfig (..)) +import Ledger.Value (Value, geq) +import Plutus.Contract (Contract, selectList) +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel +import Plutus.Contract.Test.Coverage +import Plutus.V1.Ledger.Time + +import Plutus.Contracts.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _refundSlot :: Slot + , _phase :: Phase + } deriving (Eq, Show, Data) + +data Phase = Initial | Running | Refunding deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where + data Action EscrowModel = Init Slot [(Wallet, Integer)] + | Redeem Wallet + | Pay Wallet Integer + | Refund Wallet + deriving (Eq, Show, Data) + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _refundSlot = 0 + , _phase = Initial + } + + initialInstances = [] + + startInstances _ (Init s wns) = + [StartContract (WalletKey w) (escrowParams s wns) | w <- testWallets] + startInstances _ _ = [] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} params = testContract params + + nextState a = case a of + Init s wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + refundSlot .= s + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + Refund w -> do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 + + + nextReactiveState slot = do + deadline <- viewContractState refundSlot + when (slot >= deadline) $ phase .= Refunding + + + precondition s a = case a of + Init s tgts -> currentPhase == Initial + && s > 1 + && and [Ada.adaValueOf (fromInteger n) `geq` Ada.toValue minAdaTxOut | (_,n) <- tgts] + Redeem _ -> currentPhase == Running + && fold (s ^. contractState . contributions) `geq` fold (s ^. contractState . targets) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + Refund w -> currentPhase == Refunding + && w `Map.member` (s ^. contractState . contributions) + where currentPhase = s ^. contractState . phase + + + perform h _ _ a = case a of + Init _ _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + Refund w -> do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 + + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> (Slot . getPositive <$> scale (*10) arbitrary) <*> arbitraryTargets + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] ++ + [ (1, Refund <$> elements testWallets) ] + + + shrinkAction _ (Init s tgts) = map (Init s) (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) + ++ map (`Init` tgts) (map Slot . shrink . getSlot $ s) + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +testContract :: EscrowParams Datum -> Contract () EscrowSchema EscrowError () +testContract params = selectList [ void $ payEp params + , void $ redeemEp params + , void $ refundEp params + ] >> testContract params + + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + + +escrowParams :: Slot -> [(Wallet, Integer)] -> EscrowParams d +escrowParams s tgts = + EscrowParams + { escrowTargets = + [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) (Ada.adaValueOf (fromInteger n)) + | (w,n) <- tgts + ] + , escrowDeadline = scSlotZeroTime def + POSIXTime (getSlot s * scSlotLength def) + } + +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + phase <- viewContractState phase + monitor $ tabulate "Phase" [show phase] + waitUntilDeadline + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] + +walletStrategy :: Wallet -> DL EscrowModel () +walletStrategy w = do + contribs <- viewContractState contributions + --waitUntilDeadline + when (w `Map.member` contribs) $ do + action $ Refund w + +waitUntilDeadline :: DL EscrowModel () +waitUntilDeadline = do + deadline <- viewContractState refundSlot + slot <- viewModelState currentSlot + when (slot < deadline) $ waitUntilDL deadline + +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = walletStrategy } + +prop_FinishEscrow :: Property +prop_FinishEscrow = forAllDL finishEscrow prop_Escrow + +prop_FinishFast :: Property +prop_FinishFast = forAllDL finishEscrow $ const True + +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProof noLockProof + +prop_NoLockedFundsFast :: Property +prop_NoLockedFundsFast = checkNoLockedFundsProofFast noLockProof + +check_propEscrowWithCoverage :: IO () +check_propEscrowWithCoverage = do + cr <- quickCheckWithCoverage stdArgs (set coverageIndex covIdx $ defaultCoverageOptions) $ \covopts -> + withMaxSuccess 1000 $ propRunActionsWithOptions @EscrowModel defaultCheckOptionsContractModel covopts (const (pure True)) + writeCoverageReport "Escrow" covIdx cr diff --git a/plutus-use-cases/test/Spec/Tutorial/Escrow6.hs b/plutus-use-cases/test/Spec/Tutorial/Escrow6.hs new file mode 100644 index 0000000000..1590dbea6a --- /dev/null +++ b/plutus-use-cases/test/Spec/Tutorial/Escrow6.hs @@ -0,0 +1,251 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +-- This module contains a contract model for positive testing of the +-- simplified escrow contract in Plutus.Contracts.Tutorial.Escrow, +-- with generated escrow targets. See the "Parameterising Models and +-- Dynamic Contract Instances" section of the tutorial. + +module Spec.Tutorial.Escrow6(prop_Escrow, prop_FinishEscrow, prop_FinishFast, + prop_NoLockedFunds, prop_NoLockedFundsFast, prop_CrashTolerance, + check_propEscrowWithCoverage, EscrowModel) where + +import Control.Lens hiding (both, elements) +import Control.Monad (void, when) +import Data.Data +import Data.Default +import Data.Foldable +import Data.Map (Map) +import Data.Map qualified as Map + +import Ledger (Datum, Slot (..), minAdaTxOut) +import Ledger.Ada qualified as Ada +import Ledger.TimeSlot (SlotConfig (..)) +import Ledger.Value (Value, geq) +import Plutus.Contract (Contract, selectList) +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel +import Plutus.Contract.Test.ContractModel.CrashTolerance +import Plutus.Contract.Test.Coverage +import Plutus.V1.Ledger.Time + +import Plutus.Contracts.Escrow hiding (Action (..)) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Monoid (inv) + +import Test.QuickCheck + +data EscrowModel = EscrowModel { _contributions :: Map Wallet Value + , _targets :: Map Wallet Value + , _refundSlot :: Slot + , _phase :: Phase + } deriving (Eq, Show, Data) + +data Phase = Initial | Running | Refunding deriving (Eq, Show, Data) + +makeLenses ''EscrowModel + +deriving instance Eq (ContractInstanceKey EscrowModel w s e params) +deriving instance Show (ContractInstanceKey EscrowModel w s e params) + +instance ContractModel EscrowModel where + data Action EscrowModel = Init Slot [(Wallet, Integer)] + | Redeem Wallet + | Pay Wallet Integer + | Refund Wallet + deriving (Eq, Show, Data) + + data ContractInstanceKey EscrowModel w s e params where + WalletKey :: Wallet -> ContractInstanceKey EscrowModel () EscrowSchema EscrowError (EscrowParams Datum) + + initialState = EscrowModel { _contributions = Map.empty + , _targets = Map.empty + , _refundSlot = 0 + , _phase = Initial + } + + initialInstances = [] + + startInstances _ (Init s wns) = + [StartContract (WalletKey w) (escrowParams s wns) | w <- testWallets] + startInstances _ _ = [] + + instanceWallet (WalletKey w) = w + + instanceContract _ WalletKey{} params = testContract params + + nextState a = case a of + Init s wns -> do + phase .= Running + targets .= Map.fromList [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- wns] + refundSlot .= s + Pay w v -> do + withdraw w (Ada.adaValueOf $ fromInteger v) + contributions %= Map.insertWith (<>) w (Ada.adaValueOf $ fromInteger v) + wait 1 + Redeem w -> do + targets <- viewContractState targets + contribs <- viewContractState contributions + sequence_ [ deposit w v | (w, v) <- Map.toList targets ] + let leftoverValue = fold contribs <> inv (fold targets) + deposit w leftoverValue + contributions .= Map.empty + wait 1 + Refund w -> do + v <- viewContractState $ contributions . at w . to fold + contributions %= Map.delete w + deposit w v + wait 1 + + + nextReactiveState slot = do + deadline <- viewContractState refundSlot + when (slot >= deadline) $ phase .= Refunding + + + precondition s a = case a of + Init s tgts -> currentPhase == Initial + && s > 1 + && and [Ada.adaValueOf (fromInteger n) `geq` Ada.toValue minAdaTxOut | (_,n) <- tgts] + Redeem _ -> currentPhase == Running + && fold (s ^. contractState . contributions) `geq` fold (s ^. contractState . targets) + Pay _ v -> currentPhase == Running + && Ada.adaValueOf (fromInteger v) `geq` Ada.toValue minAdaTxOut + Refund w -> currentPhase == Refunding + && w `Map.member` (s ^. contractState . contributions) + where currentPhase = s ^. contractState . phase + + + perform h _ _ a = case a of + Init _ _ -> do + return () + Pay w v -> do + Trace.callEndpoint @"pay-escrow" (h $ WalletKey w) (Ada.adaValueOf $ fromInteger v) + delay 1 + Redeem w -> do + Trace.callEndpoint @"redeem-escrow" (h $ WalletKey w) () + delay 1 + Refund w -> do + Trace.callEndpoint @"refund-escrow" (h $ WalletKey w) () + delay 1 + + arbitraryAction s + | s ^.contractState . phase == Initial + = Init <$> (Slot . getPositive <$> scale (*10) arbitrary) <*> arbitraryTargets + | otherwise + = frequency $ [ (3, Pay <$> elements testWallets <*> choose (1, 30)) ] ++ + [ (1, Redeem <$> elements testWallets) + | (s ^. contractState . contributions . to fold) `geq` (s ^. contractState . targets . to fold) + ] ++ + [ (1, Refund <$> elements testWallets) ] + + + shrinkAction _ (Init s tgts) = map (Init s) (shrinkList (\(w,n)->(w,)<$>shrink n) tgts) + ++ map (`Init` tgts) (map Slot . shrink . getSlot $ s) + shrinkAction _ (Pay w n) = [Pay w n' | n' <- shrink n] + shrinkAction _ _ = [] + +arbitraryTargets :: Gen [(Wallet,Integer)] +arbitraryTargets = do + ws <- sublistOf testWallets + vs <- infiniteListOf $ choose (1,30) + return $ zip ws vs + +testWallets :: [Wallet] +testWallets = [w1, w2, w3, w4, w5] + +testContract :: EscrowParams Datum -> Contract () EscrowSchema EscrowError () +testContract params = selectList [ void $ payEp params + , void $ redeemEp params + , void $ refundEp params + ] >> testContract params + + +prop_Escrow :: Actions EscrowModel -> Property +prop_Escrow = propRunActions_ + +escrowParams :: Slot -> [(Wallet, Integer)] -> EscrowParams d +escrowParams s tgts = escrowParams' s [(w, Ada.adaValueOf (fromInteger n)) | (w,n) <- tgts] + +escrowParams' :: Slot -> [(Wallet,Value)] -> EscrowParams d +escrowParams' s tgts' = + EscrowParams + { escrowTargets = [ payToPaymentPubKeyTarget (mockWalletPaymentPubKeyHash w) v + | (w,v) <- tgts' ] + , escrowDeadline = scSlotZeroTime def + POSIXTime (getSlot s * scSlotLength def) + } + +finishEscrow :: DL EscrowModel () +finishEscrow = do + anyActions_ + finishingStrategy + assertModel "Locked funds are not zero" (symIsZero . lockedValue) + +finishingStrategy :: DL EscrowModel () +finishingStrategy = do + contribs <- viewContractState contributions + monitor (tabulate "Refunded wallets" [show . Map.size $ contribs]) + phase <- viewContractState phase + monitor $ tabulate "Phase" [show phase] + waitUntilDeadline + sequence_ [action $ Refund w | w <- testWallets, w `Map.member` contribs] + +walletStrategy :: Wallet -> DL EscrowModel () +walletStrategy w = do + contribs <- viewContractState contributions + --waitUntilDeadline + when (w `Map.member` contribs) $ do + action $ Refund w + +waitUntilDeadline :: DL EscrowModel () +waitUntilDeadline = do + deadline <- viewContractState refundSlot + slot <- viewModelState currentSlot + when (slot < deadline) $ waitUntilDL deadline + +noLockProof :: NoLockedFundsProof EscrowModel +noLockProof = defaultNLFP + { nlfpMainStrategy = finishingStrategy + , nlfpWalletStrategy = walletStrategy } + +prop_FinishEscrow :: Property +prop_FinishEscrow = forAllDL finishEscrow prop_Escrow + +prop_FinishFast :: Property +prop_FinishFast = forAllDL finishEscrow $ const True + +prop_NoLockedFunds :: Property +prop_NoLockedFunds = checkNoLockedFundsProof noLockProof + +prop_NoLockedFundsFast :: Property +prop_NoLockedFundsFast = checkNoLockedFundsProofFast noLockProof + +instance CrashTolerance EscrowModel where + available (Init _ _) _ = True + available a alive = (Key $ WalletKey w) `elem` alive + where w = case a of + Pay w _ -> w + Redeem w -> w + Refund w -> w + Init _ _ -> undefined + + restartArguments s WalletKey{} = escrowParams' slot tgts + where slot = s ^. contractState . refundSlot + tgts = Map.toList (s ^. contractState . targets) + +prop_CrashTolerance :: Actions (WithCrashTolerance EscrowModel) -> Property +prop_CrashTolerance = propRunActions_ + +check_propEscrowWithCoverage :: IO () +check_propEscrowWithCoverage = do + cr <- quickCheckWithCoverage stdArgs (set coverageIndex covIdx $ defaultCoverageOptions) $ \covopts -> + withMaxSuccess 1000 $ propRunActionsWithOptions @EscrowModel defaultCheckOptionsContractModel covopts (const (pure True)) + writeCoverageReport "Escrow" covIdx cr diff --git a/plutus-use-cases/test/Spec/Uniswap.hs b/plutus-use-cases/test/Spec/Uniswap.hs index c6fcbd6fce..dfb56d6057 100644 --- a/plutus-use-cases/test/Spec/Uniswap.hs +++ b/plutus-use-cases/test/Spec/Uniswap.hs @@ -58,6 +58,8 @@ import Data.Semigroup qualified as Semigroup import Ledger.Constraints +import Spec.Uniswap.Endpoints + data PoolIndex = PoolIndex SymToken SymToken deriving (Show, Data) poolIndex :: SymToken -> SymToken -> PoolIndex @@ -95,13 +97,15 @@ deriving instance Show (ContractInstanceKey UniswapModel w s e params) walletOf :: Action UniswapModel -> Wallet walletOf a = case a of - SetupTokens -> w1 - Start -> w1 - CreatePool w _ _ _ _ -> w - AddLiquidity w _ _ _ _ -> w - RemoveLiquidity w _ _ _ -> w - PerformSwap w _ _ _ -> w - ClosePool w _ _ -> w + SetupTokens -> w1 + Start -> w1 + CreatePool w _ _ _ _ -> w + AddLiquidity w _ _ _ _ -> w + RemoveLiquidity w _ _ _ -> w + PerformSwap w _ _ _ -> w + ClosePool w _ _ -> w + BadRemoveLiquidity w _ _ _ _ _ -> w + Bad act -> walletOf act hasPool :: ModelState UniswapModel -> SymToken -> SymToken -> Bool hasPool s t1 t2 = isJust (s ^. contractState . pools . at (poolIndex t1 t2)) @@ -119,6 +123,9 @@ totalLiquidity s t1 t2 = sum $ s ^? contractState . pools . at (poolIndex t1 t2) hasUniswapToken :: ModelState UniswapModel -> Bool hasUniswapToken s = isJust $ s ^. contractState . uniswapToken +hasExchangeableToken :: SymToken -> ModelState UniswapModel -> Bool +hasExchangeableToken t = (^. contractState . exchangeableTokens . to (t `elem`)) + swapUnless :: Bool -> (a, a) -> (a, a) swapUnless b (a, a') = if b then (a, a') else (a', a) @@ -149,7 +156,7 @@ setupTokens = do amount = 1000000 wallets :: [Wallet] -wallets = take 9 knownWallets +wallets = take 6 knownWallets tokenNames :: [String] tokenNames = ["A", "B", "C", "D"] @@ -169,23 +176,30 @@ instance ContractModel UniswapModel where -- ^ Amount of liquidity to cash in | ClosePool Wallet SymToken SymToken -- ^ Close a liquidity pool + | Bad (Action UniswapModel) + -- ^ An action included for negative testing + | BadRemoveLiquidity Wallet SymToken Integer SymToken Integer Integer + -- ^ Remove liquidity, specify amounts to get deriving (Eq, Show, Data) data ContractInstanceKey UniswapModel w s e params where OwnerKey :: ContractInstanceKey UniswapModel (Last (Either Text.Text Uniswap)) EmptySchema ContractError () SetupKey :: ContractInstanceKey UniswapModel (Maybe (Semigroup.Last Currency.OneShotCurrency)) Currency.CurrencySchema Currency.CurrencyError () WalletKey :: Wallet -> ContractInstanceKey UniswapModel (Last (Either Text.Text UserContractState)) UniswapUserSchema Void SymToken + BadReqKey :: Wallet -> ContractInstanceKey UniswapModel () BadEndpoints Void SymToken initialInstances = [] instanceWallet OwnerKey = w1 instanceWallet SetupKey = w1 instanceWallet (WalletKey w) = w + instanceWallet (BadReqKey w) = w instanceContract tokenSem key token = case key of OwnerKey -> ownerEndpoint SetupKey -> setupTokens WalletKey _ -> toContract . userEndpoints . Uniswap . Coin . tokenSem $ token + BadReqKey _ -> toContract . badEndpoints . Uniswap . Coin . tokenSem $ token initialState = UniswapModel Nothing mempty mempty mempty @@ -193,9 +207,13 @@ instance ContractModel UniswapModel where frequency $ [ (1, pure Start) , (1, pure SetupTokens) ] ++ [ (3, createPool) | not . null $ s ^. contractState . exchangeableTokens ] ++ - [ (10, gen) | gen <- [createPool, add, swap, remove, close] + [ (10, gen) | gen <- [add True, swap True, remove True, close] , not . null $ s ^. contractState . exchangeableTokens - , not . null $ s ^. contractState . pools ] + , not . null $ s ^. contractState . pools ] ++ + [ (1, bad) | bad <- [add False, swap False, remove False] + , not . null $ s ^. contractState . exchangeableTokens ] ++ + [ (1, generalRemove) | not . null $ s ^. contractState . exchangeableTokens + , not . null $ s ^. contractState . pools ] where createPool = do w <- elements $ wallets \\ [w1] @@ -203,38 +221,68 @@ instance ContractModel UniswapModel where t2 <- elements $ s ^. contractState . exchangeableTokens . to (Set.delete t1) . to Set.toList a1 <- choose (1, 100) a2 <- choose (1, 100) - return $ CreatePool w (getAToken t1 t2) a1 (getBToken t1 t2) a2 + (tA, tB) <- elements [(t1, t2), (t2, t1)] + return $ CreatePool w tA a1 tB a2 - add = do + add good = do w <- elements $ wallets \\ [w1] - PoolIndex t1 t2 <- elements $ s ^. contractState . pools . to Map.keys - a1 <- choose (1, 100) - a2 <- choose (1, 100) - return $ AddLiquidity w (getAToken t1 t2) a1 (getBToken t1 t2) a2 + (t1, t2) <- twoTokens good + a1 <- choose (0, 100) + a2 <- choose (0, 100) + (tA, tB) <- elements [(t1, t2), (t2, t1)] + return . bad $ AddLiquidity w tA a1 tB a2 - swap = do - PoolIndex t1 t2 <- elements $ s ^. contractState . pools . to Map.keys - w <- elements $ s ^. contractState . pools . at (poolIndex t1 t2) . to fromJust . liquidities . to Map.keys + swap good = do + (t1, t2) <- twoTokens good + w <- elements $ wallets \\ [w1] a <- choose (1, 100) (tA, tB) <- elements [(t1, t2), (t2, t1)] - return $ PerformSwap w tA tB a - - remove = do - idx@(PoolIndex t1 t2) <- elements $ s ^. contractState . pools . to Map.keys - w <- elements . fold $ s ^? contractState . pools . at idx . _Just . liquidities . to (Map.filter (0<)) . to Map.keys - a <- choose (1, sum $ s ^? contractState . pools . at idx . _Just . liquidities . at w . _Just . to unAmount) - return $ RemoveLiquidity w (getAToken t1 t2) (getBToken t1 t2) a + return . bad $ PerformSwap w tA tB a + + remove good = do + (t1, t2) <- twoTokens good + let idx = poolIndex t1 t2 + w <- if good && hasOpenPool s t1 t2 then + -- if the poolIndex exists, but the pool has been closed, then we cannot choose a wallet with liquidity + elements . fold $ s ^? contractState . pools . at idx . _Just . liquidities . to (Map.filter (0<)) . to Map.keys + else + elements $ wallets \\ [w1] + a <- if good then + choose (1, sum $ s ^? contractState . pools . at idx . _Just . liquidities . at w . _Just . to unAmount) + else + oneof [choose (1,10), choose (1,100)] + (tA, tB) <- elements [(t1, t2), (t2, t1)] + return . bad $ RemoveLiquidity w tA tB a close = do w <- elements $ wallets \\ [w1] PoolIndex t1 t2 <- elements $ s ^. contractState . pools . to Map.keys - return $ ClosePool w (getAToken t1 t2) (getBToken t1 t2) + (tA, tB) <- elements [(t1, t2), (t2, t1)] + return $ ClosePool w tA tB + + generalRemove = do + r <- remove True + case r of + RemoveLiquidity w tA tB a -> do + (Positive aA, Positive aB) <- arbitrary + return . bad $ BadRemoveLiquidity w tA aA tB aB a + _ -> return r + + twoTokens True = do PoolIndex t1 t2 <- elements $ s ^. contractState . pools . to Map.keys + return (t1, t2) + twoTokens False = two (/=) . elements $ s ^. contractState . exchangeableTokens . to Set.toList + + two p gen = ((,) <$> gen <*> gen) `suchThat` uncurry p + + bad act = if precondition s act then act else Bad act startInstances s act = case act of Start -> [ StartContract OwnerKey () ] SetupTokens -> [ StartContract SetupKey () ] - _ -> [ StartContract (WalletKey $ walletOf act) (fromJust $ s ^. contractState . uniswapToken) - | walletOf act `notElem` s ^. contractState . startedUserCode ] + _ -> [ start (walletOf act) t + | Just t <- s ^. contractState . uniswapToken . to (:[]) + , walletOf act `notElem` s ^. contractState . startedUserCode + , start <- [StartContract . WalletKey, StartContract . BadReqKey] ] precondition s Start = not $ hasUniswapToken s precondition _ SetupTokens = True @@ -243,10 +291,8 @@ instance ContractModel UniswapModel where && t1 /= t2 && 0 < a1 && 0 < a2 - precondition s (AddLiquidity _ t1 a1 t2 a2) = hasOpenPool s t1 t2 + precondition s (AddLiquidity _ t1 _ t2 _ ) = hasOpenPool s t1 t2 && t1 /= t2 - && 0 < a1 - && 0 < a2 precondition s (PerformSwap _ t1 t2 a) = hasOpenPool s t1 t2 && t1 /= t2 && 0 < a @@ -258,6 +304,20 @@ instance ContractModel UniswapModel where precondition s (ClosePool w t1 t2) = hasOpenPool s t1 t2 && t1 /= t2 && liquidityOf s w t1 t2 == totalLiquidity s t1 t2 + precondition s (Bad badAction) = (wf badAction &&) . not $ precondition s badAction + where wf (AddLiquidity _ t1 _ t2 _ ) = wfTokens t1 t2 + wf (PerformSwap _ t1 t2 _) = wfTokens t1 t2 + wf (RemoveLiquidity _ t1 t2 _) = wfTokens t1 t2 + wf (BadRemoveLiquidity _ t1 _ t2 _ _) = wfTokens t1 t2 + wf _ = error "Pattern match(es) are not exhaustive\nIn an equation for `wf'." + + wfTokens t1 t2 = hasUniswapToken s && t1 /= t2 + precondition s (BadRemoveLiquidity w t1 a1 t2 a2 a) = + precondition s (RemoveLiquidity w t1 t2 a) && + let p = s ^. contractState . pools . at (poolIndex t1 t2) . to fromJust in + calculateRemoval (p ^. coinAAmount) (p ^. coinBAmount) (sum $ p ^. liquidities) (Amount a) + == + (Amount a1, Amount a2) nextState act = case act of SetupTokens -> do @@ -313,7 +373,7 @@ instance ContractModel UniswapModel where AddLiquidity w t1 a1 t2 a2 -> do startedUserCode %= Set.insert w p <- use $ pools . at (poolIndex t1 t2) . to fromJust - -- Compute the amount of liqiudity token we get + -- Compute the amount of liquidity token we get let (deltaA, deltaB) = mkAmounts t1 t2 a1 a2 deltaL = calculateAdditionalLiquidity (p ^. coinAAmount) (p ^. coinBAmount) @@ -410,6 +470,12 @@ instance ContractModel UniswapModel where deposit w $ Ada.toValue Ledger.minAdaTxOut wait 5 + Bad _ -> do + wait 5 + + BadRemoveLiquidity w t1 _ t2 _ a -> + nextState $ RemoveLiquidity w t1 t2 a -- because the precondition ensures the amounts are valid + perform h tokenSem s act = case act of SetupTokens -> do delay 20 @@ -425,8 +491,8 @@ instance ContractModel UniswapModel where CreatePool w t1 a1 t2 a2 -> do let us = s ^. contractState . uniswapToken . to fromJust - c1 = Coin (tokenSem $ getAToken t1 t2) - c2 = Coin (tokenSem $ getBToken t1 t2) + c1 = Coin (tokenSem t1) + c2 = Coin (tokenSem t2) Coin ac = liquidityCoin (fst . Value.unAssetClass . tokenSem $ us) c1 c2 Trace.callEndpoint @"create" (h (WalletKey w)) $ CreateParams c1 c2 (Amount a1) (Amount a2) delay 5 @@ -434,8 +500,8 @@ instance ContractModel UniswapModel where registerToken "Liquidity" ac AddLiquidity w t1 a1 t2 a2 -> do - let c1 = Coin (tokenSem $ getAToken t1 t2) - c2 = Coin (tokenSem $ getBToken t1 t2) + let c1 = Coin (tokenSem t1) + c2 = Coin (tokenSem t2) Trace.callEndpoint @"add" (h (WalletKey w)) $ AddParams c1 c2 (Amount a1) (Amount a2) delay 5 @@ -446,23 +512,37 @@ instance ContractModel UniswapModel where delay 5 RemoveLiquidity w t1 t2 a -> do - let c1 = Coin (tokenSem $ getAToken t1 t2) - c2 = Coin (tokenSem $ getBToken t1 t2) + let c1 = Coin (tokenSem t1) + c2 = Coin (tokenSem t2) Trace.callEndpoint @"remove" (h (WalletKey w)) $ RemoveParams c1 c2 (Amount a) delay 5 + BadRemoveLiquidity w t1 a1 t2 a2 a -> do + let c1 = Coin (tokenSem t1) + c2 = Coin (tokenSem t2) + Trace.callEndpoint @"bad-remove" (h (BadReqKey w)) $ BadRemoveParams c1 (Amount a1) c2 (Amount a2) (Amount a) + delay 5 + ClosePool w t1 t2 -> do - let c1 = Coin (tokenSem $ getAToken t1 t2) - c2 = Coin (tokenSem $ getBToken t1 t2) + let c1 = Coin (tokenSem t1) + c2 = Coin (tokenSem t2) Trace.callEndpoint @"close" (h (WalletKey w)) $ CloseParams c1 c2 delay 5 - shrinkAction _ a = case a of - CreatePool w t1 a1 t2 a2 -> [ CreatePool w t1 a1' t2 a2' | (a1', a2') <- shrink (a1, a2), a1' >= 0, a2' >= 0 ] - AddLiquidity w t1 a1 t2 a2 -> [ AddLiquidity w t1 a1' t2 a2' | (a1', a2') <- shrink (a1, a2), a1' >= 0, a2' >= 0 ] - RemoveLiquidity w t1 t2 a -> [ RemoveLiquidity w t1 t2 a' | a' <- shrink a, a' >= 0 ] - PerformSwap w t1 t2 a -> [ PerformSwap w t1 t2 a' | a' <- shrink a, a' >= 0 ] - _ -> [] + Bad act -> do + perform h tokenSem s act + + shrinkAction s a = case a of + CreatePool w t1 a1 t2 a2 -> [ CreatePool w t1 a1' t2 a2' | (a1', a2') <- shrink (a1, a2), a1' >= 0, a2' >= 0 ] + AddLiquidity w t1 a1 t2 a2 -> [ AddLiquidity w t1 a1' t2 a2' | (a1', a2') <- shrink (a1, a2), a1' >= 0, a2' >= 0 ] + RemoveLiquidity w t1 t2 a -> [ RemoveLiquidity w t1 t2 a' | a' <- shrink a, a' >= 0 ] + PerformSwap w t1 t2 a -> [ PerformSwap w t1 t2 a' | a' <- shrink a, a' >= 0 ] + Bad act -> Bad <$> shrinkAction s act + BadRemoveLiquidity w t1 a1 t2 a2 a -> [ BadRemoveLiquidity w t1 a1' t2 a2' a' | (a1', a2', a') <- shrink (a1, a2, a)] + _ -> [] + + monitoring _ (Bad act) = tabulate "Bad actions" [actionName act] + monitoring _ _ = id -- This doesn't hold prop_liquidityValue :: Property @@ -512,19 +592,16 @@ noLockProofLight :: NoLockedFundsProofLight UniswapModel noLockProofLight = NoLockedFundsProofLight{nlfplMainStrategy = nlfpMainStrategy noLockProof} prop_CheckNoLockedFundsProof :: Property -prop_CheckNoLockedFundsProof = checkNoLockedFundsProof defaultCheckOptionsContractModel noLockProof +prop_CheckNoLockedFundsProof = checkNoLockedFundsProof noLockProof prop_CheckNoLockedFundsProofFast :: Property -prop_CheckNoLockedFundsProofFast = checkNoLockedFundsProofFast defaultCheckOptionsContractModel noLockProof +prop_CheckNoLockedFundsProofFast = checkNoLockedFundsProofFast noLockProof check_propUniswapWithCoverage :: IO () -check_propUniswapWithCoverage = void $ - quickCheckWithCoverage (stdArgs { maxSuccess = 1000 }) - (set endpointCoverageReq epReqs $ set coverageIndex covIdx $ defaultCoverageOptions) - $ \covopts -> propRunActionsWithOptions @UniswapModel - defaultCheckOptionsContractModel - covopts - (const (pure True)) +check_propUniswapWithCoverage = do + cr <- quickCheckWithCoverage stdArgs (set endpointCoverageReq epReqs $ set coverageIndex covIdx $ defaultCoverageOptions) $ \covopts -> + withMaxSuccess 1000 $ propRunActionsWithOptions @UniswapModel defaultCheckOptionsContractModel covopts (const (pure True)) + writeCoverageReport "Uniswap" covIdx cr where epReqs t ep | t == Trace.walletInstanceTag w1 = 0 diff --git a/plutus-use-cases/test/Spec/Uniswap/Endpoints.hs b/plutus-use-cases/test/Spec/Uniswap/Endpoints.hs new file mode 100644 index 0000000000..7ded94355b --- /dev/null +++ b/plutus-use-cases/test/Spec/Uniswap/Endpoints.hs @@ -0,0 +1,87 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MonoLocalBinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeOperators #-} + +module Spec.Uniswap.Endpoints where + +import Control.Monad hiding (fmap) +import Data.Map qualified as Map +import Data.Text (Text) +import Data.Void (Void) +import Ledger hiding (singleton) +import Ledger.Constraints as Constraints +import Playground.Contract +import Plutus.Contract as Contract +import Plutus.Contracts.Currency () +import Plutus.Contracts.Uniswap.Pool +import Plutus.Contracts.Uniswap.Types +import PlutusTx qualified +import PlutusTx.Prelude hiding (Semigroup (..), dropWhile, flip, unless) +import Prelude as Haskell (Semigroup (..), show) + +import Plutus.Contracts.Uniswap.OffChain + +type TestUniswapUserSchema = + Endpoint "bad-remove" BadRemoveParams + .\/ UniswapUserSchema + +type BadEndpoints = Endpoint "bad-remove" BadRemoveParams + +-- | Parameters for the @bad-remove@-endpoint, which removes some liquidity from a liquidity pool. +data BadRemoveParams = BadRemoveParams + { brpCoinA :: Coin A -- ^ One 'Coin' of the liquidity pair. + , brpOutA :: Amount A -- ^ Amount to try to remove + , brpCoinB :: Coin B -- ^ The other 'Coin' of the liquidity pair. + , brpOutB :: Amount B -- ^ Amount to try to remove + , brpDiff :: Amount Liquidity-- ^ The amount of liquidity tokens to burn in exchange for liquidity from the pool. + } deriving (Show, Generic, ToJSON, FromJSON, ToSchema) + + +-- | A variant on remove which tries to remove different amounts of tokens +badRemove :: forall w s. Uniswap -> BadRemoveParams -> Contract w s Text () +badRemove us BadRemoveParams{..} = do + (_, (oref, o, lp, liquidity)) <- findUniswapFactoryAndPool us brpCoinA brpCoinB + pkh <- Contract.ownPaymentPubKeyHash + --when (brpDiff < 1 || brpDiff >= liquidity) $ throwError "removed liquidity must be positive and less than total liquidity" + let usInst = uniswapInstance us + usScript = uniswapScript us + dat = Pool lp $ liquidity - brpDiff + psC = poolStateCoin us + lC = mkCoin (liquidityCurrency us) $ lpTicker lp + psVal = unitValue psC + lVal = valueOf lC brpDiff + --inVal = view ciTxOutValue o + --inA = amountOf inVal brpCoinA + --inB = amountOf inVal brpCoinB + --(outA, outB) = calculateRemoval inA inB liquidity brpDiff + --val = psVal <> valueOf brpCoinA (inA-brpOutA) <> valueOf brpCoinB (inB-brpOutB) + -- This version allows us to control the submitted transaction more directly (and also reveals a more interesting failed test case). + val = psVal <> valueOf brpCoinA brpOutA <> valueOf brpCoinB brpOutB + redeemer = Redeemer $ PlutusTx.toBuiltinData Remove + + lookups = Constraints.typedValidatorLookups usInst <> + Constraints.otherScript usScript <> + Constraints.mintingPolicy (liquidityPolicy us) <> + Constraints.unspentOutputs (Map.singleton oref o) <> + Constraints.ownPaymentPubKeyHash pkh + + tx = Constraints.mustPayToTheScript dat val <> + Constraints.mustMintValue (negate lVal) <> + Constraints.mustSpendScriptOutput oref redeemer + + mkTxConstraints lookups tx >>= submitTxConfirmed . adjustUnbalancedTx + + logInfo $ "removed liquidity from pool: " ++ show lp + +badEndpoints :: Uniswap -> Promise () BadEndpoints Void () +badEndpoints us = + void (handleEndpoint @"bad-remove" $ either (pure . Left) (runError . badRemove us)) + <> badEndpoints us diff --git a/plutus-use-cases/test/Spec/Vesting.hs b/plutus-use-cases/test/Spec/Vesting.hs index d45da5a09d..75fbb2f4f5 100644 --- a/plutus-use-cases/test/Spec/Vesting.hs +++ b/plutus-use-cases/test/Spec/Vesting.hs @@ -209,7 +209,7 @@ noLockProof = defaultNLFP { | otherwise = return () prop_CheckNoLockedFundsProof :: Property -prop_CheckNoLockedFundsProof = checkNoLockedFundsProof defaultCheckOptionsContractModel noLockProof +prop_CheckNoLockedFundsProof = checkNoLockedFundsProof noLockProof -- Tests diff --git a/quickcheck-dynamic/quickcheck-dynamic.cabal b/quickcheck-dynamic/quickcheck-dynamic.cabal index 0c8d1b4b1c..7c98a9edb5 100644 --- a/quickcheck-dynamic/quickcheck-dynamic.cabal +++ b/quickcheck-dynamic/quickcheck-dynamic.cabal @@ -37,6 +37,7 @@ library Test.QuickCheck.DynamicLogic.Monad Test.QuickCheck.DynamicLogic.Quantify Test.QuickCheck.DynamicLogic.SmartShrinking + Test.QuickCheck.DynamicLogic.Utils Test.QuickCheck.StateModel build-depends: QuickCheck -any, diff --git a/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic.hs b/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic.hs index 5d84b6c270..25047a10d8 100644 --- a/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic.hs +++ b/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic.hs @@ -9,9 +9,9 @@ module Test.QuickCheck.DynamicLogic ( module Test.QuickCheck.DynamicLogic.Quantify - , DynLogic, DynPred + , DynLogic, DynPred, DynFormula , DynLogicModel(..), DynLogicTest(..), TestStep(..) - , ignore, passTest, afterAny, after, (|||), forAllQ, weight, toStop + , ignore, passTest, afterAny, after, (|||), forAllQ, weight, withSize, toStop , done, errorDL, monitorDL, always , forAllScripts, forAllScripts_, withDLScript, withDLScriptPrefix, forAllMappedScripts, forAllMappedScripts_ , forAllUniqueScripts, propPruningGeneratedScriptIsNoop @@ -27,6 +27,7 @@ import Test.QuickCheck hiding (generate) import Test.QuickCheck.DynamicLogic.CanGenerate import Test.QuickCheck.DynamicLogic.Quantify import Test.QuickCheck.DynamicLogic.SmartShrinking +import Test.QuickCheck.DynamicLogic.Utils qualified as QC import Test.QuickCheck.StateModel -- | Dynamic logic formulae. @@ -52,47 +53,53 @@ data ChoiceType = Angelic | Demonic type DynPred s = s -> DynLogic s +newtype DynFormula s = DynFormula {unDynFormula :: Int -> DynLogic s} + -- a DynFormula may depend on the QuickCheck size parameter + -- API for building formulae -ignore :: DynLogic s -passTest :: DynLogic s -afterAny :: DynPred s -> DynLogic s +ignore :: DynFormula s +passTest :: DynFormula s +afterAny :: (s -> DynFormula s) -> DynFormula s after :: (Show a, Typeable a, Eq (Action s a)) => - Action s a -> DynPred s -> DynLogic s -(|||) :: DynLogic s -> DynLogic s -> DynLogic s + Action s a -> (s -> DynFormula s) -> DynFormula s +(|||) :: DynFormula s -> DynFormula s -> DynFormula s forAllQ :: Quantifiable q => - q -> (Quantifies q -> DynLogic s) -> DynLogic s -weight :: Double -> DynLogic s -> DynLogic s -toStop :: DynLogic s -> DynLogic s + q -> (Quantifies q -> DynFormula s) -> DynFormula s +weight :: Double -> DynFormula s -> DynFormula s +withSize :: (Int -> DynFormula s) -> DynFormula s +toStop :: DynFormula s -> DynFormula s -done :: DynPred s -errorDL :: String -> DynLogic s +done :: s -> DynFormula s +errorDL :: String -> DynFormula s -monitorDL :: (Property -> Property) -> DynLogic s -> DynLogic s +monitorDL :: (Property -> Property) -> DynFormula s -> DynFormula s -always :: DynPred s -> DynPred s +always :: (s -> DynFormula s) -> (s -> DynFormula s) -ignore = EmptySpec -passTest = Stop -afterAny = AfterAny -after act = After (Some act) -(|||) = Alt Angelic -- In formulae, we use only angelic +ignore = DynFormula . const $ EmptySpec +passTest = DynFormula . const $ Stop +afterAny f = DynFormula $ \n -> AfterAny $ \s -> unDynFormula (f s) n +after act f = DynFormula $ \n -> After (Some act) $ \s -> unDynFormula (f s) n +DynFormula f ||| DynFormula g = DynFormula $ \n -> Alt Angelic (f n) (g n) + -- In formulae, we use only angelic -- choice. But it becomes demonic after one -- step (that is, the choice has been made). forAllQ q f | isEmptyQ q' = ignore - | otherwise = ForAll q' f + | otherwise = DynFormula $ \n -> ForAll q' $ ($n) . unDynFormula . f where q' = quantify q -weight = Weight -toStop = Stopping +weight w f = DynFormula $ Weight w . unDynFormula f +withSize f = DynFormula $ \n -> unDynFormula (f n) n +toStop (DynFormula f) = DynFormula $ Stopping . f done _ = passTest -errorDL s = After (Error s) (const ignore) +errorDL s = DynFormula . const $ After (Error s) (const EmptySpec) -monitorDL = Monitor +monitorDL m (DynFormula f) = DynFormula $ Monitor m . f -always p s = Stopping (p s) ||| Weight 0.1 (p s) ||| AfterAny (always p) +always p s = withSize $ \n -> toStop (p s) ||| p s ||| weight (fromIntegral n) (afterAny (always p)) data DynLogicTest s = BadPrecondition [TestStep s] [Any (Action s)] s | Looping [TestStep s] @@ -144,33 +151,39 @@ class StateModel s => DynLogicModel s where restricted _ = False forAllUniqueScripts :: (DynLogicModel s, Testable a) => - Int -> s -> DynLogic s -> (Actions s -> a) -> Property -forAllUniqueScripts n s d k = case generate chooseUniqueNextStep d n s 500 [] of + Int -> s -> DynFormula s -> (Actions s -> a) -> Property +forAllUniqueScripts n s f k = + QC.withSize $ \sz -> let d = unDynFormula f sz in + case generate chooseUniqueNextStep d n s 500 [] of Nothing -> counterexample "Generating Non-unique script in forAllUniqueScripts" False Just test -> validDLTest d test .&&. (applyMonitoring d test . property $ k (scriptFromDL test)) forAllScripts :: (DynLogicModel s, Testable a) => - DynLogic s -> (Actions s -> a) -> Property -forAllScripts d k = forAllMappedScripts id id d k + DynFormula s -> (Actions s -> a) -> Property +forAllScripts f k = + forAllMappedScripts id id f k forAllScripts_ :: (DynLogicModel s, Testable a) => - DynLogic s -> (Actions s -> a) -> Property -forAllScripts_ d k = + DynFormula s -> (Actions s -> a) -> Property +forAllScripts_ f k = + QC.withSize $ \n -> let d = unDynFormula f n in forAll (sized $ generateDLTest d) $ withDLScript d k forAllMappedScripts :: (DynLogicModel s, Testable a, Show rep) => - (rep -> DynLogicTest s) -> (DynLogicTest s -> rep) -> DynLogic s -> (Actions s -> a) -> Property -forAllMappedScripts to from d k = + (rep -> DynLogicTest s) -> (DynLogicTest s -> rep) -> DynFormula s -> (Actions s -> a) -> Property +forAllMappedScripts to from f k = + QC.withSize $ \n -> let d = unDynFormula f n in forAllShrink (Smart 0 <$> (sized $ (from<$>) . generateDLTest d)) (shrinkSmart ((from<$>) . shrinkDLTest d . to)) $ \(Smart _ script) -> withDLScript d k (to script) forAllMappedScripts_ :: (DynLogicModel s, Testable a, Show rep) => - (rep -> DynLogicTest s) -> (DynLogicTest s -> rep) -> DynLogic s -> (Actions s -> a) -> Property -forAllMappedScripts_ to from d k = + (rep -> DynLogicTest s) -> (DynLogicTest s -> rep) -> DynFormula s -> (Actions s -> a) -> Property +forAllMappedScripts_ to from f k = + QC.withSize $ \n -> let d = unDynFormula f n in forAll (sized $ (from<$>) . generateDLTest d) $ withDLScript d k . to @@ -178,11 +191,13 @@ withDLScript :: (DynLogicModel s, Testable a) => DynLogic s -> (Actions s -> a) withDLScript d k test = validDLTest d test .&&. (applyMonitoring d test . property $ k (scriptFromDL test)) -withDLScriptPrefix :: (DynLogicModel s, Testable a) => DynLogic s -> (Actions s -> a) -> DynLogicTest s -> Property -withDLScriptPrefix d k test = - validDLTest d test' .&&. (applyMonitoring d test' . property $ k (scriptFromDL test')) - where +withDLScriptPrefix :: (DynLogicModel s, Testable a) => DynFormula s -> (Actions s -> a) -> DynLogicTest s -> Property +withDLScriptPrefix f k test = + QC.withSize $ \n -> + let d = unDynFormula f n test' = unfailDLTest d test + in + validDLTest d test' .&&. (applyMonitoring d test' . property $ k (scriptFromDL test')) generateDLTest :: DynLogicModel s => DynLogic s -> Int -> Gen (DynLogicTest s) generateDLTest d size = generate chooseNextStep d 0 (initialStateFor d) size [] diff --git a/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic/Monad.hs b/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic/Monad.hs index 8261c1f7cf..1817fa0488 100644 --- a/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic/Monad.hs +++ b/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic/Monad.hs @@ -8,6 +8,7 @@ module Test.QuickCheck.DynamicLogic.Monad , anyActions_ , stopping , weight + , getSize , getModelStateDL , assert , assertModel @@ -34,12 +35,12 @@ import Test.QuickCheck.DynamicLogic qualified as DL import Test.QuickCheck.DynamicLogic.Quantify import Test.QuickCheck.StateModel -import Test.QuickCheck +import Test.QuickCheck hiding (getSize) -- | The `DL` monad provides a nicer interface to dynamic logic formulae than the plain API. --- It's a continuation monad producing a `DL.DynLogic` formula, with a state component threaded +-- It's a continuation monad producing a `DL.DynFormula` formula, with a state component threaded -- through. -newtype DL s a = DL { unDL :: s -> (a -> s -> DL.DynLogic s) -> DL.DynLogic s } +newtype DL s a = DL { unDL :: s -> (a -> s -> DL.DynFormula s) -> DL.DynFormula s } deriving (Functor) instance Applicative (DL s) where @@ -57,11 +58,13 @@ anyAction :: DL s () anyAction = DL $ \ _ k -> DL.afterAny $ k () anyActions :: Int -> DL s () -anyActions n = stopping <|> weight (1 / fromIntegral n) - <|> (anyAction >> anyActions n) +anyActions n = stopping <|> pure () + <|> (weight (fromIntegral n) >> anyAction >> anyActions n) +-- average number of actions same as average length of a list anyActions_ :: DL s () -anyActions_ = stopping <|> (anyAction >> anyActions_) +anyActions_ = do n <- getSize + anyActions (n `div` 2 + 1) stopping :: DL s () stopping = DL $ \ s k -> DL.toStop (k () s) @@ -69,6 +72,9 @@ stopping = DL $ \ s k -> DL.toStop (k () s) weight :: Double -> DL s () weight w = DL $ \ s k -> DL.weight w (k () s) +getSize :: DL s Int +getSize = DL $ \s k -> DL.withSize $ \n -> k n s + getModelStateDL :: DL s s getModelStateDL = DL $ \ s k -> k s s @@ -103,7 +109,7 @@ instance Alternative (DL s) where instance MonadFail (DL s) where fail = errorDL -runDL :: s -> DL s () -> DL.DynLogic s +runDL :: s -> DL s () -> DL.DynFormula s runDL s dl = unDL dl s $ \ _ _ -> DL.passTest forAllUniqueDL :: (DL.DynLogicModel s, Testable a) => diff --git a/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic/Utils.hs b/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic/Utils.hs new file mode 100644 index 0000000000..a1ec8ad80a --- /dev/null +++ b/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic/Utils.hs @@ -0,0 +1,7 @@ +module Test.QuickCheck.DynamicLogic.Utils where + +import Test.QuickCheck +import Test.QuickCheck.Property + +withSize :: Testable prop => (Int -> prop) -> Property +withSize f = MkProperty . sized $ unProperty . property . f diff --git a/quickcheck-dynamic/test/Spec/DynamicLogic/RegistryModel.hs b/quickcheck-dynamic/test/Spec/DynamicLogic/RegistryModel.hs index a275c1f62f..f4d1f04646 100644 --- a/quickcheck-dynamic/test/Spec/DynamicLogic/RegistryModel.hs +++ b/quickcheck-dynamic/test/Spec/DynamicLogic/RegistryModel.hs @@ -178,40 +178,40 @@ cleanUp = sequence [try (unregister name) :: IO (Either ErrorCall ()) | name <- allNames++["x"]] -propTest :: DynLogic RegState -> Property +propTest :: DynFormula RegState -> Property propTest d = forAllScripts d prop_Registry -- Generate normal test cases -normalTests :: DynPred s +normalTests :: s -> DynFormula s normalTests _ = passTest ||| afterAny normalTests -loopingTests :: DynPred s +loopingTests :: s -> DynFormula s loopingTests _ = afterAny loopingTests -canSpawn :: RegState -> DynLogic RegState +canSpawn :: RegState -> DynFormula RegState canSpawn _ = after Spawn done -canRegisterA :: DynPred RegState +canRegisterA :: RegState -> DynFormula RegState canRegisterA s | null (tids s) = after Spawn canRegisterA | otherwise = after (Successful $ Register "a" (head (tids s))) done -- test that the registry never contains more than k processes -regLimit :: Int -> DynPred RegState +regLimit :: Int -> RegState -> DynFormula RegState regLimit k s | length (regs s) > k = ignore -- fail? yes, gets stuck at this point | otherwise = passTest ||| afterAny (regLimit k) -- test that we can register a pid that is not dead, if we unregister the name first. -canRegisterUndead :: RegState -> DynLogic RegState +canRegisterUndead :: RegState -> DynFormula RegState canRegisterUndead s | null aliveTs = ignore | otherwise = after (Successful (Register "x" (head aliveTs))) done where aliveTs = tids s \\ dead s -canRegister :: DynPred RegState +canRegister :: RegState -> DynFormula RegState canRegister s | length (regs s) == 5 = ignore -- all names are in use | null (tids s) = after Spawn canRegister @@ -220,23 +220,23 @@ canRegister s after (Successful $ Register name tid) done -canRegisterName :: String -> RegState -> DynLogic RegState +canRegisterName :: String -> RegState -> DynFormula RegState canRegisterName name s = forAllQ (elementsQ availableTids) $ \tid -> after (Successful $ Register name tid) done where availableTids = tids s \\ map snd (regs s) -canReregister :: RegState -> DynLogic RegState +canReregister :: RegState -> DynFormula RegState canReregister s | null (regs s) = ignore | otherwise = forAllQ (elementsQ $ map fst (regs s)) $ \name -> after (Unregister name) (canRegisterName name) -canRegisterName' :: String -> RegState -> DynLogic RegState +canRegisterName' :: String -> RegState -> DynFormula RegState canRegisterName' name s = forAllQ (elementsQ availableTids) $ \tid -> after (Successful $ Register name tid) done where availableTids = (tids s \\ map snd (regs s)) \\ dead s -canReregister' :: DynPred RegState +canReregister' :: RegState -> DynFormula RegState canReregister' s | null (regs s) = toStop $ if null availableTids then after Spawn canReregister'