packages feed

dfinity-radix-tree-0.6.0: test/Properties.hs

{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

{-# OPTIONS -Wall        #-}
{-# OPTIONS -Wno-orphans #-}

module Properties
  ( tests
  )
where

import Control.Monad.Identity
import Control.Monad.State.Strict
import Data.Bifunctor
import Data.ByteString.Char8      as Strict
import Data.ByteString.Short
import Data.List                  as List
import Data.Map                   as Map
import Data.Maybe
import Test.QuickCheck
import Test.Tasty
import Test.Tasty.QuickCheck

import DFINITY.RadixTree
import Types

instance Arbitrary ByteString where
  arbitrary = Strict.pack <$> arbitrary

{-
Here we provide a test to show that the radix tree satisfies the basic
properties of an associative array. Random sequences of insert, delete, and
lookup operations are generated and applied to both the radix tree and a
reference implementation of an associative array. We then compare the results
for equality.
-}

genOps :: Gen [Op]
genOps = do
  len <- getSmall . getPositive <$> arbitrary
  key <- elements <$> vectorOf len arbitrary
  listOf $ frequency
    [ (2, Insert <$> key <*> arbitrary)
    , (2, Delete <$> key)
    , (4, Lookup <$> key <*> pure Nothing)
    , (1, pure $ Merkleize Nothing)
    ]

runReference :: [Op] -> [Maybe ByteString]
runReference ops = snd $ List.mapAccumL go Map.empty ops
 where
  go accum (Insert key value) = (Map.insert key value accum, Nothing)
  go accum (Delete key      ) = (Map.delete key accum, Nothing)
  go accum (Lookup key _    ) = (accum, Map.lookup key accum)
  go accum (Merkleize _     ) = (accum, Nothing)

runRadixTree :: [Op] -> [Maybe ByteString]
runRadixTree ops = flip evalState Map.empty $ unMapDB $ initTree >>= go ops
 where
  go []                           _    = return []
  go (Insert key value : residue) tree = do
    tree' <- insertRadixTree key value tree
    (:) Nothing <$> go residue tree'
  go (Delete key : residue) tree = do
    tree' <- deleteRadixTree key tree
    (:) Nothing <$> go residue tree'
  go (Lookup key _ : residue) tree = do
    result <- lookupRadixTree key tree
    case result of
      Nothing             -> (Nothing :) <$> go residue tree
      Just (value, tree') -> (Just value :) <$> go residue tree'
  go (Merkleize _ : residue) tree = do
    tree' <- snd <$> merkleizeRadixTree tree
    (:) Nothing <$> go residue tree'

prop_lookup :: Property
prop_lookup = forAll genOps $ \ops -> runReference ops === runRadixTree ops

{-
Here we provide a test to show that the contents of a radix tree is uniquely
defined by its root hash, regardless of the order of operations applied. Random
sequences sequences of insert, delete, lookup, and merkelize operations are
generated and applied in random order to the same tree. The final root hashs are
then calculated and tested for equality.
-}

genMap :: Gen (Map ByteString ByteString)
genMap = fromList . List.map convert . toList <$> arbitrary
  where convert = bimap Strict.pack Strict.pack

genOpsForMap :: Map ByteString ByteString -> Gen [Op]
genOpsForMap m = do
  inserts <- forM (toList m) $ \(k, v) -> do
    ops <- genOpsForKey k
    return $ ops ++ [Insert k v]
  -- Extra keys (which we delete as the last action)
  extra_keys <- listOf $ arbitrary `suchThat` (`Map.notMember` m)
  deletes    <- forM extra_keys $ \k -> do
    ops <- genOpsForKey k
    return $ ops ++ [Delete k]
  interleaves (inserts ++ deletes)
 where
  genOpsForKey k = listOf (oneof [Insert k <$> arbitrary, pure (Delete k)])

interleaves :: [[a]] -> Gen [a]
interleaves xss = frequency'
  [ (List.length (x : xs), (x :) <$> interleaves (xs : rest))
  | ((x : xs) : rest) <- rots xss
  ]
 where
  frequency' [] = return []
  frequency' xs = frequency xs
  rots xs = List.tail $ List.zipWith (++) (List.tails xs) (List.inits xs)

prop_stateRoot :: Property
prop_stateRoot = forAll genMap $ \m -> forAll (genOpsForMap m)
  $ \ops1 -> forAll (genOpsForMap m) $ \ops2 -> run ops1 === run ops2
 where
  run :: [Op] -> ByteString
  run ops0 = evalState (unMapDB $ initTree >>= go ops0) Map.empty
   where
    go :: [Op] -> RadixTree () -> PureMapDB ByteString
    go []                 t = fromShort . fst <$> merkleizeRadixTree t
    go (Insert k v : ops) t = insertRadixTree k v t >>= go ops
    go (Delete k   : ops) t = deleteRadixTree k t >>= go ops
    go (Lookup _ _ : _  ) _ = error "no lookup in this test please"
    go _                  _ = error "Cannot be reached (GADT pattern match)"

-- | 'createRadixProof' should find the same value as 'lookupRadixTree'
prop_proofLookup :: Property
prop_proofLookup = forAll arbitrary $ \keyStr ->
  let key      = Strict.pack keyStr
      proofRes = lookupProof key
      radixRes = lookupRadix key
  in  isJust proofRes && isJust radixRes && proofRes == radixRes
 where
  run = flip (evalState . unMapDB) Map.empty
  lookupProof k = run $ do
    tree       <- initTree
    tree'      <- insertRadixTree k (mappend k "suffix") tree
    (_, mTree) <- merkleizeRadixTree tree'
    fmap (getValue . fst) <$> createRadixProof k mTree

  lookupRadix k = run $ do
    tree  <- initTree
    tree' <- insertRadixTree k (mappend k "suffix") tree
    fmap fst <$> lookupRadixTree k tree'

prop_proofValid :: Property
prop_proofValid = forAll arbitrary $ \keyPrefixStr ->
  let keyPrefix          = Strict.pack keyPrefixStr
      (key, proof, root) = lookupProof keyPrefix
  in  case proof of
        Nothing -> False
        Just p  -> verifyRadixProof key root p
 where
  run = flip (evalState . unMapDB) Map.empty
  lookupProof kp = run $ do
    tree   <- initTree
    tree'  <- insertRadixTree (mappend kp "1") "body1" tree
    tree'' <- insertRadixTree (mappend kp "12") "body2" tree'
    let key = mappend kp "123"
    tree'''       <- insertRadixTree key "body3" tree''
    (root, mTree) <- merkleizeRadixTree tree'''
    proof         <- fmap fst <$> createRadixProof key mTree
    pure (key, proof, root)

tests :: TestTree
tests = testGroup
  "quickcheck"
  [ testProperty "lookup"                             prop_lookup
  , testProperty "contents"                           prop_stateRoot
  , testProperty "createRadixProof ~ lookupRadixTree" prop_proofLookup
  , testProperty "createRadixProofs are always valid" prop_proofValid
  ]

initTree :: PureMapDB (RadixTree ())
initTree = createRadixTree 2028 Nothing ()