diff --git a/Changelog.markdown b/Changelog.markdown
--- a/Changelog.markdown
+++ b/Changelog.markdown
@@ -1,3 +1,7 @@
+# 2020-10-05 (v0.8.1)
+
+* support GHC 8.10
+
 # 2020-05-25 (v0.8)
 
 * refactor backend to maximize laziness: [An Architecture for Data Independence](docs/data_independence.markdown)
diff --git a/README.markdown b/README.markdown
--- a/README.markdown
+++ b/README.markdown
@@ -6,6 +6,7 @@
 [![Hackage dependency status](https://img.shields.io/hackage-deps/v/project-m36.svg)](http://packdeps.haskellers.com/feed?needle=project-m36)
 [![Build status](https://travis-ci.org/agentm/project-m36.svg?branch=master)](https://travis-ci.org/agentm/project-m36)
 [![Windows Build status](https://ci.appveyor.com/api/projects/status/q7jyddd6dy1ibqdo/branch/master?svg=true)](https://ci.appveyor.com/project/agentm/project-m36)
+[![Github Workflow status](https://github.com/agentm/project-m36/workflows/CI/badge.svg)](https://github.com/agentm/project-m36/actions?query=workflow%3ACI)
 
 
 *Software can always be made faster, but rarely can it be made more correct.*
@@ -14,6 +15,12 @@
 
 Project:M36 implements a relational algebra engine as inspired by the writings of Chris Date.
 
+## Quick Install
+
+Project:M36 can be downloaded and run via docker, which supports Windows 10, macOS, and Linux.
+
+Run `docker run -it projectm36/project-m36 tutd` to start the TutorialD command line interface.
+
 ## Description
 
 Unlike most database management systems (DBMS), Project:M36 is opinionated software which adheres strictly to the mathematics of the relational algebra. The purpose of this adherence is to prove that software which implements mathematically-sound design principles reaps benefits in the form of code clarity, consistency, performance, and future-proofing.
@@ -78,6 +85,7 @@
 1. [Isomorphic Schemas](docs/isomorphic_schemas.markdown)
 1. [Replication](docs/replication.markdown)
 1. [Basic Operator Benchmarks](https://rawgit.com/agentm/project-m36/master/docs/basic_benchmarks.html)
+1. [Merkle Transaction Hashes](docs/merkle_transaction_graph.markdown)
 
 ### Integrations
 
diff --git a/examples/DerivingCustomTupleable.hs b/examples/DerivingCustomTupleable.hs
new file mode 100644
--- /dev/null
+++ b/examples/DerivingCustomTupleable.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE DeriveGeneric, DerivingVia, GeneralizedNewtypeDeriving, TypeOperators, DataKinds, OverloadedStrings #-}
+
+import ProjectM36.Tupleable.Deriving
+import ProjectM36.Atomable (Atomable)
+import Control.DeepSeq (NFData)
+import Data.Binary (Binary)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+newtype BlogId = BlogId { getBlogId :: Int }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving newtype (Num)
+
+instance NFData BlogId
+instance Binary BlogId
+instance Atomable BlogId
+
+data Blog = Blog
+  { blogId :: BlogId
+  , blogTitle :: Text
+  , blogAuthorName :: Text
+  }
+  deriving stock (Show, Generic)
+  deriving (Tupleable)
+    via Codec (Field (DropPrefix "blog" >>> CamelCase)) Blog
+
+data Comment = Comment
+  { commentAuthorName :: Text
+  , commentComment :: Text
+  , commentFor :: BlogId
+  }
+  deriving stock (Show, Generic)
+  deriving (Tupleable)
+    via Codec (Field (DropPrefix "comment" >>> CamelCase)) Comment
+
+main :: IO ()
+main = do
+  let exampleBlog = Blog
+        { blogId = 0
+        , blogTitle = "Cat Pics"
+        , blogAuthorName = "Alice"
+        }
+      exampleComment = Comment
+        { commentAuthorName = "Bob"
+        , commentComment = "great"
+        , commentFor = 0
+        }
+  print exampleBlog
+  print (toTuple exampleBlog)
+  putStrLn ""
+  print exampleComment
+  print (toTuple exampleComment)
diff --git a/project-m36.cabal b/project-m36.cabal
--- a/project-m36.cabal
+++ b/project-m36.cabal
@@ -1,5 +1,5 @@
 Name: project-m36
-Version: 0.8
+Version: 0.8.1
 License: PublicDomain
 Build-Type: Simple
 Homepage: https://github.com/agentm/project-m36
@@ -34,9 +34,9 @@
      Default: True
 
 Library
-    Build-Depends: base>=4.8 && < 4.14, ghc-paths, mtl, containers, unordered-containers, hashable, haskeline, directory, MonadRandom, random-shuffle, uuid >= 1.3.12, cassava >= 0.4.5.1 && < 0.6, text, bytestring, deepseq, deepseq-generics, vector, parallel, monad-parallel, exceptions, transformers, gnuplot, binary, filepath, zlib, directory, vector-binary-instances, temporary, stm, time, hashable-time, old-locale, rset, attoparsec, either, base64-bytestring, data-interval, extended-reals, network-transport, aeson >= 1.1, path-pieces, conduit, resourcet, http-api-data, semigroups, QuickCheck, quickcheck-instances, distributed-process-client-server >= 0.2.3, distributed-process >= 0.7.4, distributed-process-extras >= 0.3.2, distributed-process-async >= 0.2.4.1, network-transport-tcp >= 0.6.0, network-transport, list-t, stm-containers >= 0.2.15, foldl, optparse-applicative, Glob, cryptohash-sha256
+    Build-Depends: base>=4.8 && < 4.15, ghc-paths, mtl, containers, unordered-containers, hashable, haskeline, directory, MonadRandom, random-shuffle, uuid >= 1.3.12, cassava >= 0.4.5.1 && < 0.6, text, bytestring, deepseq, deepseq-generics, vector, parallel, monad-parallel, exceptions, transformers, gnuplot, binary, filepath, zlib, directory, vector-binary-instances, temporary, stm, time, hashable-time, old-locale, rset, attoparsec, either, base64-bytestring, data-interval, extended-reals, network-transport, aeson >= 1.1, path-pieces, conduit, resourcet, http-api-data, semigroups, QuickCheck, quickcheck-instances, distributed-process-client-server >= 0.2.3, distributed-process >= 0.7.4, distributed-process-extras >= 0.3.2, distributed-process-async >= 0.2.4.1, network-transport-tcp >= 0.6.0, network-transport, list-t, stm-containers >= 0.2.15, foldl, optparse-applicative, Glob, cryptohash-sha256, text-manipulate >= 0.2.0.1 && < 0.3
     if flag(haskell-scripting)
-        Build-Depends: ghc >= 8.2 && < 8.9
+        Build-Depends: ghc >= 8.2 && < 8.11
         CPP-Options: -DPM36_HASKELL_SCRIPTING
     if impl(ghc>= 8) && flag(haskell-scripting)
       build-depends:
@@ -79,6 +79,7 @@
                      ProjectM36.Relation.Parse.CSV,
                      ProjectM36.IsomorphicSchema,
                      ProjectM36.InclusionDependency,
+                     ProjectM36.MerkleHash,
                      ProjectM36.Client,
                      ProjectM36.Client.Simple,
                      ProjectM36.Persist,
@@ -88,6 +89,7 @@
                      ProjectM36.AtomFunctions.Primitive,
                      ProjectM36.Atomable,
                      ProjectM36.Tupleable,
+                     ProjectM36.Tupleable.Deriving,
                      ProjectM36.DataConstructorDef,
                      ProjectM36.DataTypes.Basic,
                      ProjectM36.DataTypes.Sorting,
@@ -138,7 +140,7 @@
 
 Executable tutd
     if flag(haskell-scripting)
-        Build-Depends: ghc >= 8.2 && < 8.9
+        Build-Depends: ghc >= 8.2 && < 8.11
     Build-Depends: base >=4.8 && <5.0,
                    ghc-paths,
                    project-m36,
@@ -203,7 +205,7 @@
 
 Executable project-m36-server
     if flag(haskell-scripting)
-        Build-Depends: ghc >= 8.2 && < 8.9
+        Build-Depends: ghc >= 8.2 && < 8.11
     Build-Depends: base,
                    ghc-paths,
                    project-m36,
@@ -407,6 +409,16 @@
     Default-Extensions: OverloadedStrings
     Build-Depends: base, HUnit, Cabal, containers, hashable, unordered-containers, mtl, vector, vector-binary-instances, time, hashable-time, bytestring, uuid, stm, deepseq, deepseq-generics, binary, parallel, cassava, attoparsec, gnuplot, directory, temporary, haskeline, megaparsec, text, base64-bytestring, data-interval, filepath, transformers, stm-containers, list-t, aeson, path-pieces, either, conduit, http-api-data, template-haskell, ghc, ghc-paths, project-m36
     Main-Is: examples/CustomTupleable.hs
+    GHC-Options: -Wall -threaded
+
+Executable Example-DerivingCustomTupleable
+    if impl(ghc>=8.6)
+      Buildable: True
+    else
+      Buildable: False
+    Default-Language: Haskell2010
+    Build-Depends: base, text, deepseq, binary, project-m36
+    Main-Is: examples/DerivingCustomTupleable.hs
     GHC-Options: -Wall -threaded
 
 Test-Suite test-scripts
diff --git a/src/bin/ProjectM36/Client/Json.hs b/src/bin/ProjectM36/Client/Json.hs
--- a/src/bin/ProjectM36/Client/Json.hs
+++ b/src/bin/ProjectM36/Client/Json.hs
@@ -3,6 +3,7 @@
 import Data.Aeson
 import ProjectM36.Server.RemoteCallTypes.Json ()
 import ProjectM36.Client
+import ProjectM36.TransactionGraph
 import Control.Exception (IOException)
 
 instance ToJSON EvaluatedNotification
@@ -12,3 +13,5 @@
 
 instance ToJSON IOException where
   toJSON err = object ["IOException" .= show err]
+
+instance ToJSON MerkleValidationError  
diff --git a/src/bin/ProjectM36/Server/RemoteCallTypes/Json.hs b/src/bin/ProjectM36/Server/RemoteCallTypes/Json.hs
--- a/src/bin/ProjectM36/Server/RemoteCallTypes/Json.hs
+++ b/src/bin/ProjectM36/Server/RemoteCallTypes/Json.hs
@@ -10,6 +10,7 @@
 import ProjectM36.Error
 import ProjectM36.IsomorphicSchema
 import ProjectM36.Server.RemoteCallTypes
+import ProjectM36.MerkleHash
 
 import Data.Aeson
 import Data.ByteString.Base64 as B64
@@ -131,6 +132,15 @@
 
 instance ToJSON ScriptCompilationError
 instance FromJSON ScriptCompilationError
+
+instance ToJSON MerkleHash where
+  toJSON h = object [ "merklehash" .= decodeUtf8 (B64.encode (_unMerkleHash h))]
+instance FromJSON MerkleHash where
+  parseJSON = withObject "merklehash" $ \o -> do
+    b64bs <- encodeUtf8 <$> o .: "merklehash"
+    case B64.decode b64bs of
+      Left err -> fail ("Failed to parse merkle hash: " ++ err)
+      Right bs -> pure (MerkleHash bs)
 
 instance ToJSON RelationalError
 instance FromJSON RelationalError
diff --git a/src/bin/TutorialD/Interpreter/TransactionGraphOperator.hs b/src/bin/TutorialD/Interpreter/TransactionGraphOperator.hs
--- a/src/bin/TutorialD/Interpreter/TransactionGraphOperator.hs
+++ b/src/bin/TutorialD/Interpreter/TransactionGraphOperator.hs
@@ -2,8 +2,9 @@
 module TutorialD.Interpreter.TransactionGraphOperator where
 import TutorialD.Interpreter.Base
 import ProjectM36.TransactionGraph hiding (autoMergeToHead)
-import ProjectM36.Client
+import ProjectM36.Client as C
 import ProjectM36.Base
+import ProjectM36.Relation (relationTrue)
 import Data.Functor
 
 data ConvenienceTransactionGraphOperator = AutoMergeToHead MergeStrategy HeadName
@@ -71,6 +72,9 @@
   reservedOp ":mergetrans"
   MergeTransactions <$> mergeTransactionStrategyP <*> identifier <*> identifier
 
+validateMerkleHashesP :: Parser ROTransactionGraphOperator
+validateMerkleHashesP = reservedOp ":validatemerklehashes" $> ValidateMerkleHashes
+
 transactionGraphOpP :: Parser TransactionGraphOperator
 transactionGraphOpP = 
   jumpToHeadP
@@ -83,7 +87,7 @@
   <|> mergeTransactionsP
 
 roTransactionGraphOpP :: Parser ROTransactionGraphOperator
-roTransactionGraphOpP = showGraphP
+roTransactionGraphOpP = showGraphP <|> validateMerkleHashesP
 
 
 
@@ -101,6 +105,11 @@
 
 evalROGraphOp :: SessionId -> Connection -> ROTransactionGraphOperator -> IO (Either RelationalError Relation)
 evalROGraphOp sessionId conn ShowGraph = transactionGraphAsRelation sessionId conn
+evalROGraphOp sessionId conn ValidateMerkleHashes = do
+  eVal <- C.validateMerkleHashes sessionId conn
+  case eVal of
+    Left err -> pure (Left err)
+    Right _ -> pure (Right relationTrue)
 
 evalConvenienceGraphOp :: SessionId -> Connection -> ConvenienceTransactionGraphOperator -> IO (Either RelationalError ())
 evalConvenienceGraphOp sessionId conn (AutoMergeToHead strat head') = autoMergeToHead sessionId conn strat head'
diff --git a/src/bin/TutorialD/tutd.hs b/src/bin/TutorialD/tutd.hs
--- a/src/bin/TutorialD/tutd.hs
+++ b/src/bin/TutorialD/tutd.hs
@@ -5,8 +5,10 @@
 import ProjectM36.Server.ParseArgs
 import ProjectM36.Server
 import System.IO
+import GHC.IO.Encoding
 import Options.Applicative
 import System.Exit
+import Control.Monad
 #if __GLASGOW_HASKELL__ < 804
 import Data.Monoid
 #endif
@@ -86,8 +88,15 @@
   putStrLn "A full tutorial is available at:"
   putStrLn "https://github.com/agentm/project-m36/blob/master/docs/tutd_tutorial.markdown"
 
+-- | If the locale is set to ASCII, upgrade it to UTF-8 because tutd outputs UTF-8-encoded attributes. This is especially important in light docker images where the locale data may be missing.
+setLocaleIfNecessary :: IO ()
+setLocaleIfNecessary = do
+  l <- getLocaleEncoding
+  when (textEncodingName l == "ASCII") (setLocaleEncoding utf8)
+    
 main :: IO ()
 main = do
+  setLocaleIfNecessary
   interpreterConfig <- execParser opts
   let connInfo = connectionInfoForConfig interpreterConfig
   fscheck <- checkFSType (checkFSForConfig interpreterConfig) (fromMaybe NoPersistence (persistenceStrategyForConfig interpreterConfig))
diff --git a/src/lib/ProjectM36/AtomFunction.hs b/src/lib/ProjectM36/AtomFunction.hs
--- a/src/lib/ProjectM36/AtomFunction.hs
+++ b/src/lib/ProjectM36/AtomFunction.hs
@@ -9,7 +9,8 @@
 import qualified ProjectM36.Attribute as A
 import qualified Data.HashSet as HS
 import qualified Data.Text as T
-
+import qualified Data.ByteString.Lazy as BL
+import Data.Binary as B
 
 foldAtomFuncType :: AtomType -> AtomType -> [AtomType]
 --the underscore in the attribute name means that any attributes are acceptable
@@ -87,3 +88,13 @@
         atomFuncToTuple aFunc = [TextAtom (atomFuncName aFunc),
                                  TextAtom (atomFuncTypeToText aFunc)]
         atomFuncTypeToText aFunc = T.intercalate " -> " (map prettyAtomType (atomFuncType aFunc))
+
+--for calculating the merkle hash
+hashBytes :: AtomFunction -> BL.ByteString
+hashBytes func = mconcat [B.encode (atomFuncName func),
+                          B.encode (atomFuncType func),
+                          bodyBin
+                         ]
+  where
+    bodyBin = case atomFuncBody func of
+                AtomFunctionBody mScript _ -> B.encode mScript
diff --git a/src/lib/ProjectM36/Base.hs b/src/lib/ProjectM36/Base.hs
--- a/src/lib/ProjectM36/Base.hs
+++ b/src/lib/ProjectM36/Base.hs
@@ -4,6 +4,7 @@
 module ProjectM36.Base where
 import ProjectM36.DatabaseContextFunctionError
 import ProjectM36.AtomFunctionError
+import ProjectM36.MerkleHash
 
 import qualified Data.Map as M
 import qualified Data.HashSet as HS
@@ -413,7 +414,8 @@
 -- The `TransactionDiff`s represent child/edge relationships to previous transactions (branches or continuations of the same branch).
 data TransactionInfo = TransactionInfo {
   parents :: TransactionParents,
-  stamp :: UTCTime
+  stamp :: UTCTime,
+  merkleHash :: MerkleHash
   } deriving (Show, Generic)
 
 type TransactionParents = NE.NonEmpty TransactionId
diff --git a/src/lib/ProjectM36/Client.hs b/src/lib/ProjectM36/Client.hs
--- a/src/lib/ProjectM36/Client.hs
+++ b/src/lib/ProjectM36/Client.hs
@@ -80,6 +80,7 @@
        databaseContextExprForUniqueKey,
        databaseContextExprForForeignKey,
        createScriptedAtomFunction,
+       ProjectM36.Client.validateMerkleHashes,
        AttributeExprBase(..),
        TypeConstructorBase(..),
        TypeConstructorDef(..),
@@ -118,7 +119,7 @@
 import qualified ProjectM36.RelationalExpression as RE
 import ProjectM36.DatabaseContext (basicDatabaseContext)
 import qualified ProjectM36.TransactionGraph as Graph
-import ProjectM36.TransactionGraph
+import ProjectM36.TransactionGraph as TG
 import qualified ProjectM36.Transaction as Trans
 import ProjectM36.TransactionGraph.Persist
 import ProjectM36.Attribute
@@ -265,6 +266,7 @@
 data ConnectionError = SetupDatabaseDirectoryError PersistenceError |
                        IOExceptionError IOException |
                        NoSuchDatabaseByNameError DatabaseName |
+                       DatabaseValidationError [MerkleValidationError] |
                        LoginError 
                        deriving (Show, Eq, Generic)
                   
@@ -379,12 +381,15 @@
       case graph of
         Left err' -> return $ Left (SetupDatabaseDirectoryError err')
         Right graph' -> do
-          tvarGraph <- newTVarIO graph'
-          sessions <- StmMap.newIO
-          clientNodes <- StmSet.newIO
-          (localNode, transport) <- createLocalNode
-          lockMVar <- newMVar digest
-          let conn = InProcessConnection InProcessConnectionConf {
+          case TG.validateMerkleHashes graph' of
+            Left merkleErrs -> pure (Left (DatabaseValidationError merkleErrs))
+            Right _ -> do
+              tvarGraph <- newTVarIO graph'
+              sessions <- StmMap.newIO
+              clientNodes <- StmSet.newIO
+              (localNode, transport) <- createLocalNode
+              lockMVar <- newMVar digest
+              let conn = InProcessConnection InProcessConnectionConf {
                                              ipPersistenceStrategy = strat,
                                              ipClientNodes = clientNodes,
                                              ipSessions = sessions,
@@ -395,9 +400,9 @@
                                              ipLocks = Just (lockFileH, lockMVar)
                                              }
 
-          notificationPid <- startNotificationListener localNode notificationCallback 
-          addClientNode conn notificationPid
-          pure (Right conn)
+              notificationPid <- startNotificationListener localNode notificationCallback
+              addClientNode conn notificationPid
+              pure (Right conn)
           
 -- | Create a new session at the transaction id and return the session's Id.
 createSessionAtCommit :: Connection -> TransactionId -> IO (Either RelationalError SessionId)
@@ -1205,3 +1210,16 @@
               pure (Right dFrame'')
 executeDataFrameExpr sessionId conn@(RemoteProcessConnection _) dfExpr = remoteCall conn (ExecuteDataFrameExpr sessionId dfExpr)
         
+validateMerkleHashes :: SessionId -> Connection -> IO (Either RelationalError ())
+validateMerkleHashes sessionId (InProcessConnection conf) = do
+  let sessions = ipSessions conf
+  atomically $ do
+    eSession <- sessionForSessionId sessionId sessions
+    case eSession of
+      Left err -> pure (Left err)
+      Right _ -> do
+        graph <- readTVar (ipTransactionGraph conf)
+        case Graph.validateMerkleHashes graph of
+          Left merkleErrs -> pure $ Left $ someErrors (map (\(MerkleValidationError tid expected actual) -> MerkleHashValidationError tid expected actual) merkleErrs)
+          Right () -> pure (Right ())
+validateMerkleHashes sessionId conn@RemoteProcessConnection{} = remoteCall conn (ExecuteValidateMerkleHashes sessionId)
diff --git a/src/lib/ProjectM36/DatabaseContext.hs b/src/lib/ProjectM36/DatabaseContext.hs
--- a/src/lib/ProjectM36/DatabaseContext.hs
+++ b/src/lib/ProjectM36/DatabaseContext.hs
@@ -5,8 +5,11 @@
 import qualified Data.HashSet as HS
 import ProjectM36.DataTypes.Basic
 import ProjectM36.AtomFunctions.Basic
-import ProjectM36.DatabaseContextFunction
 import ProjectM36.Relation
+import qualified Data.ByteString.Lazy as BL
+import Data.Binary as B
+import ProjectM36.AtomFunction as AF
+import ProjectM36.DatabaseContextFunction as DBCF
 
 empty :: DatabaseContext
 empty = DatabaseContext { inclusionDependencies = M.empty, 
@@ -38,3 +41,14 @@
                                          notifications = M.empty,
                                          typeConstructorMapping = basicTypeConstructorMapping
                                          }
+
+--for building the Merkle hash
+hashBytes :: DatabaseContext -> BL.ByteString
+hashBytes ctx = mconcat [incDeps, rvs, atomFs, dbcFs, nots, tConsMap]
+  where
+    incDeps = B.encode (inclusionDependencies ctx)
+    rvs = B.encode (relationVariables ctx)
+    atomFs = HS.foldr (mappend . AF.hashBytes) mempty (atomFunctions ctx)
+    dbcFs = HS.foldr (mappend . DBCF.hashBytes) mempty (dbcFunctions ctx)
+    nots = B.encode (notifications ctx)
+    tConsMap = B.encode (typeConstructorMapping ctx)
diff --git a/src/lib/ProjectM36/DatabaseContextFunction.hs b/src/lib/ProjectM36/DatabaseContextFunction.hs
--- a/src/lib/ProjectM36/DatabaseContextFunction.hs
+++ b/src/lib/ProjectM36/DatabaseContextFunction.hs
@@ -10,6 +10,8 @@
 import qualified Data.Map as M
 import ProjectM36.ScriptSession
 import qualified Data.Text as T
+import qualified Data.ByteString.Lazy as BL
+import Data.Binary as B
 
 emptyDatabaseContextFunction :: DatabaseContextFunctionName -> DatabaseContextFunction
 emptyDatabaseContextFunction name = DatabaseContextFunction { 
@@ -77,4 +79,12 @@
     dbcFuncToTuple func = [TextAtom (dbcFuncName func),
                            TextAtom (dbcTextType (dbcFuncType func))]
     dbcTextType typ = T.intercalate " -> " (map prettyAtomType typ ++ ["DatabaseContext", "DatabaseContext"])
-                                                
+
+-- for merkle hash                       
+hashBytes :: DatabaseContextFunction -> BL.ByteString
+hashBytes func = mconcat [fname, ftype, fbody]
+  where
+    fname = B.encode (dbcFuncName func)
+    ftype = B.encode (dbcFuncType func)
+    fbody = case dbcFuncBody func of
+      DatabaseContextFunctionBody mBody _ -> B.encode mBody
diff --git a/src/lib/ProjectM36/Error.hs b/src/lib/ProjectM36/Error.hs
--- a/src/lib/ProjectM36/Error.hs
+++ b/src/lib/ProjectM36/Error.hs
@@ -1,6 +1,7 @@
 {-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
 module ProjectM36.Error where
 import ProjectM36.Base
+import ProjectM36.MerkleHash
 import ProjectM36.DatabaseContextFunctionError
 import ProjectM36.AtomFunctionError
 import qualified Data.Set as S
@@ -101,6 +102,8 @@
 
                      | NoUncommittedContextInEvalError
                      | TupleExprsReferenceMultipleMarkersError
+
+                     | MerkleHashValidationError TransactionId MerkleHash MerkleHash
                        
                      | MultipleErrors [RelationalError]
                        deriving (Show,Eq,Generic,Binary,Typeable, NFData) 
diff --git a/src/lib/ProjectM36/MerkleHash.hs b/src/lib/ProjectM36/MerkleHash.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/ProjectM36/MerkleHash.hs
@@ -0,0 +1,10 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveGeneric #-}
+module ProjectM36.MerkleHash where
+import Data.ByteString (ByteString)
+import GHC.Generics
+import Data.Binary (Binary)
+import Control.DeepSeq (NFData)
+
+newtype MerkleHash = MerkleHash { _unMerkleHash :: ByteString }
+  deriving (Show, Eq, Binary, Generic, Monoid, Semigroup, NFData)
+                                
diff --git a/src/lib/ProjectM36/Relation/Show/Term.hs b/src/lib/ProjectM36/Relation/Show/Term.hs
--- a/src/lib/ProjectM36/Relation/Show/Term.hs
+++ b/src/lib/ProjectM36/Relation/Show/Term.hs
@@ -9,6 +9,8 @@
 import qualified Data.List as L
 import qualified Data.Text as T
 import Control.Arrow hiding (left)
+import Data.ByteString.Base64 as B64
+import qualified Data.Text.Encoding as TE
 #if __GLASGOW_HASKELL__ < 804
 import Data.Monoid
 #endif
@@ -97,6 +99,7 @@
 showAtom _ (RelationAtom rel) = renderTable $ relationAsTable rel
 showAtom level (ConstructedAtom dConsName _ atoms) = showParens (level >= 1 && not (null atoms)) $ T.concat (L.intersperse " " (dConsName : map (showAtom 1) atoms))
 showAtom _ (TextAtom t) = "\"" <> t <> "\""
+showAtom _ (ByteStringAtom bs) = TE.decodeUtf8 (B64.encode bs)
 showAtom _ atom = atomToText atom
 
 renderTable :: Table -> StringType
diff --git a/src/lib/ProjectM36/RelationalExpression.hs b/src/lib/ProjectM36/RelationalExpression.hs
--- a/src/lib/ProjectM36/RelationalExpression.hs
+++ b/src/lib/ProjectM36/RelationalExpression.hs
@@ -741,6 +741,7 @@
     Right dbcListFunc -> let newContext = currentContext { dbcFunctions = mergedFuncs }
                              mergedFuncs = HS.union (dbcFunctions currentContext) (HS.fromList dbcListFunc)
                                   in putDBCIOContext newContext
+#endif
 evalGraphRefDatabaseContextIOExpr (CreateArbitraryRelation relVarName attrExprs range) = do
   --Define
   currentContext <- getDBCIOContext
@@ -771,7 +772,7 @@
                           case runDatabaseContextEvalMonad currentContext evalEnv (setRelVar relVarName (ExistingRelation rel)) of
                             Left err -> pure (Left err)
                             Right dbstate' -> putDBCIOContext (dbc_context dbstate')
-#endif
+
 {-
 updateTupleWithAtomExprs :: M.Map AttributeName AtomExpr -> DatabaseContext -> TransactionGraph -> RelationTuple -> Either RelationalError RelationTuple
 updateTupleWithAtomExprs exprMap context graph tupIn = do
@@ -793,7 +794,11 @@
     tempStamp = UTCTime { utctDay = fromGregorian 2000 1 1,
                           utctDayTime = secondsToDiffTime 0 }
     tempSchemas = Schemas context M.empty
-    tempTrans = Transaction U.nil (TransactionInfo (transId NE.:| []) tempStamp) tempSchemas
+    tempTrans = Transaction U.nil  tempTransInfo tempSchemas
+    tempTransInfo = TransactionInfo { parents = transId NE.:| [],
+                                      stamp = tempStamp,
+                                      merkleHash = mempty
+                                      }
     
     deps = inclusionDependencies context
       -- no optimization available here, really? perhaps the optimizer should be passed down to here or the eval function should be passed through the environment
@@ -1150,15 +1155,27 @@
         [] -> pure emptyAttributes
         (headTuple:tailTuples) -> do
       --gather up resolved atom types or throw an error if an attribute cannot be made concrete from the inferred types- this could still fail if the type cannot be inferred (e.g. from [Nothing, Nothing])
-          let mostResolvedTypes =
-                foldr (\tup acc ->
-                         fmap (\(tupAttr,accAttr) -> --if the attribute is a constructedatomtype, we can recurse into it to potentially resolve type variables
-                                 if isResolvedAttribute accAttr then
-                                   accAttr
-                                 else
-                                   case resolveAttributes accAttr tupAttr of
-                                     Left _ -> accAttr
-                                     Right val -> val) (zip (V.toList $ tupleAttributes tup) acc)) (V.toList $ tupleAttributes headTuple) tailTuples
+          let 
+              processTupleAttrs (tupAttr, accAttr) =
+                --if the attribute is a constructedatomtype, we can recurse into it to potentially resolve type variables                
+                if isResolvedAttribute accAttr && tupAttr == accAttr then
+                  pure accAttr
+                else
+                  lift $ except $ resolveAttributes accAttr tupAttr 
+          mostResolvedTypes <-
+                foldM (\acc tup -> do
+                         let zipped = zip (V.toList $ tupleAttributes tup) acc
+                             accNames = S.fromList $ map A.attributeName acc
+                             tupNames = A.attributeNameSet (tupleAttributes tup)
+                             attrNamesDiff = S.union (S.difference accNames tupNames) (S.difference tupNames accNames)
+                         unless (null attrNamesDiff) (throwError (AttributeNamesMismatchError attrNamesDiff))
+                         nextTupleAttrs <- mapM processTupleAttrs zipped
+                         let diff = A.attributesDifference (A.attributesFromList nextTupleAttrs) (A.attributesFromList acc)
+                         if diff == A.emptyAttributes then
+                           pure nextTupleAttrs
+                           else
+                           throwError (TupleAttributeTypeMismatchError diff)
+                      ) (V.toList $ tupleAttributes headTuple) tailTuples
           pure (A.attributesFromList mostResolvedTypes)
   --strategy: if all the tuple expr transaction markers refer to one location, then we can pass the type constructor mapping from that location, otherwise, we cannot assume that the types are the same
   tConsMap <- case singularTransactions tupleExprL of
diff --git a/src/lib/ProjectM36/ScriptSession.hs b/src/lib/ProjectM36/ScriptSession.hs
--- a/src/lib/ProjectM36/ScriptSession.hs
+++ b/src/lib/ProjectM36/ScriptSession.hs
@@ -18,6 +18,7 @@
 import Data.Text (Text, unpack)
 import Data.Maybe
 import GHC.Paths (libdir)
+import System.Environment
 #if __GLASGOW_HASKELL__ >= 800
 import GHC.LanguageExtensions
 import GHCi.ObjLink
@@ -78,6 +79,8 @@
   handleGhcException excHandler $ runGhc (Just libdir) $ do
     dflags <- getSessionDynFlags
     let ghcVersion = projectVersion dflags
+    -- get nix packages dir, if available
+    mNixLibDir <- liftIO $ lookupEnv "NIX_GHC_LIBDIR"
 
     sandboxPkgPaths <- liftIO $ concat <$> mapM glob [
       --"./dist-newstyle/packagedb/ghc-" ++ ghcVersion, --rely on cabal 3's ghc.environment install: "cabal install --lib project-m36"
@@ -89,7 +92,7 @@
       --homeDir </> ".cabal/store/ghc-" ++ ghcVersion ++ "/package.db"
       ]
 
-    let localPkgPaths = map PkgConfFile (ghcPkgPaths ++ sandboxPkgPaths)
+    let localPkgPaths = map PkgConfFile (ghcPkgPaths ++ sandboxPkgPaths ++ maybeToList mNixLibDir)
 
     let dflags' = applyGopts . applyXopts $ dflags { hscTarget = HscInterpreted ,
                            ghcLink = LinkInMemory,
@@ -144,7 +147,9 @@
           ideclSourceSrc = Nothing,
 #endif
 
-#if __GLASGOW_HASKELL__ >= 806
+#if __GLASGOW_HASKELL__ >= 810
+          ideclExt = noExtField,
+#elif __GLASGOW_HASKELL__ >= 806
           ideclExt = NoExt,
 #endif
           ideclName      = noLoc mn,
@@ -152,7 +157,11 @@
           ideclSource    = False,
           ideclSafe      = True,
           ideclImplicit  = False,
+#if __GLASGOW_HASKELL__ >= 810
+          ideclQualified = if isJust mQual then QualifiedPre else NotQualified,
+#else
           ideclQualified = isJust mQual,
+#endif
           ideclAs        = mQual,
           ideclHiding    = Nothing
           }
diff --git a/src/lib/ProjectM36/Server.hs b/src/lib/ProjectM36/Server.hs
--- a/src/lib/ProjectM36/Server.hs
+++ b/src/lib/ProjectM36/Server.hs
@@ -53,6 +53,7 @@
      handleCall (\conn (RetrieveSessionIsDirty sessionId) -> handleRetrieveSessionIsDirty ti sessionId conn),
      handleCall (\conn (ExecuteAutoMergeToHead sessionId strat headName') -> handleExecuteAutoMergeToHead ti sessionId conn strat headName'),
      handleCall (\conn (RetrieveTypeConstructorMapping sessionId) -> handleRetrieveTypeConstructorMapping ti sessionId conn),
+     handleCall (\conn (ExecuteValidateMerkleHashes sessionId) -> handleValidateMerkleHashes ti sessionId conn),
      handleCall (\conn Logout -> handleLogout ti conn)
      ] ++ testModeHandlers,
   unhandledMessagePolicy = Terminate
diff --git a/src/lib/ProjectM36/Server/EntryPoints.hs b/src/lib/ProjectM36/Server/EntryPoints.hs
--- a/src/lib/ProjectM36/Server/EntryPoints.hs
+++ b/src/lib/ProjectM36/Server/EntryPoints.hs
@@ -172,3 +172,7 @@
   ret <- timeoutOrDie ti (C.typeConstructorMapping sessionId conn)
   reply ret conn
  
+handleValidateMerkleHashes :: Timeout -> SessionId -> Connection -> Reply (Either RelationalError ())
+handleValidateMerkleHashes ti sessionId conn = do
+  ret <- timeoutOrDie ti (C.validateMerkleHashes sessionId conn)
+  reply ret conn
diff --git a/src/lib/ProjectM36/Server/ParseArgs.hs b/src/lib/ProjectM36/Server/ParseArgs.hs
--- a/src/lib/ProjectM36/Server/ParseArgs.hs
+++ b/src/lib/ProjectM36/Server/ParseArgs.hs
@@ -73,7 +73,13 @@
 
 
 helpOption :: Parser (a -> a)
-helpOption = abortOption ShowHelpText $ mconcat
+helpOption = abortOption helpText $ mconcat
   [ long "help"
   , help "Show this help text"
   , hidden ]
+  where
+#if MIN_VERSION_optparse_applicative(0,16,0)
+    helpText = ShowHelpText Nothing
+#else               
+    helpText = ShowHelpText
+#endif
diff --git a/src/lib/ProjectM36/Server/RemoteCallTypes.hs b/src/lib/ProjectM36/Server/RemoteCallTypes.hs
--- a/src/lib/ProjectM36/Server/RemoteCallTypes.hs
+++ b/src/lib/ProjectM36/Server/RemoteCallTypes.hs
@@ -69,3 +69,6 @@
                               deriving (Binary, Generic)
 data RetrieveTypeConstructorMapping = RetrieveTypeConstructorMapping SessionId 
                                       deriving (Binary, Generic)
+
+data ExecuteValidateMerkleHashes = ExecuteValidateMerkleHashes SessionId
+                          deriving (Binary, Generic)
diff --git a/src/lib/ProjectM36/Transaction.hs b/src/lib/ProjectM36/Transaction.hs
--- a/src/lib/ProjectM36/Transaction.hs
+++ b/src/lib/ProjectM36/Transaction.hs
@@ -17,11 +17,10 @@
 -- | Return the same transaction but referencing only the specific child transactions. This is useful when traversing a graph and returning a subgraph. This doesn't filter parent transactions because it assumes a head-to-root traversal.
 filterTransactionInfoTransactions :: S.Set TransactionId -> TransactionInfo -> TransactionInfo
 filterTransactionInfoTransactions filterIds tinfo =
-  TransactionInfo { parents = case
+  tinfo { parents = case
                       NE.filter (`S.member`  filterIds) (parents tinfo) of
                       [] -> rootParent
-                      xs -> NE.fromList xs,
-                    stamp = stamp tinfo }
+                      xs -> NE.fromList xs}
 
 filterParent :: TransactionId -> S.Set TransactionId -> TransactionId
 filterParent parentId validIds = if S.member parentId validIds then parentId else U.nil
@@ -43,7 +42,13 @@
 subschemas (Transaction _ _ (Schemas _ sschemas)) = sschemas
 
 fresh :: TransactionId -> UTCTime -> Schemas -> Transaction
-fresh freshId stamp' = Transaction freshId (TransactionInfo rootParent stamp')
+fresh freshId stamp' = Transaction freshId tinfo
+  where
+    tinfo = TransactionInfo {parents = rootParent,
+                             stamp = stamp',
+                             merkleHash = mempty
+                            }
 
 timestamp :: Transaction -> UTCTime
 timestamp (Transaction _ tinfo _) = stamp tinfo
+
diff --git a/src/lib/ProjectM36/TransactionGraph.hs b/src/lib/ProjectM36/TransactionGraph.hs
--- a/src/lib/ProjectM36/TransactionGraph.hs
+++ b/src/lib/ProjectM36/TransactionGraph.hs
@@ -9,6 +9,7 @@
 import ProjectM36.Tuple
 import ProjectM36.RelationalExpression
 import ProjectM36.TransactionGraph.Merge
+import ProjectM36.MerkleHash
 import qualified ProjectM36.DisconnectedTransaction as Discon
 import qualified ProjectM36.Attribute as A
 
@@ -22,7 +23,7 @@
 import Data.Time.Clock
 import qualified Data.Text as T
 import GHC.Generics
-import Data.Binary
+import Data.Binary as B
 import Data.Either (lefts, rights, isRight)
 #if __GLASGOW_HASKELL__ < 804
 import Data.Monoid
@@ -30,6 +31,9 @@
 import Control.Arrow
 import Data.Maybe
 import Data.UUID.V4
+import qualified Data.ByteString.Lazy as BL
+import ProjectM36.DatabaseContext as DBC
+import Crypto.Hash.SHA256
 
 -- | Record a lookup for a specific transaction in the graph.
 data TransactionIdLookup = TransactionIdLookup TransactionId |
@@ -58,7 +62,7 @@
 isCommit Commit = True
 isCommit _ = False
                                        
-data ROTransactionGraphOperator = ShowGraph
+data ROTransactionGraphOperator = ShowGraph | ValidateMerkleHashes
                                   deriving Show
 
 bootstrapTransactionGraph :: UTCTime -> TransactionId -> DatabaseContext -> TransactionGraph
@@ -67,7 +71,8 @@
     bootstrapHeads = M.singleton "master" freshTransaction
     newSchemas = Schemas context M.empty
     freshTransaction = fresh freshId stamp' newSchemas
-    bootstrapTransactions = S.singleton freshTransaction
+    hashedTransaction = Transaction freshId ((transactionInfo freshTransaction) { merkleHash = calculateMerkleHash freshTransaction emptyTransactionGraph }) newSchemas
+    bootstrapTransactions = S.singleton hashedTransaction
 
 -- | Create a transaction graph from a context.
 freshTransactionGraph :: DatabaseContext -> IO (TransactionGraph, TransactionId)
@@ -125,10 +130,13 @@
 
 --adds a disconnected transaction to a transaction graph at some head
 addDisconnectedTransaction :: UTCTime -> TransactionId -> HeadName -> DisconnectedTransaction -> TransactionGraph -> Either RelationalError (Transaction, TransactionGraph)
-addDisconnectedTransaction stamp' newId headName (DisconnectedTransaction parentId schemas' _) = addTransactionToGraph headName newTrans
+addDisconnectedTransaction stamp' newId headName (DisconnectedTransaction parentId schemas' _) graph = addTransactionToGraph headName newTrans' graph
   where
-    newTrans = Transaction newId (TI.singleParent parentId stamp') schemas'
-
+    newTrans = Transaction newId newTInfo schemas'
+    newTInfo = TI.singleParent parentId stamp'
+    newTrans' = Transaction newId (newTInfo { merkleHash = newHash }) schemas'
+    newHash = calculateMerkleHash newTrans graph--lookup parents in graph to calculate
+--LOOP
 addTransactionToGraph :: HeadName -> Transaction -> TransactionGraph -> Either RelationalError (Transaction, TransactionGraph)
 addTransactionToGraph headName newTrans graph = do
   let parentIds' = parentIds newTrans
@@ -295,6 +303,7 @@
   mkRelationFromList attrs tupleMatrix
   where
     attrs = A.attributesFromList [Attribute "id" TextAtomType,
+                                  Attribute "hash" ByteStringAtomType,
                                   Attribute "stamp" DateTimeAtomType,
                                   Attribute "parents" (RelationAtomType parentAttributes),
                                   Attribute "current" BoolAtomType,
@@ -304,6 +313,7 @@
     tupleGenerator transaction = case transactionParentsRelation transaction graph of
       Left err -> Left err
       Right parentTransRel -> Right [TextAtom $ T.pack $ show (transactionId transaction),
+                                     ByteStringAtom $ _unMerkleHash (merkleHash (transactionInfo transaction)),
                                      DateTimeAtom (timestamp transaction),
                                      RelationAtom parentTransRel,
                                      BoolAtom $ parentId == transactionId transaction,
@@ -335,10 +345,12 @@
 createMergeTransaction stamp' newId (SelectedBranchMergeStrategy selectedBranch) t2@(trans1, trans2) = do
   graph <- gfGraph
   selectedTrans <- validateHeadName selectedBranch graph t2
-  pure $ Transaction newId (TransactionInfo {
-                        parents = NE.fromList [transactionId trans1,
-                                               transactionId trans2],
-                        stamp = stamp' }) (schemas selectedTrans)
+  pure $ addMerkleHash graph $
+    Transaction newId (TransactionInfo {
+                          parents = NE.fromList [transactionId trans1,
+                                                 transactionId trans2],
+                          stamp = stamp',
+                          merkleHash = mempty }) (schemas selectedTrans)
                        
 -- merge functions, relvars, individually
 createMergeTransaction stamp' newId strat@UnionMergeStrategy t2 =
@@ -493,10 +505,12 @@
         typeConstructorMapping = types
         }
       newSchemas = Schemas newContext (subschemas t1)
-  pure (Transaction newId (TransactionInfo {
-                              parents = NE.fromList [transactionId t1,
-                                                     transactionId t2],
-                              stamp = stamp'}) newSchemas)
+  pure $ addMerkleHash graph $
+    Transaction newId (TransactionInfo {
+                          parents = NE.fromList [transactionId t1,
+                                                  transactionId t2],
+                            stamp = stamp',
+                            merkleHash = mempty }) newSchemas
 
 lookupTransaction :: TransactionGraph -> TransactionIdLookup -> Either RelationalError Transaction
 lookupTransaction graph (TransactionIdLookup tid) = transactionForId tid graph
@@ -572,3 +586,48 @@
   pure (discon''''', graph''''')
 
 
+addMerkleHash :: TransactionGraph -> Transaction -> Transaction
+addMerkleHash graph trans = Transaction (transactionId trans) newInfo (schemas trans)
+  where
+    newInfo = (transactionInfo trans) { merkleHash = calculateMerkleHash trans graph }
+  
+-- the new hash includes the parents' ids, the current id, and the hash of the context, and the merkle hashes of the parent transactions
+calculateMerkleHash :: Transaction -> TransactionGraph -> MerkleHash
+calculateMerkleHash trans graph = MerkleHash $ hashlazy (mconcat [transIds,
+                                                     dbcBytes,
+                                                     schemasBytes,
+                                                     parentMerkleHashes])
+  where
+    parentMerkleHashes = BL.fromChunks $ map (_unMerkleHash . getMerkleHash) parentTranses
+    parentTranses =
+      case transactionsForIds (parentIds trans) graph of
+        Left RootTransactionTraversalError -> []
+        Left e -> error ("failed to find transaction in Merkle hash construction: " ++ show e)
+        Right t -> S.toList t
+    getMerkleHash t = merkleHash (transactionInfo t)
+    transIds = B.encode (transactionId trans : S.toList (parentIds trans))
+    dbcBytes = DBC.hashBytes (concreteDatabaseContext trans)
+    schemasBytes = B.encode (subschemas trans)
+
+validateMerkleHash :: Transaction -> TransactionGraph -> Either MerkleValidationError ()
+validateMerkleHash trans graph = 
+  if expectedHash /= actualHash  then
+    Left (MerkleValidationError (transactionId trans) expectedHash actualHash)
+  else
+    pure ()
+  where
+    expectedHash = merkleHash (transactionInfo trans)
+    actualHash = calculateMerkleHash trans graph
+
+data MerkleValidationError = MerkleValidationError TransactionId MerkleHash MerkleHash
+  deriving (Show, Eq, Generic)
+
+validateMerkleHashes :: TransactionGraph -> Either [MerkleValidationError] ()
+validateMerkleHashes graph =
+  if null errs then pure () else Left errs
+  where
+    errs = S.foldr validateTrans [] (transactionsForGraph graph)    
+    validateTrans trans acc =
+      case validateMerkleHash trans graph of
+        Left err -> err : acc
+        _ -> acc
diff --git a/src/lib/ProjectM36/TransactionInfo.hs b/src/lib/ProjectM36/TransactionInfo.hs
--- a/src/lib/ProjectM36/TransactionInfo.hs
+++ b/src/lib/ProjectM36/TransactionInfo.hs
@@ -1,10 +1,12 @@
 module ProjectM36.TransactionInfo where
 import ProjectM36.Base
 import Data.Time.Clock
-import Data.List.NonEmpty
+import qualified Data.List.NonEmpty as NE
 
 -- | Create a TransactionInfo with just one parent transaction ID.
 singleParent :: TransactionId -> UTCTime -> TransactionInfo
 singleParent tid stamp' = TransactionInfo {
-  parents = tid :| [],
-  stamp = stamp' }
+  parents = tid NE.:| [],
+  stamp = stamp',
+  merkleHash = mempty }
+
diff --git a/src/lib/ProjectM36/Tupleable.hs b/src/lib/ProjectM36/Tupleable.hs
--- a/src/lib/ProjectM36/Tupleable.hs
+++ b/src/lib/ProjectM36/Tupleable.hs
@@ -6,8 +6,27 @@
 {-# LANGUAGE UndecidableInstances #-}
 {-# LANGUAGE CPP #-}
 
-module ProjectM36.Tupleable where
+module ProjectM36.Tupleable
+  ( toInsertExpr
+  , toDefineExpr
+  , tupleAssocsEqualityPredicate
+  , partitionByAttributes
+  , toUpdateExpr
+  , toDeleteExpr
+  , validateAttributes
+  , Tupleable(..)
 
+    -- * Generics
+  , genericToTuple
+  , genericFromTuple
+  , genericToAttributes
+  , TupleableG(..)
+    -- ** Options
+  , defaultTupleableOptions
+  , TupleableOptions()
+  , fieldModifier
+  ) where
+
 import           Data.Foldable
 import           Data.List                      (partition)
 import qualified Data.Map                       as Map
@@ -121,56 +140,109 @@
   where
       nonMatchingAttrs = attributeNamesNotContained actualAttrs expectedAttrs
 
-
+-- | Types that can be converted to and from 'RelationTuple'.
+--
+-- deriving without customization:
+--
+-- > data Example = Example
+-- >     { foo :: Integer
+-- >     , bar :: Text
+-- >     }
+-- >     deriving (Generic)
+-- >
+-- > instance Tupleable Example
+--
+-- deriving with customization using "ProjectM36.Tupleable.Deriving":
+--
+-- > data Example = Example
+-- >     { exampleFoo :: Integer
+-- >     , exampleBar :: Text
+-- >     }
+-- >     deriving stock (Generic)
+-- >     deriving (Tupleable)
+-- >         via Codec (Field (DropPrefix "example" >>> CamelCase)) Example
 class Tupleable a where
   toTuple :: a -> RelationTuple
 
   fromTuple :: RelationTuple -> Either RelationalError a
 
-  toAttributes :: proxy a -> Attributes
+  toAttributes :: Proxy a -> Attributes
 
   default toTuple :: (Generic a, TupleableG (Rep a)) => a -> RelationTuple
-  toTuple v = toTupleG (from v)
+  toTuple = genericToTuple defaultTupleableOptions
 
   default fromTuple :: (Generic a, TupleableG (Rep a)) => RelationTuple -> Either RelationalError a
-  fromTuple tup = to <$> fromTupleG tup
+  fromTuple = genericFromTuple defaultTupleableOptions
 
-  default toAttributes :: (Generic a, TupleableG (Rep a)) => proxy a -> Attributes
-  toAttributes _ = toAttributesG (from (undefined :: a))
+  default toAttributes :: (Generic a, TupleableG (Rep a)) => Proxy a -> Attributes
+  toAttributes = genericToAttributes defaultTupleableOptions
 
+-- | Options that influence deriving behavior.
+newtype TupleableOptions = TupleableOptions {
+  -- | A function that translates record field names into attribute names.
+  fieldModifier :: T.Text -> T.Text
+  }
+
+-- | The default options for deriving Tupleable instances.
+--
+-- These options can be customized by using record update syntax. For example,
+--
+-- > defaultTupleableOptions
+-- >     { fieldModifier = \fieldName ->
+-- >         case Data.Text.stripPrefix "example" fieldName of
+-- >             Nothing -> fieldName
+-- >             Just attributeName -> attributeName
+-- >     }
+--
+-- will result in record field names being translated into attribute names by
+-- removing the prefix "example" from the field names.
+defaultTupleableOptions :: TupleableOptions
+defaultTupleableOptions = TupleableOptions {
+  fieldModifier = id
+  }
+
+genericToTuple :: (Generic a, TupleableG (Rep a)) => TupleableOptions -> a -> RelationTuple
+genericToTuple opts v = toTupleG opts (from v)
+
+genericFromTuple :: (Generic a, TupleableG (Rep a)) => TupleableOptions -> RelationTuple -> Either RelationalError a
+genericFromTuple opts tup = to <$> fromTupleG opts tup
+
+genericToAttributes :: forall a. (Generic a, TupleableG (Rep a)) => TupleableOptions -> Proxy a -> Attributes
+genericToAttributes opts _ = toAttributesG opts (from (undefined :: a))
+
 class TupleableG g where
-  toTupleG :: g a -> RelationTuple
-  toAttributesG :: g a -> Attributes
-  fromTupleG :: RelationTuple -> Either RelationalError (g a)
+  toTupleG :: TupleableOptions -> g a -> RelationTuple
+  toAttributesG :: TupleableOptions -> g a -> Attributes
+  fromTupleG :: TupleableOptions -> RelationTuple -> Either RelationalError (g a)
   isRecordTypeG :: g a -> Bool
 
 --data type metadata
 instance (Datatype c, TupleableG a) => TupleableG (M1 D c a) where
-  toTupleG (M1 v) = toTupleG v
-  toAttributesG (M1 v) = toAttributesG v
-  fromTupleG v = M1 <$> fromTupleG v
+  toTupleG opts (M1 v) = toTupleG opts v
+  toAttributesG opts (M1 v) = toAttributesG opts v
+  fromTupleG opts v = M1 <$> fromTupleG opts v
   isRecordTypeG (M1 v) = isRecordTypeG v
 
 --constructor metadata
 instance (Constructor c, TupleableG a, AtomableG a) => TupleableG (M1 C c a) where
-  toTupleG (M1 v) = RelationTuple attrs atoms
+  toTupleG opts (M1 v) = RelationTuple attrs atoms
     where
-      attrsToCheck = toAttributesG v
+      attrsToCheck = toAttributesG opts v
       counter = V.generate (V.length attrsToCheck) id
       attrs = V.zipWith (\num attr@(Attribute name typ) -> if T.null name then
                                                              Attribute ("attr" <> T.pack (show (num + 1))) typ
                                                            else
                                                              attr) counter attrsToCheck
       atoms = V.fromList (toAtomsG v)
-  toAttributesG (M1 v) = toAttributesG v
-  fromTupleG tup = M1 <$> fromTupleG tup
+  toAttributesG opts (M1 v) = toAttributesG opts v
+  fromTupleG opts tup = M1 <$> fromTupleG opts tup
   isRecordTypeG (M1 v) = isRecordTypeG v
 
 -- product types
 instance (TupleableG a, TupleableG b) => TupleableG (a :*: b) where
   toTupleG = error "toTupleG"
-  toAttributesG ~(x :*: y) = toAttributesG x V.++ toAttributesG y --a bit of extra laziness prevents whnf so that we can use toAttributes (undefined :: Test2T Int Int) without throwing an exception
-  fromTupleG tup = (:*:) <$> fromTupleG tup <*> fromTupleG processedTuple
+  toAttributesG opts ~(x :*: y) = toAttributesG opts x V.++ toAttributesG opts y --a bit of extra laziness prevents whnf so that we can use toAttributes (undefined :: Test2T Int Int) without throwing an exception
+  fromTupleG opts tup = (:*:) <$> fromTupleG opts tup <*> fromTupleG opts processedTuple
     where
       processedTuple = if isRecordTypeG (undefined :: a x) then
                          tup
@@ -181,18 +253,22 @@
 --selector/record
 instance (Selector c, AtomableG a) => TupleableG (M1 S c a) where
   toTupleG = error "toTupleG"
-  toAttributesG m@(M1 v) = V.singleton (Attribute name aType)
+  toAttributesG opts m@(M1 v) = V.singleton (Attribute modifiedName aType)
    where
      name = T.pack (selName m)
+     modifiedName = if T.null name then
+                      name
+                    else
+                      fieldModifier opts name
      aType = toAtomTypeG v
-  fromTupleG tup = if null name then -- non-record type, just pull off the first tuple item
+  fromTupleG opts tup = if null name then -- non-record type, just pull off the first tuple item
                      M1 <$> atomv (V.head (tupleAtoms tup))
                    else do
-                     atom <- atomForAttributeName (T.pack name) tup
+                     atom <- atomForAttributeName (fieldModifier opts (T.pack name)) tup
                      val <- atomv atom
                      pure (M1 val)
    where
-     expectedAtomType = atomType (V.head (toAttributesG (undefined :: M1 S c a x)))
+     expectedAtomType = atomType (V.head (toAttributesG opts (undefined :: M1 S c a x)))
      atomv atom = maybe (Left (AtomTypeMismatchError
                                expectedAtomType
                                (atomTypeForAtom atom)
@@ -203,8 +279,8 @@
 --constructors with no arguments
 --basically useless but orthoganal to relationTrue
 instance TupleableG U1 where
-  toTupleG _= emptyTuple
-  toAttributesG _ = emptyAttributes
-  fromTupleG _ = pure U1
+  toTupleG _ _ = emptyTuple
+  toAttributesG _ _ = emptyAttributes
+  fromTupleG _ _ = pure U1
   isRecordTypeG _ = False
 
diff --git a/src/lib/ProjectM36/Tupleable/Deriving.hs b/src/lib/ProjectM36/Tupleable/Deriving.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/ProjectM36/Tupleable/Deriving.hs
@@ -0,0 +1,229 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+-- | Newtypes for deriving Tupleable instances with customization using
+-- @DerivingVia@.
+--
+-- Inspired by
+-- [Dhall.Deriving](https://hackage.haskell.org/package/dhall-1.33.1/docs/Dhall-Deriving.html)
+-- which in turn was inspired by Matt Parson's blog post
+-- [Mirror Mirror: Reflection and Encoding Via](https://www.parsonsmatt.org/2020/02/04/mirror_mirror.html).
+--
+-- required extensions:
+--
+--   * DerivingVia
+--   * DeriveGenerics
+--   * TypeOperators (for @('<<<')@ and @('>>>')@)
+--   * DataKinds (for types that take a string argument)
+
+module ProjectM36.Tupleable.Deriving
+  ( -- * DerivingVia Newtype
+    Codec(..)
+
+    -- * Type-level Options
+  , ModifyOptions(..)
+  , Field
+
+    -- * Type-level 'T.Text' -> 'T.Text' Functions
+  , ModifyText(..)
+  , AddPrefix
+  , DropPrefix
+  , AddSuffix
+  , DropSuffix
+  , UpperCase
+  , LowerCase
+  , TitleCase
+  , CamelCase
+  , PascalCase
+  , SnakeCase
+  , SpinalCase
+  , TrainCase
+
+    -- * Composition
+  , AsIs
+  , type (<<<)
+  , type (>>>)
+
+    -- * Re-Exports
+  , Generic
+  , module ProjectM36.Tupleable
+  ) where
+import           Data.Maybe           (fromMaybe)
+import           Data.Proxy
+import qualified Data.Text            as T
+import           Data.Text.Manipulate
+import           GHC.TypeLits
+import           GHC.Generics         (Generic, Rep)
+import           ProjectM36.Tupleable
+
+
+-- | A newtype wrapper to allow for easier deriving of 'Tupleable' instances
+-- with customization.
+--
+-- The @tag@ type variable can be used to specify options for converting the
+-- datatype to and from a 'RelationTuple'. For example,
+--
+-- > data Example = Example
+-- >     { exampleFoo :: Int
+-- >     , exampleBar :: Int
+-- >     }
+-- >     deriving stock (Generic)
+-- >     deriving (Tupleable)
+-- >         via Codec (Field (DropPrefix "example" >>> CamelCase)) Example
+--
+-- will derive an instance of 'Tupleable' where field names are translated into
+-- attribute names by dropping the prefix @"example"@ and then converting the
+-- result to camelCase. So @"exampleFoo"@ becomes @"foo"@ and @"exampleBar"@
+-- becomes @"bar"@.
+--
+-- Requires the @DerivingGeneric@ and @DerivingVia@ extensions to be enabled.
+newtype Codec tag a = Codec { unCodec :: a }
+
+instance (ModifyOptions tag, Generic a, TupleableG (Rep a)) => Tupleable (Codec tag a) where
+  toTuple v = genericToTuple opts (unCodec v)
+    where
+      opts = modifyOptions (Proxy :: Proxy tag) defaultTupleableOptions
+
+  fromTuple tup = Codec <$> genericFromTuple opts tup
+    where
+      opts = modifyOptions (Proxy :: Proxy tag) defaultTupleableOptions
+
+  toAttributes _ = genericToAttributes opts (Proxy :: Proxy a)
+    where
+      opts = modifyOptions (Proxy :: Proxy tag) defaultTupleableOptions
+
+-- | Types that can be used as tags for 'Codec'.
+class ModifyOptions a where
+  modifyOptions :: proxy a -> TupleableOptions -> TupleableOptions
+
+-- | Change how record field names are translated into attribute names. For
+-- example,
+--
+-- > Field SnakeCase
+--
+-- will translate the field name @fooBar@ into the attribute name @foo_bar@.
+data Field a
+
+instance ModifyText a => ModifyOptions (Field a) where
+  modifyOptions _ opts = opts { fieldModifier = newFieldModifier }
+    where
+      newFieldModifier = modifyText (Proxy :: Proxy a) . fieldModifier opts
+
+-- | Types that can be used in options that modify 'T.Text' such as in 'Field'.
+class ModifyText a where
+  modifyText :: proxy a -> T.Text -> T.Text
+
+-- | Add a prefix. @AddPrefix "foo"@ will transform @"bar"@ into @"foobar"@.
+data AddPrefix (prefix :: Symbol)
+
+instance KnownSymbol prefix => ModifyText (AddPrefix prefix) where
+  modifyText _ oldText = prefixText <> oldText
+    where
+      prefixText = T.pack (symbolVal (Proxy :: Proxy prefix))
+
+-- | Drop a prefix. @DropPrefix "bar"@ will transform @"foobar"@ into @"foo"@.
+data DropPrefix (prefix :: Symbol)
+
+instance KnownSymbol prefix => ModifyText (DropPrefix prefix) where
+  modifyText _ oldText = fromMaybe oldText (T.stripPrefix prefixText oldText)
+    where
+      prefixText = T.pack (symbolVal (Proxy :: Proxy prefix))
+
+-- | Add a suffix. @AddSuffix "bar"@ will transform @"foo"@ into @"foobar"@.
+data AddSuffix (suffix :: Symbol)
+
+instance KnownSymbol suffix => ModifyText (AddSuffix suffix) where
+  modifyText _ oldText = oldText <> suffixText
+    where
+      suffixText = T.pack (symbolVal (Proxy :: Proxy suffix))
+
+-- | Drop a suffix. @DropSuffix "bar"@ will transform @"foobar"@ into @"foo"@.
+data DropSuffix (suffix :: Symbol)
+
+instance KnownSymbol suffix => ModifyText (DropSuffix suffix) where
+  modifyText _ oldText = fromMaybe oldText (T.stripSuffix suffixText oldText)
+    where
+      suffixText = T.pack (symbolVal (Proxy :: Proxy suffix))
+
+-- | Convert to UPPERCASE. Will transform @"foobar"@ into @\"FOOBAR\"@.
+data UpperCase
+
+instance ModifyText UpperCase where
+  modifyText _ = T.toUpper
+
+-- | Convert to lowercase. Will transform @\"FOOBAR\"@ into @"foobar"@.
+data LowerCase
+
+instance ModifyText LowerCase where
+  modifyText _ = T.toLower
+
+-- | Convert to Title Case. Will transform @"fooBar"@ into @\"Foo Bar\"@.
+data TitleCase
+
+instance ModifyText TitleCase where
+  modifyText _ = toTitle
+
+-- | Convert to camelCase. Will transform @"foo_bar"@ into @"fooBar"@.
+data CamelCase
+
+instance ModifyText CamelCase where
+  modifyText _ = toCamel
+
+-- | Convert to PascalCase. Will transform @"foo_bar"@ into @\"FooBar\"@.
+data PascalCase
+
+instance ModifyText PascalCase where
+  modifyText _ = toPascal
+
+-- | Convert to snake_case. Will transform @"fooBar"@ into @"foo_bar"@.
+data SnakeCase
+
+instance ModifyText SnakeCase where
+  modifyText _ = toSnake
+
+-- | Convert to spinal-case. will transform @"fooBar"@ into @"foo-bar"@.
+data SpinalCase
+
+instance ModifyText SpinalCase where
+  modifyText _ = toSpinal
+
+-- | Convert to Train-Case. Will transform @"fooBar"@ into @\"Foo-Bar\"@.
+data TrainCase
+
+instance ModifyText TrainCase where
+  modifyText _ = toTrain
+
+-- | Identity option.
+type AsIs = ()
+
+instance ModifyOptions () where
+  modifyOptions _ = id
+
+instance ModifyText () where
+  modifyText _ = id
+
+-- | Right to left composition.
+--
+-- Requires the @TypeOperators@ extension to be enabled.
+data a <<< b
+
+instance (ModifyOptions a, ModifyOptions b) => ModifyOptions (a <<< b) where
+  modifyOptions _ = modifyOptions (Proxy :: Proxy a) . modifyOptions (Proxy :: Proxy b)
+
+instance (ModifyText a, ModifyText b) => ModifyText (a <<< b) where
+  modifyText _ = modifyText (Proxy :: Proxy a) . modifyText (Proxy :: Proxy b)
+
+-- | Left to right composition.
+--
+-- Requires the @TypeOperators@ extension to be enabled.
+data a >>> b
+
+instance (ModifyOptions a, ModifyOptions b) => ModifyOptions (a >>> b) where
+  modifyOptions _ = modifyOptions (Proxy :: Proxy b) . modifyOptions (Proxy :: Proxy a)
+
+instance (ModifyText a, ModifyText b) => ModifyText (a >>> b) where
+  modifyText _ = modifyText (Proxy :: Proxy b) . modifyText (Proxy :: Proxy a)
diff --git a/test/Relation/Tupleable.hs b/test/Relation/Tupleable.hs
--- a/test/Relation/Tupleable.hs
+++ b/test/Relation/Tupleable.hs
@@ -1,13 +1,22 @@
-{-# LANGUAGE DeriveGeneric, FlexibleInstances, FlexibleContexts, DeriveAnyClass #-}
+{-# LANGUAGE
+    DeriveGeneric
+  , FlexibleInstances
+  , FlexibleContexts
+  , DeriveAnyClass
+  , ScopedTypeVariables
+  , TypeApplications
+  , DataKinds
+  , TypeOperators
+  , AllowAmbiguousTypes
+  #-}
 import Test.HUnit
-import ProjectM36.Tupleable
+import ProjectM36.Tupleable.Deriving
 import ProjectM36.Atomable
 import ProjectM36.Attribute
 import ProjectM36.Error
 import Data.Binary
 import ProjectM36.Base
 import Control.DeepSeq (NFData)
-import GHC.Generics
 import qualified Data.Vector as V
 import qualified Data.Map as M
 import qualified Data.Text as T
@@ -58,6 +67,13 @@
                 attr9C :: Double
               }
             deriving (Generic, Show, Eq)
+
+data Test10T = Test10C {
+  longAttrNameA10 :: Integer,
+  long_attr_name_b10 :: Integer,
+  longMixed_attr_NameC10 :: Integer
+  }
+  deriving (Generic, Show, Eq)
            
 instance Tupleable Test1T
 
@@ -83,7 +99,7 @@
   if errors tcounts + failures tcounts > 0 then exitFailure else exitSuccess
 
 testList :: Test
-testList = TestList [testADT1, testADT2, testADT3, testADT4, testADT6, testADT7, testADT8, testInsertExpr, testDefineExpr, testUpdateExpr, testUpdateExprEmptyAttrs, testDeleteExpr, testUpdateExprWrongAttr, testReorderedTuple]
+testList = TestList [testADT1, testADT2, testADT3, testADT4, testADT6, testADT7, testADT8, testInsertExpr, testDefineExpr, testUpdateExpr, testUpdateExprEmptyAttrs, testDeleteExpr, testUpdateExprWrongAttr, testReorderedTuple, testAddPrefixField, testDropPrefixField, testAddSuffixField, testDropSuffixField, testUpperCaseField, testLowerCaseField, testTitleCaseField, testCamelCaseField, testPascalCaseField, testSnakeCaseField, testSpinalCaseField, testTrainCaseField, testAsIsCodec, testAsIsField, testComposeRLCodec, testComposeRLField, testComposeLRCodec, testComposeLRField]
 
 testADT1 :: Test
 testADT1 = TestCase $ assertEqual "one record constructor" (Right example) (fromTuple (toTuple example))
@@ -200,3 +216,153 @@
                          attrC = 4}
       actual = fromTuple . tupleRev . toTuple $ expected
   assertEqual "reordered tuple" (Right expected) actual
+
+testCodec :: forall tag. ModifyOptions tag => String -> [AttributeName] -> Test
+testCodec msg expected = TestCase $
+    assertEqual msg (S.fromList expected) actual
+  where
+    actual = attributeNameSet $ toAttributes (Proxy :: Proxy (Codec tag Test10T))
+
+testAddPrefixField :: Test
+testAddPrefixField = testCodec
+  @(Field (AddPrefix "prefix_"))
+  "codec field add prefix attributes"
+  ["prefix_longAttrNameA10",
+   "prefix_long_attr_name_b10",
+   "prefix_longMixed_attr_NameC10"]
+
+testDropPrefixField :: Test
+testDropPrefixField = testCodec
+  @(Field (DropPrefix "long"))
+  "codec field drop prefix"
+  ["AttrNameA10",
+   "_attr_name_b10",
+   "Mixed_attr_NameC10"]
+
+testAddSuffixField :: Test
+testAddSuffixField = testCodec
+  @(Field (AddSuffix "_suffix"))
+  "codec field add suffix"
+  ["longAttrNameA10_suffix",
+   "long_attr_name_b10_suffix",
+   "longMixed_attr_NameC10_suffix"]
+
+testDropSuffixField :: Test
+testDropSuffixField = testCodec
+  @(Field (DropSuffix "10"))
+  "codec field drop suffix"
+  ["longAttrNameA",
+   "long_attr_name_b",
+   "longMixed_attr_NameC"]
+
+testUpperCaseField :: Test
+testUpperCaseField = testCodec
+  @(Field UpperCase)
+  "codec field uppercase"
+  ["LONGATTRNAMEA10",
+   "LONG_ATTR_NAME_B10",
+   "LONGMIXED_ATTR_NAMEC10"]
+
+testLowerCaseField :: Test
+testLowerCaseField = testCodec
+  @(Field LowerCase)
+  "codec field lowercase"
+  ["longattrnamea10",
+   "long_attr_name_b10",
+   "longmixed_attr_namec10"]
+
+testTitleCaseField :: Test
+testTitleCaseField = testCodec
+  @(Field TitleCase)
+  "codec field title case"
+  ["Long Attr Name A10",
+   "Long Attr Name B10",
+   "Long Mixed Attr Name C10"]
+
+testCamelCaseField :: Test
+testCamelCaseField = testCodec
+  @(Field CamelCase)
+  "codec field camel case"
+  ["longAttrNameA10",
+   "longAttrNameB10",
+   "longMixedAttrNameC10"]
+
+testPascalCaseField :: Test
+testPascalCaseField = testCodec
+  @(Field PascalCase)
+  "codec field pascal case"
+  ["LongAttrNameA10",
+   "LongAttrNameB10",
+   "LongMixedAttrNameC10"]
+
+testSnakeCaseField :: Test
+testSnakeCaseField = testCodec
+  @(Field SnakeCase)
+  "codec field snake case"
+  ["long_attr_name_a10",
+   "long_attr_name_b10",
+   "long_mixed_attr_name_c10"]
+
+testSpinalCaseField :: Test
+testSpinalCaseField = testCodec
+  @(Field SpinalCase)
+  "codec field spinal case"
+  ["long-attr-name-a10",
+   "long-attr-name-b10",
+   "long-mixed-attr-name-c10"]
+
+testTrainCaseField :: Test
+testTrainCaseField = testCodec
+  @(Field TrainCase)
+  "codec field train case"
+  ["Long-Attr-Name-A10",
+   "Long-Attr-Name-B10",
+   "Long-Mixed-Attr-Name-C10"]
+
+testAsIsCodec :: Test
+testAsIsCodec = testCodec
+  @AsIs
+  "codec as is"
+  ["longAttrNameA10",
+   "long_attr_name_b10",
+   "longMixed_attr_NameC10"]
+
+testAsIsField :: Test
+testAsIsField = testCodec
+  @(Field AsIs)
+  "codec field as is"
+  ["longAttrNameA10",
+   "long_attr_name_b10",
+   "longMixed_attr_NameC10"]
+
+testComposeRLCodec :: Test
+testComposeRLCodec = testCodec
+  @(Field UpperCase <<< Field (DropPrefix "long"))
+  "codec (<<<)"
+  ["ATTRNAMEA10",
+   "_ATTR_NAME_B10",
+   "MIXED_ATTR_NAMEC10"]
+
+testComposeRLField :: Test
+testComposeRLField = testCodec
+  @(Field (UpperCase <<< DropPrefix "long"))
+  "codec field (<<<)"
+  ["ATTRNAMEA10",
+   "_ATTR_NAME_B10",
+   "MIXED_ATTR_NAMEC10"]
+
+testComposeLRCodec :: Test
+testComposeLRCodec = testCodec
+  @(Field (DropPrefix "long") >>> Field UpperCase)
+  "codec (>>>)"
+  ["ATTRNAMEA10",
+   "_ATTR_NAME_B10",
+   "MIXED_ATTR_NAMEC10"]
+
+testComposeLRField :: Test
+testComposeLRField = testCodec
+  @(Field (DropPrefix "long" >>> UpperCase))
+  "codec field (>>>)"
+  ["ATTRNAMEA10",
+   "_ATTR_NAME_B10",
+   "MIXED_ATTR_NAMEC10"]
diff --git a/test/TransactionGraph/Merge.hs b/test/TransactionGraph/Merge.hs
--- a/test/TransactionGraph/Merge.hs
+++ b/test/TransactionGraph/Merge.hs
@@ -4,6 +4,7 @@
 import ProjectM36.Attribute
 import ProjectM36.Relation
 import ProjectM36.Transaction
+import ProjectM36.TransactionInfo as TI
 import ProjectM36.TransactionGraph
 import ProjectM36.Error
 import ProjectM36.Key
@@ -14,6 +15,17 @@
 
 import qualified Data.ByteString.Lazy as BS
 import System.Exit
+
+
+
+
+
+
+
+
+
+
+
 import Data.Word
 import qualified Data.UUID as U
 import qualified Data.Set as S
@@ -21,7 +33,6 @@
 import Data.Maybe
 import Data.Time.Clock
 import Data.Time.Calendar
-import qualified Data.List.NonEmpty as NE
 
 {-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}
 
@@ -61,10 +72,9 @@
       uuidA = fakeUUID 10
       uuidB = fakeUUID 11
       uuidRoot = fakeUUID 1
-      parents' = uuidRoot NE.:| []
       rootContext = concreteDatabaseContext rootTrans
-  (_, bsGraph') <- addTransaction "branchA" (createTrans uuidA (TransactionInfo parents' testTime) rootContext) bsGraph
-  (_, bsGraph'') <- addTransaction "branchB" (createTrans uuidB (TransactionInfo parents' testTime) rootContext) bsGraph'
+  (_, bsGraph') <- addTransaction "branchA" (createTrans uuidA (TI.singleParent uuidRoot testTime) rootContext) bsGraph
+  (_, bsGraph'') <- addTransaction "branchB" (createTrans uuidB (TI.singleParent uuidRoot testTime) rootContext) bsGraph'
   pure bsGraph''
   
 addTransaction :: HeadName -> Transaction -> TransactionGraph -> IO (Transaction, TransactionGraph)
@@ -108,7 +118,7 @@
   baseGraph <- basicTransactionGraph  
   transA <- assertMaybe (transactionForHead "branchA" baseGraph) "failed to get branchA"
   transB <- assertMaybe (transactionForHead "branchB" baseGraph) "failed to get branchB"
-  (_, graph) <- addTransaction "branchC" (createTrans (fakeUUID 12) (TransactionInfo (fakeUUID 1 NE.:| []) testTime) (concreteDatabaseContext transA)) baseGraph
+  (_, graph) <- addTransaction "branchC" (createTrans (fakeUUID 12) (TI.singleParent (fakeUUID 1) testTime) (concreteDatabaseContext transA)) baseGraph
   subgraph <- assertEither $ subGraphOfFirstCommonAncestor graph (transactionHeadsForGraph baseGraph) transA transB S.empty
   assertGraph subgraph
   let graphEq graphArg = S.map transactionId (transactionsForGraph graphArg)  
@@ -127,13 +137,13 @@
   updatedBranchBContext <- case runDatabaseContextEvalMonad branchBContext env (optimizeAndEvalDatabaseContextExpr True (Assign "branchBOnlyRelvar" (ExistingRelation relationTrue))) of
     Left err -> assertFailure (show err) >> undefined
     Right st -> pure $ dbc_context st
-  (_, graph') <- addTransaction "branchB" (createTrans (fakeUUID 3) (TransactionInfo (transactionId branchBTrans NE.:| []) testTime) updatedBranchBContext) graph
+  (_, graph') <- addTransaction "branchB" (createTrans (fakeUUID 3) (TI.singleParent (transactionId branchBTrans) testTime) updatedBranchBContext) graph
   branchBTrans' <- assertMaybe (transactionForHead "branchB" graph') "failed to get branchB head"  
   assertEqual "branchB id 3" (fakeUUID 3) (transactionId branchBTrans')  
   
   -- add another transaction to branchA
   branchATrans <- assertMaybe (transactionForHead "branchA" graph) "failed to get branchA head"  
-  (_, graph'') <- addTransaction "branchA" (createTrans (fakeUUID 4) (TransactionInfo (transactionId branchATrans NE.:| []) testTime) (concreteDatabaseContext branchATrans)) graph'
+  (_, graph'') <- addTransaction "branchA" (createTrans (fakeUUID 4) (TI.singleParent (transactionId branchATrans) testTime) (concreteDatabaseContext branchATrans)) graph'
   branchATrans' <- assertMaybe (transactionForHead "branchA" graph'') "failed to get branchA head"
   assertEqual "branchA id 4" (fakeUUID 4) (transactionId branchATrans')
                                               
@@ -156,7 +166,7 @@
   updatedBranchBContext <- case runDatabaseContextEvalMonad branchBContext env (optimizeAndEvalDatabaseContextExpr True (Assign "branchBOnlyRelvar" (ExistingRelation relationTrue))) of
     Left err -> assertFailure (show err) >> undefined
     Right st -> pure (dbc_context st)
-  (_, graph') <- addTransaction "branchB" (createTrans (fakeUUID 3) (TransactionInfo (transactionId branchBTrans NE.:| []) testTime) updatedBranchBContext) graph
+  (_, graph') <- addTransaction "branchB" (createTrans (fakeUUID 3) (TI.singleParent (transactionId branchBTrans) testTime) updatedBranchBContext) graph
   --create the merge transaction in the graph
   let eGraph' = runGraphRefRelationalExprM gfEnv $ mergeTransactions testTime (fakeUUID 4) (fakeUUID 10) (SelectedBranchMergeStrategy "branchA") ("branchA", "branchB")
       gfEnv = freshGraphRefRelationalExprEnv Nothing graph'
@@ -185,8 +195,8 @@
       branchBContext = (concreteDatabaseContext branchBTrans) {relationVariables = M.insert conflictRelVarName (ExistingRelation branchBRelVar) (relationVariables (concreteDatabaseContext branchBTrans))}
       conflictRelVarName = "conflictRelVar" 
 
-  (_, graph') <- addTransaction "branchA" (createTrans (fakeUUID 3) (TransactionInfo (transactionId branchATrans NE.:| []) testTime) branchAContext) graph
-  (_, graph'') <- addTransaction "branchB" (createTrans (fakeUUID 4) (TransactionInfo (transactionId branchBTrans NE.:| []) testTime) branchBContext) graph'
+  (_, graph') <- addTransaction "branchA" (createTrans (fakeUUID 3) (TI.singleParent (transactionId branchATrans) testTime) branchAContext) graph
+  (_, graph'') <- addTransaction "branchB" (createTrans (fakeUUID 4) (TI.singleParent (transactionId branchBTrans) testTime) branchBContext) graph'
   -- validate that the conflict is hidden because we preferred a branch
   let merged = runGraphRefRelationalExprM env $ mergeTransactions testTime (fakeUUID 5) (fakeUUID 3) (UnionPreferMergeStrategy "branchB") ("branchA", "branchB")
       env = freshGraphRefRelationalExprEnv Nothing graph''
@@ -215,7 +225,7 @@
       branchAOnlyIncDepName = "branchAOnlyIncDep"
       branchBOnlyRelVarName = "branchBOnlyRelVar"
       branchAOnlyIncDep = InclusionDependency (ExistingRelation relationTrue) (ExistingRelation relationTrue)
-  (_, graph') <- addTransaction "branchB" (createTrans (fakeUUID 3) (TransactionInfo (transactionId branchBTrans NE.:| []) testTime) updatedBranchBContext) graph
+  (_, graph') <- addTransaction "branchB" (createTrans (fakeUUID 3) (TI.singleParent (transactionId branchBTrans) testTime) updatedBranchBContext) graph
   let env = freshGraphRefRelationalExprEnv Nothing graph'
   (discon, _) <- assertEither $ runGraphRefRelationalExprM env $ mergeTransactions testTime (fakeUUID 5) (fakeUUID 10) UnionMergeStrategy ("branchA", "branchB")
   let Just rvExpr = M.lookup branchBOnlyRelVarName (relationVariables (Discon.concreteDatabaseContext discon))
@@ -223,7 +233,7 @@
       reEnv = freshGraphRefRelationalExprEnv (Just (Discon.concreteDatabaseContext discon)) graph
       
   assertEqual "branchBOnlyRelVar should appear in the merge" relationTrue rvRel
-  (_, graph'') <- addTransaction "branchA" (createTrans (fakeUUID 4) (TransactionInfo (transactionId branchATrans NE.:| []) testTime) updatedBranchAContext) graph'
+  (_, graph'') <- addTransaction "branchA" (createTrans (fakeUUID 4) (TI.singleParent (transactionId branchATrans) testTime) updatedBranchAContext) graph'
   let eMergeGraph = runGraphRefRelationalExprM gfEnv $ mergeTransactions testTime (fakeUUID 5) (fakeUUID 3) UnionMergeStrategy ("branchA", "branchB")
       gfEnv = freshGraphRefRelationalExprEnv Nothing graph''
   case eMergeGraph of
@@ -243,7 +253,7 @@
       conflictRelVar <- assertEither $ mkRelationFromList (attributesFromList [Attribute "conflict" IntAtomType]) []
       let conflictContextA = updatedBranchAContext {relationVariables = M.insert branchBOnlyRelVarName (ExistingRelation conflictRelVar) (relationVariables updatedBranchAContext) }
       conflictBranchATrans <- assertMaybe (transactionForHead "branchA" graph'') "retrieving head transaction for expected conflict"
-      (_, graph''') <- addTransaction "branchA" (createTrans (fakeUUID 6) (TransactionInfo (transactionId conflictBranchATrans NE.:| []) testTime) conflictContextA) graph''
+      (_, graph''') <- addTransaction "branchA" (createTrans (fakeUUID 6) (TI.singleParent (transactionId conflictBranchATrans) testTime) conflictContextA) graph''
       let failingMerge = runGraphRefRelationalExprM gfEnv' $ mergeTransactions testTime (fakeUUID 5) (fakeUUID 3) UnionMergeStrategy ("branchA", "branchB")
           gfEnv' = freshGraphRefRelationalExprEnv Nothing graph'''
       case failingMerge of
@@ -272,9 +282,9 @@
 
 
    --add the rv in new commits to both branches
-  (_, graph') <- addTransaction "branchB" (createTrans (fakeUUID 3) (TransactionInfo (transactionId branchBTrans NE.:| []) testTime) branchBContext) graph
+  (_, graph') <- addTransaction "branchB" (createTrans (fakeUUID 3) (TI.singleParent (transactionId branchBTrans) testTime) branchBContext) graph
                   
-  (_, graph'') <- addTransaction "branchA" (createTrans (fakeUUID 4) (TransactionInfo (transactionId branchATrans NE.:| []) testTime) branchAContext) graph'
+  (_, graph'') <- addTransaction "branchA" (createTrans (fakeUUID 4) (TI.singleParent (transactionId branchATrans) testTime) branchAContext) graph'
   
   --check that the union merge fails due to a violated constraint
   let eMerge = runGraphRefRelationalExprM env $ mergeTransactions testTime (fakeUUID 5) (fakeUUID 3) UnionMergeStrategy ("branchA", "branchB")
diff --git a/test/TransactionGraph/Persist.hs b/test/TransactionGraph/Persist.hs
--- a/test/TransactionGraph/Persist.hs
+++ b/test/TransactionGraph/Persist.hs
@@ -3,11 +3,12 @@
 import ProjectM36.Base
 import ProjectM36.Persist (DiskSync(NoDiskSync))
 import ProjectM36.TransactionGraph.Persist
-import ProjectM36.TransactionGraph
+import ProjectM36.TransactionGraph as TG
 import ProjectM36.Transaction
 import ProjectM36.DateExamples
 import System.IO.Temp
 import System.Exit
+import System.Directory
 import Data.Either
 import Data.UUID.V4 (nextRandom)
 import System.FilePath
@@ -16,10 +17,9 @@
 import qualified Data.Set as S
 import Data.Time.Clock
 import Data.Time.Calendar
-#ifdef PM36_HASKELL_SCRIPTING
-import ProjectM36.Client
+import ProjectM36.Client as C
 import ProjectM36.Relation
-#endif
+import ProjectM36.Transaction.Persist
 
 main :: IO ()           
 main = do 
@@ -29,7 +29,8 @@
 testList :: Test
 testList = TestList [testBootstrapDB, 
                      testDBSimplePersistence,
-                     testFunctionPersistence]
+                     testFunctionPersistence,
+                     testMerkleHashValidation]
                     
 
 stamp' :: UTCTime
@@ -74,8 +75,63 @@
                   case graphErr of
                     Left err -> assertFailure (show err)
                     Right graph'' -> assertBool "graph equality" (mapEq graph'' == mapEq graph')
-      
 
+testMerkleHashValidation :: Test
+testMerkleHashValidation = TestCase $
+  -- add a commit and validate the hashes successfully
+  withSystemTempDirectory "m36testdb" $ \tempdir -> do
+  let dbdir = tempdir </> "dbdir"
+      connInfo = InProcessConnectionInfo (MinimalPersistence dbdir) emptyNotificationCallback []
+  conn <- assertIOEither $ connectProjectM36 connInfo
+  sess <- assertIOEither $ createSessionAtHead conn "master"
+  Right _ <- executeDatabaseContextExpr sess conn (Assign "x" (ExistingRelation relationTrue))
+  Right _ <- commit sess conn
+  val <- C.validateMerkleHashes sess conn
+  assertEqual "merkle success" (Right ()) val
+
+  --read graph from disk
+  conn' <- assertIOEither $ connectProjectM36 connInfo
+  sess' <- assertIOEither $ createSessionAtHead conn' "master"
+
+  val' <- C.validateMerkleHashes sess' conn'
+  assertEqual "merkle read again success" (Right ()) val'
+
+  --alter the on-disk representation and check that the hash validation fails
+  eGraph <- transactionGraphLoad dbdir emptyTransactionGraph Nothing
+  case eGraph of
+    Left err -> assertFailure $ "failed to load graph" ++ show err
+    Right graph -> do
+      let trans = transactionHeadsForGraph graph M.! "master"
+          updatedTrans = Transaction (transactionId trans) (transactionInfo trans) updatedSchemas
+          transactionDir' = dbdir </> show (transactionId trans)
+          updatedSchemas =
+            case schemas trans of
+              Schemas ctx sschemas ->
+                let updatedContext = ctx {
+                      relationVariables = M.insert "malicious" (ExistingRelation relationFalse) (relationVariables ctx)
+                      } in
+                Schemas updatedContext sschemas
+          maliciousGraph = TransactionGraph malHeads malTransSet
+          malHeads = M.insert "master" updatedTrans (transactionHeadsForGraph graph)
+          malTransSet = S.insert updatedTrans (transactionsForGraph graph)
+          malMerkleHash = calculateMerkleHash updatedTrans maliciousGraph
+          regMerkleHash = merkleHash (transactionInfo trans)
+      --validate and fail
+      let val'' = TG.validateMerkleHashes maliciousGraph
+      assertEqual "loaded graph merkle hashes" (Left [MerkleValidationError (transactionId trans) regMerkleHash malMerkleHash]) val''
+      --delete existing transaction directory
+      removeDirectoryRecursive transactionDir'
+      writeTransaction NoDiskSync dbdir updatedTrans
+
+      --read graph from disk
+      eConnFail <- connectProjectM36 connInfo
+      case eConnFail of
+        Left err -> 
+          assertEqual "open connection merkle validation" (DatabaseValidationError [MerkleValidationError (transactionId trans) regMerkleHash malMerkleHash]) err
+        Right _ -> assertFailure "open connection validation" 
+
+
+
 --only Haskell-scripted dbc and atom functions can be serialized                   
 testFunctionPersistence :: Test
 #if !defined(PM36_HASKELL_SCRIPTING)
@@ -103,10 +159,11 @@
   let expectedRel = mkRelationFromList (attributesFromList [Attribute "a" IntAtomType]) [[IntAtom 3]]
   assertEqual "testdisk dbc function run" expectedRel res
 
+#endif  
+
 assertIOEither :: (Show a) => IO (Either a b) -> IO b
 assertIOEither x = do
   ret <- x
   case ret of
     Left err -> assertFailure (show err) >> undefined
     Right val -> pure val
-#endif  
diff --git a/test/TutorialD/Interpreter.hs b/test/TutorialD/Interpreter.hs
--- a/test/TutorialD/Interpreter.hs
+++ b/test/TutorialD/Interpreter.hs
@@ -78,7 +78,8 @@
       testAtomFunctionArgumentMismatch,
       testInvalidDataConstructor,
       testBasicList,
-      testRelationDeclarationMismatch
+      testRelationDeclarationMismatch,
+      testInvalidTuples
       ]
 
 simpleRelTests :: Test
@@ -691,3 +692,11 @@
 testRelationDeclarationMismatch = TestCase $ do
   (sessionId, dbconn) <- dateExamplesConnection emptyNotificationCallback
   expectTutorialDErr sessionId dbconn (T.isPrefixOf "AtomTypeMismatchError") "data A a = A a | B | C; a := relation{a A Integer}{tuple{a A \"1\"}}"
+
+--generate errors when the tuples in a new relation don't match
+testInvalidTuples :: Test
+testInvalidTuples = TestCase $ do
+  (sessionId, dbconn) <- dateExamplesConnection emptyNotificationCallback
+  expectTutorialDErr sessionId dbconn (T.isPrefixOf "AttributeNamesMismatchError") ":showexpr relation{tuple{a 1},tuple{b 2}}"
+  expectTutorialDErr sessionId dbconn (T.isPrefixOf "AttributeNamesMismatchError") ":showexpr relation{tuple{a 1},tuple{a 2, b 3}}"
+--  expectTutorialDErr sessionId dbconn (T.isPrefixOf "ParseErrorBundle") ":showexpr relation{tuple{a 2, a 3}}" --parse failure can't be validated with this function
