{-| __Please read the source code to view the tests.__ -} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-unused-top-binds #-} module Language.Nominal.Properties.Examples.IdealisedEUTxOSpec where import Control.Monad (replicateM) import Data.List.NonEmpty (NonEmpty (..)) import Data.Maybe (fromJust, isJust) import Data.Proxy (Proxy (..)) import qualified Data.Set as S import GHC.Generics import Numeric.Partial.Semigroup import Numeric.Partial.Monoid import Test.QuickCheck import Type.Reflection import Language.Nominal.Examples.IdealisedEUTxO import Language.Nominal.NameSet import Language.Nominal.Properties.SpecUtilities (genEvFinMap) import Language.Nominal.Properties.UnifySpec () import Language.Nominal.Unify import Language.Nominal.Equivar import Language.Nominal.Nom import Language.Nominal.Binder instance Arbitrary r => Arbitrary (Input r) where arbitrary = Input <$> arbitrary <*> arbitrary shrink (Input p r) = Input p <$> shrink r -- only shrinking the redeemer, not the position instance Arbitrary (ValTriv r d) where arbitrary = return ValTriv shrink = const [] instance (Eq r, UnifyPerm r, Arbitrary r, Eq d, UnifyPerm d, Arbitrary d) => Arbitrary (ValFin r d) where arbitrary = ValFin <$> genEvFinMap (scale (`div` 3) arbitrary) arbitrary shrink (ValFin f) = ValFin <$> shrink f instance (Arbitrary d, Arbitrary v) => Arbitrary (Output d v) where arbitrary = Output <$> arbitrary <*> arbitrary <*> arbitrary shrink (Output p d v) = (Output p d <$> shrink v) -- shrink the validator ++ ((\d' -> Output p d' v) <$> shrink d) -- shrink the datum instance (Arbitrary r, Arbitrary d, Arbitrary v) => Arbitrary (Transaction r d v) where arbitrary = Transaction <$> arbitrary <*> arbitrary shrink (Transaction is os) = (Transaction is <$> shrink os) ++ (flip Transaction os <$> shrink is) instance (Arbitrary r, Arbitrary d, Arbitrary v) => Arbitrary (Context r d v) where arbitrary = Transaction <$> arbitrary <*> arbitrary newtype ValidTx r d v = ValidTx (Transaction r d v) deriving (Show, Generic) fromValidTx :: ValidTx r d v -> Transaction r d v fromValidTx (ValidTx tx) = tx instance (Arbitrary r, Arbitrary d, Arbitrary v) => Arbitrary (ValidTx r d v) where arbitrary = do tx <- arbitrary return $ ValidTx $ go [tx] where go [] = Transaction [] [] go (tx : txs) | transactionValid tx = tx | otherwise = go $ take 10 $ txs ++ shrink tx shrink (ValidTx tx) = ValidTx <$> shrink tx instance ( Arbitrary r , Support r , Arbitrary d , Support d , Arbitrary v , Validator r d v ) => Arbitrary (Chunk r d v) where arbitrary = sized $ \size -> do l <- choose (0, min limit size) scale (min limit) $ go l where go :: Int -> Gen (Chunk r d v) go 0 = return pzero go l = do valids <- replicateM 5 $ scale (min limit) arbitrary chunk <- go $ pred l go' valids chunk go' [] chunk = return chunk go' (ValidTx tx : txs) chunk = do let c = unsafeSingletonChunk tx let c1 = padd c chunk c2 = padd chunk c case (c1, c2) of (Just chunk', _) -> return chunk' (Nothing , Just chunk') -> return chunk' (Nothing , Nothing) -> go' txs chunk limit :: Int limit = 15 shrink = const [] -- TODO: We need something better here newtype SmallChunk r d v = SmallChunk (Chunk r d v) deriving Show instance Arbitrary (Chunk r d v) => Arbitrary (SmallChunk r d v) where arbitrary = SmallChunk <$> scale (min 7) arbitrary shrink (SmallChunk ch) = SmallChunk <$> shrink ch class Validator r d v => Fixable r d v | v -> r d where -- | Takes a context and an output and modifies the output such that it can be consumed by the context. fixOutput :: Context r d v -> Output d v -> Output d v instance (Support r, Support d) => Fixable r d (Val r d) where fixOutput (Transaction (Input p _ :| _) _) (Output _ d _) = Output p d $ Val $ EvFun $ const True instance (Eq r, UnifyPerm r, Eq d, UnifyPerm d) => Fixable r d (ValFin r d) where fixOutput c@(Transaction (Input p _ :| _) _) (Output _ d (ValFin f)) = Output p d (ValFin $ extEvFinMap (d, c) True f) instance (Support r, Support d) => Fixable r d (ValTriv r d) where fixOutput (Transaction (Input p _ :| _) _) (Output _ d v) = Output p d v fixChunkToBlockchain :: (Arbitrary d, Arbitrary v, Fixable r d v) => Chunk r d v -> Gen (Blockchain r d v) fixChunkToBlockchain chunk | isBlockchain chunk = return $ blockchain chunk | otherwise = do -- Gen monad -- get the utxcs of the chunk, still in a Nom binding let utxcsCh' = utxcsOfChunk chunk -- generate an arbitrary output for each utxc /first/ os' <- replicateM (resAppC length utxcsCh') arbitrary -- :: Gen (Output d v) -- and only /then/ unpack the list of utxcs. Fresh atoms generated are for input-output bindings and should not interfere with atoms in os' let cs = exit $ utxcsCh' -- fix each output so it matches the relevant utxc. let os = map (uncurry fixOutput) (zip cs os') -- make genesis block let genesis = unsafeSingletonChunk (Transaction [] os) -- add it to the chunk and parcel it up as a blockchain -- genesis block is on the right because lists grow from right-to-left in Haskell return . blockchain . fromJust $ chunk `padd` genesis instance ( Arbitrary r , Support r , Arbitrary d , Support d , Arbitrary v , Fixable r d v ) => Arbitrary (Blockchain r d v) where arbitrary = arbitrary >>= fixChunkToBlockchain shrink = const [] -- TODO: We need something better here -- Tests -- TODO: two ways to combine different validator types in one property, one lightweight, one more heavy-handed: -- Given prop_chunkrefl below, you can either manually (or in the actual test suite) do -- -- quickCheck $ prop_chunkrefl atVal -- quickCheck $ prop_chunkrefl atValFin -- -- If you want to automate this, you can use the ValProxy-machinery above, so by doing -- -- quickCheck $ withVal (property . prop_chunkrefl) -- -- a random validator type will be picked for each test. We could extend this to include more different validator types -- and also to include types for redeemers and/or datum if we want. {-- Types 1 type TR = () type TD = () type TV = ValTriv TR TD --} -- Types 2 type TR = Int type TD = Int type TV = ValFin TR TD -- type TC = Chunk TR TD TV type SmallTC = SmallChunk TR TD TV type TB = Blockchain TR TD TV type TX = ValidTx TR TD TV type TC' v = Chunk TR TD v atValTriv :: Proxy (ValTriv TR TD) atValTriv = Proxy atValFin :: Proxy (ValFin TR TD) atValFin = Proxy type CVal v = (Typeable v, Show v, Arbitrary v, UnifyPerm v, Validator TR TD v) data ValProxy where ValProxy :: CVal v => Proxy v -> ValProxy instance Show ValProxy where show (ValProxy (Proxy :: Proxy v)) = "{" ++ show (typeRep :: TypeRep v) ++ "}" instance Arbitrary ValProxy where arbitrary = elements [ValProxy atValTriv, ValProxy atValFin] shrink = const [] valTriv, valFin :: ValProxy valTriv = ValProxy atValTriv valFin = ValProxy atValFin withVal :: (forall v proxy. CVal v => proxy v -> a) -> ValProxy -> a withVal f (ValProxy p) = f p -- | Sanity check: every arbitrary chunk is indeed a valid chunk. prop_arbitraryChunkIsValid :: TC -> Bool prop_arbitraryChunkIsValid = isChunk -- | Sanity check: every arbitrary chunk is indeed a valid chunk (alternate test). prop_arbitraryChunkIsValid' :: TC -> Bool prop_arbitraryChunkIsValid' = isChunk' -- | Sanity check: every arbitrary chunk is equal to itself (not trivial because 'Eq' instance is nontrivial) prop_arbitraryChunkEqCheck :: TC -> Bool prop_arbitraryChunkEqCheck ch = ch == ch -- | Sanity check: not every chunk is a blockchain (might have UTxIs)! prop_notEveryChunkBlockchain :: TC -> Property prop_notEveryChunkBlockchain c = expectFailure $ isBlockchain c -- | Sanity check: an arbitrary transaction is indeed a valid transaction. prop_arbitraryTxIsValid :: TX -> Bool prop_arbitraryTxIsValid = transactionValid . fromValidTx -- | Sanity check: an arbitrary blockchain is indeed a valid blockchain. prop_arbitraryBlockchainIsValid :: TB -> Bool prop_arbitraryBlockchainIsValid = isBlockchain . getBlockchain -- | Sanity check: arbitrary instance of TB generates a blockchain, which is equal to itself. -- -- /'unifiablePerm' must be used here, because of the local scope./ prop_blockchainToChunkAndBack :: TB -> Bool prop_blockchainToChunkAndBack b = (blockchain . getBlockchain $ b) `unifiablePerm` b -- | Blockchain has no UTxIs prop_blockchainHasNoUTxIs :: TB -> Bool prop_blockchainHasNoUTxIs b = null . utxisOfChunk . getBlockchain $ b -- | Chunk is equal to itself prop_chunkrefl :: CVal v => proxy v -> TC' v -> Bool prop_chunkrefl _ ch = ch == ch -- chunk equality goes through @'chunkEq'@ -- | There are two different chunks prop_chunkneq :: TC -> TC -> Property prop_chunkneq ch ch' = expectFailure $ ch == ch' -- | Empty chunk is prefix of any chunk prop_emptyIsPrefix :: TC -> Bool prop_emptyIsPrefix ch = isPrefixChunk (return pzero) ch -- | The tail of a chunk, is a chunk. prop_chunkTail_is_chunk :: TC -> Property prop_chunkTail_is_chunk ch = chunkLength ch > 0 ==> isJust $ chunkTail ch -- | The tail of a chunk, is a prefix of the original chunk. prop_chunkTail_is_prefix :: TC -> Property prop_chunkTail_is_prefix ch = chunkLength ch > 0 ==> isPrefixChunk (fromJust $ chunkTail ch) ch -- | The tail of a chunk, generating fresh atoms if required genChunkTail :: (UnifyPerm r, UnifyPerm d, UnifyPerm v, Validator r d v) => Chunk r d v -> Maybe (Chunk r d v) genChunkTail = fmap exit . chunkTail -- | The tail of a chunk, is a prefix of the original chunk. Gotcha version, that doesn't have the Nom bindings: expect failure here. prop_chunkTail_is_prefix_gotcha :: TC -> Property prop_chunkTail_is_prefix_gotcha ch = expectFailure $ chunkLength ch > 0 ==> isPrefixChunk (return . fromJust $ genChunkTail ch) ch -- | Plausible-but-wrong @'warningNotChunkTail'@ is wrong: expect failure here. prop_warningNotChunkTail_is_not_chunk :: TC -> Property prop_warningNotChunkTail_is_not_chunk ch = expectFailure $ chunkLength ch > 0 ==> fromJust $ isChunk <$> warningNotChunkTail ch -- prop_warningNotChunkTail_is_not_chunk ch = expectFailure $ chunkLength ch > 0 ==> (isJust . nomTxListToChunk) ((chunkToTxList . fromJust . warningNotChunkTail) ch) -- | /Underbinding/ is when not enough atoms are bound in a chunk prop_underbinding :: TC -> Property prop_underbinding ch = expectFailure $ isChunk $ ch @@! \_ txs -> Chunk (return txs) -- | /Overbinding/ is when too many atoms are bound in a chunk prop_overbinding :: TC -> Property prop_overbinding ch = expectFailure $ isChunk $ restrict (S.toList $ supp ch) ch -- | Gotcha: Fails because loss of information from two Nom bindings. prop_chunkHead_chunkTail_recombine :: TC -> Property prop_chunkHead_chunkTail_recombine ch = expectFailure $ chunkLength ch > 0 ==> unNom $ do -- Nom monad h <- fromJust $ chunkHead ch t <- fromJust $ chunkTail ch return $ Just ch == appendTxChunk h t -- | Succeeds because one Nom binding thus no loss of information. prop_chunkHdTl_recombine :: TC -> Property prop_chunkHdTl_recombine ch = chunkLength ch > 0 ==> unNom $ do -- Nom monad (tx,ch') <- fromJust $ chunkToHdTl ch return $ Just ch == appendTxChunk tx ch' {- prop_chunkTailIsPrefix :: TC -> Property prop_tailIsPrefix ch = let mt = chunkTail ch in isJust mt ==> isPrefixChunk (fromJust mt) ch -} {- prop_tailIsPrefix :: TC -> Property prop_tailIsPrefix ch = let mt = chunkTail ch in isJust mt ==> unNom $ isPrefixChunk (fromJust mt) ch -} -- | If you reverse the order of transactions in a chunk, it need not be valid prop_reverseIsNotValid :: TC -> Property prop_reverseIsNotValid ch = expectFailure $ isJust (reverseTxsOf ch) prop_subchunksValid :: SmallChunk TR TD TV -> Bool prop_subchunksValid (SmallChunk ch) = and . unNom $ map (isJust . txListToChunk) <$> (subTxListOf ch) -- also subchunk and composition interact -- | If two transactions are apart, they can be validly combined. -- | Corresponds to Lemma 2.14(1) of . prop_apart_is_valid_tx :: TX -> TX -> Bool prop_apart_is_valid_tx vtx1 vtx2 = unNom $ do -- Nom monad let [tx1, tx2] = fromValidTx <$> [vtx1, vtx2] tx2' <- freshen tx2 return . isJust $ txListToChunk [tx1, tx2'] -- | If two chunks are apart, they can be validly combined. -- | Extends Lemma 2.14(1) of . prop_apart_is_valid_ch :: TC -> TC -> Bool prop_apart_is_valid_ch ch1 ch2 = unNom $ do -- Nom monad ch2' <- freshen ch2 return . isJust $ (Just ch1) <> (Just ch2') -- A helper to observe equivalence of chunks observe :: Maybe TC -> Maybe TC -> Maybe TC -> Bool observe mch1 mch2 mch3 = isJust (mch3 <> mch1) == isJust (mch3 <> mch2) && isJust (mch1 <> mch3) == isJust (mch2 <> mch3) -- | If two chunks are apart, they can be validly commuted. -- | Extends Lemma 2.14(1) of . -- We use smaller chunks for speed. prop_chunk_apart_commutes :: SmallTC -> SmallTC -> SmallTC -> Bool prop_chunk_apart_commutes (SmallChunk ch1) (SmallChunk ch2) (SmallChunk ch3) = unNom $ do -- Nom monad ch2' <- freshen ch2 return $ observe ((Just ch1) <> (Just ch2')) ((Just ch2') <> (Just ch1)) (Just ch3) -- | This corresponds to a key result, Lemma 2.14(2) of . prop_validity_fresh :: TC -> Property prop_validity_fresh ch = chunkLength ch > 1 ==> unNom $ do -- Nom monad (tx1, tx2, ch') <- fromJust $ chunkToHdHdTl ch let mch2 = appendTxChunk tx1 ch' return $ (isBlockchain ch && isBlockchain' mch2) <= (tx1 `apart` tx2)