packages feed

project-m36-0.9.8: src/lib/ProjectM36/HashSecurely.hs

-- | A unified class for walking the database structure to produce a hash used for Merkle trees and validation.
{-# LANGUAGE FlexibleInstances, GeneralizedNewtypeDeriving, RankNTypes, ExistentialQuantification, BangPatterns #-}
module ProjectM36.HashSecurely where
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Builder.Scientific as BSB
import ProjectM36.Base
import ProjectM36.Tuple (tupleAttributes, tupleAtoms)
import ProjectM36.Serialise.Base ()
import ProjectM36.IsomorphicSchema
import ProjectM36.Transaction
import qualified Data.HashSet as HS
import qualified ProjectM36.DataConstructorDef as DC
import ProjectM36.MerkleHash
import Data.List (sortOn)
import qualified Data.Map as M
import qualified ProjectM36.TypeConstructorDef as TCons
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Data.Text.Encoding as TE
import qualified Data.Text as T
import qualified Data.UUID as UUID
import qualified Data.Vector as V
import qualified Data.Set as S
import Data.Time.Calendar
import Data.Time.Clock
import Codec.Winery (Serialise)

newtype SecureHash = SecureHash { _unSecureHash :: B.ByteString }
  deriving (Serialise, Show, Eq)

-- run a SHA256 hasher across the necessary data structures
class HashBytes a where
  hashBytes :: a -> SHA256.Ctx -> SHA256.Ctx

instance HashBytes Atom where
  hashBytes atm ctx =
    case atm of
      IntegerAtom i -> up ("IntegerAtom" <> build (BSB.integerDec i))
      IntAtom i -> up ("IntAtom" <> build (BSB.intDec i))
      ScientificAtom s -> up ("ScientificAtom" <> build (BSB.scientificBuilder s))
      DoubleAtom d -> up ("DoubleAtom" <> build (BSB.doubleDec d))
      TextAtom t -> up ("TextAtom" <> TE.encodeUtf8 t)
      DayAtom d -> up ("DayAtom" <> build (BSB.integerDec (toModifiedJulianDay d)))
      DateTimeAtom dt -> up ("DateTimeAtom" <>
                             build (BSB.integerDec (toModifiedJulianDay (utctDay dt)) <>
                              BSB.integerDec (diffTimeToPicoseconds (utctDayTime dt))))
      ByteStringAtom bs -> up ("ByteStringAtom" <> bs)
      BoolAtom b -> up ("BoolAtom" <> if b then "1" else "0")
      UUIDAtom u -> up ("UUIDAtom" <> BL.toStrict (UUID.toByteString u))
      RelationAtom r -> hashBytesL ctx "RelationAtom" [SHash r]
      RelationalExprAtom e -> hashBytesL ctx "RelationalExprAtom" [SHash e]
      ConstructedAtom d typ args ->
          hashBytesL ctx "ConstructedAtom" ([SHash d, SHash typ] <> map SHash args)
      where
        build = BL.toStrict . BSB.toLazyByteString
        up = SHA256.update ctx


instance HashBytes T.Text where
  hashBytes t ctx = SHA256.update ctx (TE.encodeUtf8 t)
  
instance HashBytes Relation where
  hashBytes (Relation attrs tupSet) ctx =
    hashBytesL ctx "Relation" [SHash attrs, SHash tupSet]

data SHash = forall a. HashBytes a => SHash !a

hashBytesL :: Foldable f => SHA256.Ctx -> B.ByteString -> f SHash -> SHA256.Ctx
hashBytesL ctx name = foldr (\(SHash i) ctx'@(SHA256.Ctx !bs) -> bs `seq` hashBytes i ctx') (SHA256.update ctx name)

instance HashBytes a => HashBytes (RelationalExprBase a) where
  hashBytes (MakeRelationFromExprs mAttrs tupleExprs) ctx =
    hashBytesL ctx "MakeRelationFromExprs" [SHash mAttrs, SHash tupleExprs]
  hashBytes (MakeStaticRelation attrs tupSet) ctx = -- blowing up here!
    hashBytesL ctx "MakeStaticRelation" [SHash attrs, SHash tupSet]
--  hashBytes _ ctx = ctx
  hashBytes (ExistingRelation (Relation attrs tupSet)) ctx =
    hashBytesL ctx "ExistingRelation" [SHash tupSet, SHash attrs]
  hashBytes (RelationVariable rvName marker) ctx =
    hashBytesL ctx "RelationVariable" [SHash rvName, SHash marker]
  hashBytes (Project attrNames expr) ctx =
    hashBytesL ctx "Project" [SHash attrNames, SHash expr]
  hashBytes (Union exprA exprB) ctx =
    hashBytesL ctx "Union" [SHash exprA, SHash exprB]
  hashBytes (Join exprA exprB) ctx =
    hashBytesL ctx "Join" [SHash exprA, SHash exprB]
  hashBytes (Rename nameA nameB expr) ctx =
    hashBytesL ctx "Rename" [SHash nameA, SHash nameB, SHash expr]
  hashBytes (Difference exprA exprB) ctx =
    hashBytesL ctx "Difference" [SHash exprA, SHash exprB]
  hashBytes (Group names name expr) ctx =
    hashBytesL ctx "Group" [SHash names, SHash name, SHash expr]
  hashBytes (Ungroup name expr) ctx =
    hashBytesL ctx "Ungroup" [SHash name, SHash expr]
  hashBytes (Restrict pred' expr) ctx =
    hashBytesL ctx "Restrict" [SHash pred', SHash expr]
  hashBytes (Equals exprA exprB) ctx =
    hashBytesL ctx "Equals" [SHash exprA, SHash exprB]
  hashBytes (NotEquals exprA exprB) ctx =
    hashBytesL ctx "NotEquals" [SHash exprA, SHash exprB]
  hashBytes (Extend ext expr) ctx =
    hashBytesL ctx "Extend" [SHash ext, SHash expr]
  hashBytes (With withExprs expr) ctx =
    hashBytesL ctx "With" (SHash expr: map SHash (sortOn (\(WithNameExpr rv _, _) -> rv) withExprs))

instance HashBytes a => HashBytes (AttributeNamesBase a) where
  hashBytes (AttributeNames s) ctx = hashBytesL ctx "AttributeNames" (map SHash (S.toAscList s))
  hashBytes (InvertedAttributeNames s) ctx = hashBytesL ctx "InvertedAttributeNames" (map SHash (S.toAscList s))
  hashBytes (UnionAttributeNames a b) ctx = hashBytesL ctx "UnionAttributeNames" [SHash a, SHash b]
  hashBytes (IntersectAttributeNames a b) ctx = hashBytesL ctx "IntersectAttributeNames" [SHash a, SHash b]
  hashBytes (RelationalExprAttributeNames r) ctx = hashBytesL ctx "RelationalExprAttributeNames" [SHash r]

instance HashBytes a => HashBytes (ExtendTupleExprBase a) where
  hashBytes (AttributeExtendTupleExpr name expr) ctx =
    hashBytesL ctx "AttributeExtendTupleExpr" [SHash name, SHash expr]

instance HashBytes a => HashBytes (WithNameExprBase a) where
  hashBytes (WithNameExpr rv marker) ctx = hashBytesL ctx "WithNameExpr" [SHash rv, SHash marker]
  
instance HashBytes GraphRefTransactionMarker where
  hashBytes (TransactionMarker tid) ctx = SHA256.update ctx (BL.toStrict ("TransactionMarker" <> UUID.toByteString tid))
  hashBytes UncommittedContextMarker ctx = SHA256.update ctx "UncommittedContextMarker"


instance HashBytes a => HashBytes (TupleExprBase a) where
  hashBytes (TupleExpr exprMap) ctx =
    foldr (\(attrName, atomExpr) ctx' ->
             hashBytesL ctx' "TupleExpr" [SHash attrName, SHash atomExpr]) 
    ctx (M.toAscList exprMap)

instance HashBytes a => HashBytes (AtomExprBase a) where
  hashBytes atomExpr ctx =
    case atomExpr of
      (AttributeAtomExpr a) -> hashBytesL ctx "AttributeAtomExpr" [SHash a]
      (NakedAtomExpr a) -> hashBytesL ctx "NakedAtomExpr" [SHash a]
      (FunctionAtomExpr fname args marker) ->
        hashBytesL ctx "FunctionAtomExpr" $ [SHash fname, SHash marker] <> map SHash args
      (RelationAtomExpr r) -> hashBytesL ctx "RelationAtomExpr" [SHash r]
      (ConstructedAtomExpr dConsName args marker) ->
        hashBytesL ctx "ConstructedAtomExpr" ([SHash dConsName, SHash marker] <> map SHash args)

instance HashBytes () where
  hashBytes () ctx = SHA256.update ctx "()"

instance HashBytes AtomType where
  hashBytes typ ctx =
    case typ of
      IntAtomType -> hashb "IntAtomType"
      IntegerAtomType -> hashb "IntegerAtomType"
      ScientificAtomType -> hashb "ScientificAtomType"
      DoubleAtomType -> hashb "DoubleAtomType"
      TextAtomType -> hashb "TextAtomType"
      DayAtomType -> hashb "DayAtomType"
      DateTimeAtomType -> hashb "DateTimeAtomType"
      ByteStringAtomType -> hashb "ByteStringAtomType"
      BoolAtomType -> hashb "BoolAtomType"
      UUIDAtomType -> hashb "UUIDAtomType"
      RelationAtomType attrs -> hashBytesL ctx "RelationAtomType" (V.map SHash (attributesVec attrs))
      ConstructedAtomType tConsName tvarMap -> hashBytesL ctx "ConstructedAtomType" (SHash tConsName : map SHash (M.toAscList tvarMap))
      RelationalExprAtomType -> hashb "RelationalExprAtomType"
      TypeVariableType tvn -> hashBytesL ctx "TypeVariableType" [SHash tvn]
    where
      hashb = SHA256.update ctx

instance HashBytes Attributes where
  hashBytes attrs ctx =
    hashBytesL ctx "Attributes" (V.map SHash (attributesVec attrs))

instance HashBytes RelationTupleSet where
  hashBytes tupSet ctx =
    hashBytesL ctx "RelationTupleSet" (map SHash (asList tupSet))

instance HashBytes a => HashBytes (Maybe [AttributeExprBase a]) where
  hashBytes Nothing ctx = SHA256.update ctx "MaybeAttributeExprBaseNothing"
  hashBytes (Just exprs) ctx =
    hashBytesL ctx "MaybeAttributeExprBase" (map SHash exprs)

instance HashBytes a => HashBytes (TupleExprsBase a) where
  hashBytes (TupleExprs marker tupleExprs) ctx =
    hashBytesL ctx "TupleExprs" (SHash marker : map SHash tupleExprs)

instance HashBytes Attribute where
  hashBytes (Attribute name typ) ctx =
    hashBytesL ctx "Attribute" [SHash name, SHash typ]

instance (HashBytes a, HashBytes b) => HashBytes (a, b) where
  hashBytes (a,b) ctx =
    hashBytesL ctx "HTuple" [SHash a, SHash b]

instance HashBytes RelationTuple where
  hashBytes tup ctx =
    hashBytesL ctx "RelationTuple" (V.cons (SHash (tupleAttributes tup)) (V.map SHash (tupleAtoms tup)))

instance HashBytes a => HashBytes (AttributeExprBase a) where
  hashBytes (AttributeAndTypeNameExpr aname tcons marker) ctx =
    hashBytesL ctx "AttributeAndTypeNameExpr" [SHash aname, SHash tcons, SHash marker]
  hashBytes (NakedAttributeExpr attr) ctx =
    hashBytesL ctx "NakedAttributeExpr" [SHash attr]

instance HashBytes TypeConstructor where
  hashBytes tcons ctx =
    case tcons of
      ADTypeConstructor tName args ->
        hashBytesL ctx "ADTypeConstructor" (SHash tName : map SHash args)
      PrimitiveTypeConstructor tConsName typ ->
        hashBytesL ctx "PrimitiveTypeConstructor" [SHash tConsName, SHash typ]
      RelationAtomTypeConstructor attrExprs ->
        hashBytesL ctx "RelationAtomTypeConstructor" (map SHash attrExprs)
      TypeVariable tv ->
        hashBytesL ctx "TypeVariable" [SHash tv]

instance HashBytes TransactionId where
  hashBytes tid ctx = SHA256.update ctx ("TransactionId" <> BL.toStrict (UUID.toByteString tid))

instance HashBytes Schema where
  hashBytes (Schema morphs) ctx =
    hashBytesL ctx "Schema" (map SHash (sortOn sortIso morphs))
    where
      sortIso iso = mconcat (isomorphInRelVarNames iso)
                            

instance HashBytes SchemaIsomorph where
  hashBytes (IsoRestrict r p (a,b)) ctx =
    hashBytesL ctx "IsoRestrict" [SHash r, SHash p, SHash a, SHash b]
  hashBytes (IsoRename a b) ctx =
    hashBytesL ctx "IsoRename" [SHash a, SHash b]
  hashBytes (IsoUnion (a,b) p r) ctx =
    hashBytesL ctx "IsoUnion" [SHash a, SHash b, SHash p, SHash r]

instance HashBytes a => HashBytes (RestrictionPredicateExprBase a) where
  hashBytes TruePredicate ctx = SHA256.update ctx "TruePredicate"
  hashBytes (AndPredicate a b) ctx = hashBytesL ctx "AndPredicate" [SHash a, SHash b]
  hashBytes (OrPredicate a b) ctx = hashBytesL ctx "OrPredicate" [SHash a, SHash b]
  hashBytes (NotPredicate a) ctx = hashBytesL ctx "NotPredicate" [SHash a]
  hashBytes (RelationalExprPredicate e) ctx = hashBytesL ctx "RelationalExprPredicate" [SHash e]
  hashBytes (AtomExprPredicate a) ctx = hashBytesL ctx "AtomExprPredicate" [SHash a]
  hashBytes (AttributeEqualityPredicate a e) ctx = hashBytesL ctx "AttributeEqualityPredicate" [SHash a, SHash e]


instance HashBytes MerkleHash where
  hashBytes h ctx =
    SHA256.update ctx (_unMerkleHash h)

instance HashBytes UTCTime where
  hashBytes tim ctx =
    SHA256.update ctx (BL.toStrict ("UTCTime" <>
                                    BSB.toLazyByteString (BSB.integerDec (toModifiedJulianDay (utctDay tim))) <>
                                    BSB.toLazyByteString (BSB.integerDec (diffTimeToPicoseconds (utctDayTime tim)))))

instance HashBytes DatabaseContext where
  hashBytes db ctx =
    hashBytesL ctx "DatabaseContext" [SHash (inclusionDependencies db),
                                      SHash (relationVariables db),
                                      SHash (notifications db),
                                      SHash (typeConstructorMapping db),
                                      SHash (atomFunctions db),
                                      SHash (dbcFunctions db)]

instance HashBytes InclusionDependencies where
  hashBytes incDeps ctx =
    hashBytesL ctx "InclusionDependencies" (map SHash (M.toAscList incDeps))

instance HashBytes RelationVariables where
  hashBytes rvs ctx =
    hashBytesL ctx "RelationVariables" (map SHash (M.toAscList rvs))

instance HashBytes Notifications where
  hashBytes nots ctx =
    hashBytesL ctx "Notifications" (map SHash (M.toAscList nots))

instance HashBytes TypeConstructorMapping where
  hashBytes tConsMap ctx =
    hashBytesL ctx "TypeConstructorMapping" (map SHash (sortOn (TCons.name . fst) tConsMap))

instance HashBytes AtomFunctions where
  hashBytes afuncs ctx =
    hashBytesL ctx "AtomFunctions" (map SHash (sortOn funcName (HS.toList afuncs)))

instance HashBytes AtomFunction where
  hashBytes func ctx =
    hashBytesL ctx "AtomFunction" (SHash (funcName func):
                                   SHash (funcBody func):
                                   map SHash (funcType func))

instance HashBytes DatabaseContextFunction where
  hashBytes func ctx =
    hashBytesL ctx "DatabaseContextFunction" (SHash (funcName func):
                                              SHash (funcBody func):
                                              map SHash (funcType func))

instance HashBytes DatabaseContextFunctions where
  hashBytes dbcfuncs ctx =
    hashBytesL ctx "DatabaseContextFunctions" (map SHash (sortOn funcName (HS.toList dbcfuncs)))

instance HashBytes InclusionDependency where    
  hashBytes (InclusionDependency exprA exprB) ctx =
    hashBytesL ctx "InclusionDependency" [SHash exprA, SHash exprB]

instance HashBytes Notification where
  hashBytes notif ctx =
    hashBytesL ctx "Notification" [SHash (changeExpr notif),
                                   SHash (reportOldExpr notif),
                                   SHash (reportNewExpr notif)]

instance HashBytes DataConstructorDef where
  hashBytes (DataConstructorDef dConsName args) ctx =
    hashBytesL ctx "DataConstructorDef" (SHash dConsName : map SHash args)

instance HashBytes [DataConstructorDef] where
  hashBytes defs ctx =
    hashBytesL ctx "DataConstructoDefList" (map SHash (sortOn DC.name defs))

instance HashBytes TypeConstructorDef where
  hashBytes (ADTypeConstructorDef tCons args) ctx =
    hashBytesL ctx "ADTypeConstructorDef" (SHash tCons: map SHash args)
  hashBytes (PrimitiveTypeConstructorDef tCons typ) ctx =
    hashBytesL ctx "PrimitiveTypeConstructorDef" [SHash tCons, SHash typ]

instance HashBytes (FunctionBody a) where
  hashBytes (FunctionScriptBody s _) ctx = hashBytesL ctx "FunctionScriptBody" [SHash s]
  hashBytes (FunctionBuiltInBody _) ctx = SHA256.update ctx "FunctionBuiltInBody"
  hashBytes (FunctionObjectLoadedBody a b c _) ctx = hashBytesL ctx "FunctionObjectLoadedBody" (map (SHash . T.pack) [a,b,c])

instance HashBytes DataConstructorDefArg where
  hashBytes (DataConstructorDefTypeConstructorArg tCons) ctx =
    hashBytesL ctx "DataConstructorDefTypeConstructorArg" [SHash tCons]
  hashBytes (DataConstructorDefTypeVarNameArg tv) ctx =
    hashBytesL ctx "DataConstructorDefTypeVarNameArg" [SHash tv]

instance HashBytes (M.Map RelVarName Relation) where
  hashBytes m ctx =
    hashBytesL ctx "rvtypes" (map SHash (M.toAscList m))

-- | Hash a transaction within its graph context to create a Merkle hash for it.
hashTransaction :: Transaction -> S.Set Transaction -> MerkleHash
hashTransaction trans parentTranses = MerkleHash (SHA256.finalize newHash)
  where
    newHash = hashBytesL SHA256.init "Transaction" (map SHash transIds <>
                                         map SHash (M.toAscList (subschemas trans)) <>
                                         map SHash parentMerkleHashes <>
                                         [SHash tstamp,
                                         SHash (concreteDatabaseContext trans)]
                                                   )
    tstamp = stamp (transactionInfo trans)
    parentMerkleHashes = map getMerkleHash (S.toAscList parentTranses)
    getMerkleHash t = merkleHash (transactionInfo t)
    transIds = transactionId trans : S.toAscList (parentIds trans)

-- | Return a hash of just DDL-specific (schema) attributes. This is useful for determining if a client has the appropriate updates needed to work with the current schema.
mkDDLHash :: DatabaseContext -> M.Map RelVarName Relation -> SecureHash
mkDDLHash ctx rvtypemap = do
  -- we cannot merely hash the relational representation of the type because the order of items matters when hashing
  -- registered queries are not included here because a client could be compatible with a schema even if the queries are not registered. The client should validate registered query state up-front. Perhaps there should be another hash for registered queries.
  SecureHash $ SHA256.finalize $ hashBytesL SHA256.init "DDLHash" [SHash (inclusionDependencies ctx),
                                                                    SHash (atomFunctions ctx),
                                                                    SHash (dbcFunctions ctx),
                                                                    SHash (typeConstructorMapping ctx),
                                                                    SHash rvtypemap]