Repository: SINTEF/dlite (language: python)

Issue:
WIP: Add transactions
Work in progress. Started adding transactions.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index a5a75885a..c73e976c0 100644
--- a/README.md
+++ b/README.md
@@ -495,10 +495,11 @@ The following terms have a special meaning in dlite:
   - **Relation**: A subject-predicate-object triplet. Relations
     are immutable.
   - **Storage**: A generic handle encapsulating actual storage backends.
-  - **Transaction**: A not yet implemented feature, that enables to
-    represent the evolution of the state of a software as a series of
-    immutable instances.  See also the
-    [SOFT5 nomenclauture][SOFT5_nomenclauture].
+  - **Transaction**: An instance that has a reference to an immutable
+    (frozen) parent instance is called a *transaction*.  Transactions are
+    very useful for ensuring data provenance and makes it easy to work
+    with time series.  Conceptually, they share many similarities with
+    git.  See also the [SOFT5 nomenclauture][SOFT5_nomenclauture].
   - **uri**: A [uniform resource identifier (URI)][URI] is a
     generalisation of URL, but follows the same syntax rules.  In
     dlite, the term "uri" is used as an human readable identifier for
diff --git a/bindings/python/dlite-entity-python.i b/bindings/python/dlite-entity-python.i
index 622f169dc..7608d078f 100644
--- a/bindings/python/dlite-entity-python.i
+++ b/bindings/python/dlite-entity-python.i
@@ -481,15 +481,20 @@ def get_instance(id: "str", metaid: "str"=None, check_storages: "bool"=True) ->
 
     def __getitem__(self, ind):
         if isinstance(ind, int):
-            return self.get_property_by_index(ind)
+            value = self.get_property_by_index(ind)
         elif self.has_property(ind):
-            return self.get_property(ind)
+            value = self.get_property(ind)
         elif isinstance(ind, int):
             raise IndexError('instance property index out of range: %d' % ind)
         else:
             raise KeyError('no such property: %s' % ind)
+        if isinstance(value, np.ndarray) and self.is_frozen():
+            value.flags.writeable = False  # ensure immutability
+        return value
 
     def __setitem__(self, ind, value):
+        if self.is_frozen():
+            raise DLiteError('frozen instance does not support item assignment')
         if isinstance(ind, int):
             self.set_property_by_index(ind, value)
         elif self.has_property(ind):
@@ -507,17 +512,23 @@ def get_instance(id: "str", metaid: "str"=None, check_storages: "bool"=True) ->
             return object.__getattribute__(self, name)
         d = object.__getattribute__(self, '__dict__')
         if name in d:
-            return d[name]
+            value = d[name]
         elif self.has_property(name):
-            return _get_property(self, name)
+            value = _get_property(self, name)
         elif self.has_dimension(name):
-            return self.get_dimension_size(name)
+            value = self.get_dimension_size(name)
         else:
             raise AttributeError('Instance object has no attribute %r' % name)
+        if isinstance(value, np.ndarray) and self.is_frozen():
+            value.flags.writeable = False  # ensure immutability
+        return value
 
     def __setattr__(self, name, value):
         if name == 'this':
             object.__setattr__(self, name, value)
+        elif self.is_frozen():
+            raise DLiteError(
+                'frozen instance does not support attribute assignment')
         elif _has_property(self, name):
             _set_property(self, name, value)
         else:
@@ -601,10 +612,22 @@ def get_instance(id: "str", metaid: "str"=None, check_storages: "bool"=True) ->
         return d
 
     def asjson(self, soft7=True, uuid=True, **kwargs):
-        """Returns a JSON representation of self.  Arguments are passed to
-        json.dumps()."""
+        """Returns a JSON representation of self.  Keyword arguments are
+        passed to json.dumps()."""
         return json.dumps(self.asdict(soft7=soft7, uuid=uuid),
                           cls=InstanceEncoder, **kwargs)
+
+    # Deprecated methods
+    def get_copy(self):
+        """Returns a copy of self.
+
+        This method is deprecated.  Use copy() instead.
+        """
+        warnings.warn(
+            DeprecationWarning,
+            "Instance.get_copy() is deprecated.  Use Instance.copy() instead."
+        )
+        return self.copy()
 %}
 
 }
diff --git a/bindings/python/dlite-entity.i b/bindings/python/dlite-entity.i
index 0e6898a85..0cd7521f0 100644
--- a/bindings/python/dlite-entity.i
+++ b/bindings/python/dlite-entity.i
@@ -270,6 +270,7 @@ Instance(metaid=None, dims=None, id=None, url=None, storage=None, driver=None, l
 struct _DLiteInstance {
   %immutable;
   char uuid[DLITE_UUID_LENGTH+1];
+  unsigned char _flags;
   char *uri;
   int _refcount;
   /* const struct _DLiteMeta *meta; */
@@ -352,6 +353,27 @@ struct _DLiteInstance {
     return (const DLiteInstance *)$self->meta;
   }
 
+  %feature("docstring", "Returns reference to parent uuid.") _get_parent_uuid;
+  const char *_get_parent_uuid() {
+    return ($self->_parent) ? $self->_parent->uuid : NULL;
+  }
+
+  %feature("docstring", "Returns reference to parent hash.") _get_parent_hash;
+  %newobject _get_parent_hash;
+  char *_get_parent_hash() {
+    char *hex;
+    size_t hexsize = DLITE_HASH_SIZE*2 + 1;
+    if (!$self->_parent) return NULL;
+    if (!(hex = malloc(hexsize)))
+      FAIL("allocation failure");
+    if (strhex_encode(hex, hexsize, $self->_parent->hash, DLITE_HASH_SIZE) < 0)
+      FAIL("cannot hex-encode instance hash");
+    return hex;
+  fail:
+    if (hex) free(hex);
+    return NULL;
+  }
+
   %feature("docstring", "\
 Saves this instance to url or storage.
 
@@ -382,6 +404,159 @@ Call signatures:
     dlite_instance_save(storage, $self);
   }
 
+  %feature("docstring", "Returns a hash of the instance.") get_hash;
+  %newobject get_hash;
+  char *get_hash() {
+    uint8_t hash[DLITE_HASH_SIZE];
+    char *hex;
+    if (dlite_instance_get_hash($self, hash, DLITE_HASH_SIZE))
+      return NULL;
+    if (!(hex = malloc(2*DLITE_HASH_SIZE+1)))
+      return dlite_err(1, "allocation failure"), NULL;
+    if (strhex_encode(hex, 2*DLITE_HASH_SIZE+1, hash, DLITE_HASH_SIZE) < 0) {
+      dlite_err(1, "failed hex-encoding hash of '%s'", $self->uuid);
+      free(hex);
+      return NULL;
+    }
+    return hex;
+  }
+
+  %feature("docstring",
+           "Verify the hash of the instance.\n"
+           "\n"
+           "If `hash` is not None, this function verifies that the hash of\n"
+           "`inst` corresponds to the memory pointed to by `hash`.  The size\n"
+           "of the memory should be `DLITE_HASH_SIZE` bytes.\n"
+           "\n"
+           "If `hash` is None and `inst` has a parent, this function will\n"
+           "instead verify that the parent hash stored in `inst` corresponds\n"
+           "to the value of the parent.\n"
+           "\n"
+           "If `recursive` is true, all ancestors of `inst` are also\n"
+           "included in the verification.\n"
+           "\n"
+           "Raises a DLiteVerifyError exception if the hash is not valid."
+           ) verify_hash;
+  void verify_hash(const char *hash=NULL, bool recursive=false) {
+    uint8_t *hashp = NULL;
+    if (hash) {
+      uint8_t data[DLITE_HASH_SIZE];
+      if (strhex_decode(data, sizeof(data), hash, strlen(hash)) < 0) {
+        dlite_err(1, "cannot decode hash: %s\n", hash);
+        return;
+      }
+      hashp = data;
+    }
+    if (dlite_instance_verify_hash($self, hashp, recursive))
+      dlite_swig_exception = DLiteVerifyError;
+  }
+
+  %feature("docstring",
+           "Returns a copy of the instance.  If `newid` is given, it will be\n"
+           "the id of the new instance, otherwise it will be given a\n"
+           "random UUID.") copy;
+  %newobject copy;
+  struct _DLiteInstance *copy(const char *newid=NULL) {
+    return dlite_instance_copy($self, newid);
+  }
+
+  %feature("docstring",
+           "Make a snapshot of the current state of the instance.  It can\n"
+           "be retrieved with `get_snapshot()`.\n"
+           "\n"
+           "The instance will be a transaction whose parent is the snapshot.\n"
+           "If `inst` already has a parent, that will now be the parent of\n"
+           "the snapshot.\n"
+           "\n"
+           "The reason that `inst` must be mutable, is that its hash will\n"
+           "change due to a change in its parent.\n"
+           "\n"
+           "The snapshot will be assigned an URI of the form\n"
+           "\"snapshot-XXXXXXXXXXXX\" (or inst->uri#snapshot-XXXXXXXXXXXX\n"
+           "if inst->uri is not NULL) where each X is replaces with a\n"
+           "random character."
+           ) snapshot;
+  void snapshot(void) {
+    dlite_instance_snapshot($self);
+  }
+
+  %feature("docstring",
+           "Returns shapshot number `n` of the current instance, where `n`\n"
+           "counts backward.  Hence, `n=0` returns the current instance,\n"
+           "`n=1` returns its parent, etc...\n"
+           "\n"
+           "This function may pull snapshots back into memory. Use\n"
+           "`dlite_instance_pull()` if you know the storage where the snapshots "
+           "are stored.") get_snapshot;
+  %newobject get_snapshop;
+  struct _DLiteInstance *get_snapshot(int n=1) {
+    DLiteInstance *inst =
+      (DLiteInstance *)dlite_instance_get_snapshot($self, n);
+    if (inst) dlite_instance_incref(inst);
+    return inst;
+  }
+
+  %feature("docstring",
+           "Like `dlite_instance_get_snapshot()`, except that possible stored\n"
+           "snapshots are pulled from a specified storage to memory.\n"
+           "\n"
+           "Returns shapshot number `n` of the current instance, where `n`\n"
+           "counts backward.  Hence, `n=0` returns the current instance,\n"
+           "`n=1` returns its parent, etc... "
+           ) pull_snapshot;
+  %newobject get_snapshop;
+  struct _DLiteInstance *pull_snapshot(struct _DLiteStorage *storage, int n) {
+    DLiteInstance *inst =
+      (DLiteInstance *)dlite_instance_pull_snapshot($self, storage, n);
+    if (inst) dlite_instance_incref(inst);
+    return inst;
+  }
+
+  %feature("docstring",
+           "Push all ancestors of snapshot `n` from memory to storage,\n"
+           "where `n=0` corresponds to `inst`, `n=1` to the parent of\n"
+           "`inst`, etc...\n"
+           "\n"
+           "No snapshot is pulled back from storage, so if the snapshots are\n"
+           "already in storage, this function has no effect.\n"
+           "\n"
+           "This function starts pushing closest to the root to ensure\n"
+           "that the transaction is in a consistent state at all times."
+           ) push_snapshot;
+  void push_snapshot(struct _DLiteStorage *storage, int n) {
+    dlite_instance_push_snapshot($self, storage, n);
+  }
+
+  %feature("docstring",
+           "Turn instance `inst` into a transaction node with parent\n"
+           "`parent`.  This requires that `inst` is mutable, and `parent`\n"
+           "is immutable.  If `inst` already has a parent, it will be\n"
+           "replaced.\n"
+           "\n"
+           "Use `dlite_instance_freeze()` and `dlite_instance_is_frozen()` to\n"
+           "make and check that an instance is immutable, respectively."
+           ) set_parent;
+  void set_parent(struct _DLiteInstance *parent) {
+    dlite_instance_set_parent($self, parent);
+  }
+
+  %feature("docstring",
+           "Mark the instance as immutable.  This can never be reverted."
+           ) freeze;
+  void freeze(void) {
+    dlite_instance_freeze($self);
+  }
+
+  %feature("docstring", "Returns whether the instance is frozen and immutable.") is_frozen;
+  bool is_frozen(void) {
+    return dlite_instance_is_frozen($self);
+  }
+
+  %feature("docstring", "Verifies a transaction.") verify_transaction;
+  bool verify_transaction(void) {
+    return dlite_instance_verify_transaction($self);
+  }
+
   %feature("docstring", "Returns array with dimension sizes.") get_dimensions;
   %newobject get_dimensions;
   obj_t *get_dimensions() {
@@ -515,11 +690,19 @@ Call signatures:
   %feature("docstring",
            "") tojson;
   %newobject tojson;
-  char *tojson(int indent=0, int flags=0) {
+  char *tojson(int indent=0, bool single=false, bool urikey=false,
+               bool with_uuid=false, bool with_meta=false,
+               bool with_arrays=false, bool no_parent=false) {
+    DLiteJsonFlag flags=0;
+    if (single) flags |= dliteJsonSingle;
+    if (urikey) flags |= dliteJsonUriKey;
+    if (with_uuid) flags |= dliteJsonWithUuid;
+    if (with_meta) flags |= dliteJsonWithMeta;
+    if (with_arrays) flags |= dliteJsonArrays;
+    if (no_parent) flags |= dliteJsonNoParent;
     return dlite_json_aprint($self, indent, flags);
   }
 
-
 };
 
 
diff --git a/bindings/python/dlite-python.i b/bindings/python/dlite-python.i
index bbea82a96..ed3037acf 100644
--- a/bindings/python/dlite-python.i
+++ b/bindings/python/dlite-python.i
@@ -27,6 +27,7 @@
 
   /* forward declarations */
   static PyObject *DLiteError = NULL;
+  static PyObject *DLiteVerifyError = NULL;
   char *strndup(const char *s, size_t n);
 %}
 
@@ -62,12 +63,20 @@ int dlite_swig_set_scalar(void *ptr, DLiteType type, size_t size, obj_t *obj);
 %init %{
   Py_Initialize();  /* should already be called, but just in case... */
   import_array();  /* Initialize numpy */
+
   DLiteError = PyErr_NewExceptionWithDoc(
     "dlite.DLiteError",                      // name
     "Base exception for the dlite module.",  // doc
     NULL,                                    // base
     NULL                                     // dict
   );
+
+  DLiteVerifyError = PyErr_NewExceptionWithDoc(
+    "dlite.DLiteVerifyError",                // name
+    "Object cannot be verified.",            // doc
+    DLiteError,                              // base
+    NULL                                     // dict
+  );
 %}
 
 %numpy_typemaps(unsigned char, NPY_UBYTE,  size_t)
@@ -86,6 +95,9 @@ int dlite_swig_set_scalar(void *ptr, DLiteType type, size_t size, obj_t *obj);
 PyObject *_get_DLiteError(void) {
   return DLiteError;
 }
+PyObject *_get_DLiteVerifyError(void) {
+  return DLiteVerifyError;
+}
 
 /* Free's array of allocated strings. */
 void free_str_array(char **arr, size_t len)
@@ -1328,10 +1340,11 @@ PyObject *dlite_python_mapping_base(void);
  * Expose generic api
  * ------------------ */
 PyObject *_get_DLiteError(void);
-
+PyObject *_get_DLiteVerifyError(void);
 
 %pythoncode %{
   DLiteError = _dlite._get_DLiteError()
+  DLiteVerifyError = _dlite._get_DLiteVerifyError()
   DLiteStorageBase = _dlite._get_storage_base()
   DLiteMappingBase = _dlite._get_mapping_base()
 %}
diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
index c7cff1af1..f83f1c1e5 100644
--- a/doc/CMakeLists.txt
+++ b/doc/CMakeLists.txt
@@ -45,6 +45,8 @@ if(DOXYGEN_FOUND)
     ${dlite_SOURCE_DIR}/doc/mappings.md
     ${dlite_SOURCE_DIR}/doc/storage_plugins.md
     ${dlite_SOURCE_DIR}/doc/tools.md
+    ${dlite_SOURCE_DIR}/doc/transactions.md
+    ${dlite_SOURCE_DIR}/doc/figs/transactions.png
 
     ${dlite_SOURCE_DIR}/src/dlite.h
     ${dlite_SOURCE_DIR}/src/dlite-arrays.h
@@ -113,6 +115,11 @@ if(DOXYGEN_FOUND)
     COMMAND ${CMAKE_COMMAND}
       -E copy ${dlite_SOURCE_DIR}/doc/SOFT-metadata-structure.png
          html/SOFT-metadata-structure.png
+    COMMAND ${CMAKE_COMMAND}
+      -E make_directory html/figs
+    COMMAND ${CMAKE_COMMAND}
+      -E copy ${dlite_SOURCE_DIR}/doc/figs/transactions.png
+         html/figs/transactions.png
     COMMAND ${CMAKE_COMMAND}
       -E make_directory html/python-storage-plugins
     COMMAND ${CMAKE_COMMAND}
diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in
index 722f7c375..177854964 100644
--- a/doc/Doxyfile.in
+++ b/doc/Doxyfile.in
@@ -778,6 +778,7 @@ WARN_LOGFILE           =
 INPUT                  = @dlite_SOURCE_DIR@/doc \
                          @dlite_BINARY_DIR@/doc/README.md \
                          @dlite_SOURCE_DIR@/doc/concepts.md \
+                         @dlite_SOURCE_DIR@/doc/transactions.md \
                          @dlite_SOURCE_DIR@/src \
                          @dlite_SOURCE_DIR@/src/utils \
                          @dlite_SOURCE_DIR@/storages/python/python-storage-plugins/README.md \
diff --git a/doc/features.md b/doc/features.md
index 086305ee4..476c77ee1 100644
--- a/doc/features.md
+++ b/doc/features.md
@@ -28,3 +28,4 @@ Features
   - Fortran bindings
   - Python bindings
   - Embedding of Python allowing writing storage and mapping plugins in Python
+  - Full provenance in instance evolution; see [transactions](transactions.md)
diff --git a/doc/figs/transactions-branch.png b/doc/figs/transactions-branch.png
new file mode 100644
index 000000000..7575c8f44
Binary files /dev/null and b/doc/figs/transactions-branch.png differ
diff --git a/doc/figs/transactions.drawio b/doc/figs/transactions.drawio
new file mode 100644
index 000000000..3aa4fc7d9
--- /dev/null
+++ b/doc/figs/transactions.drawio
@@ -0,0 +1,1 @@
+<mxfile host="Electron" modified="2022-06-12T13:10:05.575Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/15.7.3 Chrome/91.0.4472.164 Electron/13.6.1 Safari/537.36" etag="0CJW7rjY4Jklgvm6WgN_" version="15.7.3" type="device" pages="2"><diagram id="YWjJvqGcW2yBJzztndss" name="snapshots">7VjRcqIwFP0aZtwHZyAI4qNat92ZttOpDzvbt0gCZDcSNkaFfv1eJCgUa+2sVnfri+aeG8LlnHtIBsMeTtNriZPoThDKDWSS1LCvDISQg+A3B7ICsEzkFEgoGdHYBhizZ1pO1OicETqrTVRCcMWSOuiLOKa+qmFYSrGsTwsEr981wSFtAGMf8yb6nREVFajnmBv8hrIwKu9smTozxeVkDcwiTMSyAtkjwx5KIVQxmqZDynPySl6K676+kl0XJmms9rlgYd0G3vPy4Z4wFGeP6f1TN2xreRaYz/UDG8jlsN4gELAsVK0yTYX7ey7KRHu2EqoPEywvSTdJGIX5f98q14GCiqWKhOZivSqCIkFLCAbLiCk6TrCfZ5bQToBFasohsmCIZ0khcMBSSvJCGOdDwYVcLWQTTL3AB3ympPhFKxnX9+gkWN+8SplmcUGlomkF0hReUzGlSmYwpcwiT+upO9rV4XLTHWVzRJXGKDGs+zFcr7yRDAZatXco6DQZJdDBOoxFnFMrxTwmOWlXJkRCqkiEIsb8VohE0/uTKpVp/+G5EnXycwl10vJ2MTkTc+nTHfV2tImxDKl6uzPzh9mpi6QcK7ao2/XgLHeO5RP03/qkU/eJ5ZzYKN1/zCjunkbpnJVR3GMZ5cN9Eng+9bf6ZOI5Hcc8lE/Muk/WvjmVT6xeQ8KW/6XBMzyfqhNa50kbqkqqhjBnYQyhD6xRwAc5WwyOXH2dmDJC+GsK1h26t93+4s2Fek1F3C2K2AdQRCEnu7N/jB6cx5vu0HaDp/hb22oIcjmk7ZbQPN0hbauC6Hz3nq312s29Z1dnfvzes6vqy96zt01OeEbbqmDz9NCafKat56UgJ996uhdLvUtBr+ec1cZjNT/wtPAnctRLPY5oKAg3H/NWuconUXv0Bw==</diagram><diagram id="Ogl24J9AhT0XdjK2_kvF" name="branch">7Vpbb9owFP41SNtDpdgmITxy6dpNrbQJbaiPJjGJNxNnjrn1188hNuCGFVibkkFfwP6O48t3zmfHBxqoN1ncCJzG9zwkrAGdcNFA/QaE0IXqMweWBQAc6BZIJGiosQ0woI/ENNTolIYksxpKzpmkqQ0GPElIIC0MC8HndrMxZ/aoKY5ICRgEmJXRIQ1lXKC+62zwW0Kj2IwMHG2ZYNNYA1mMQz7fgtB1A/UE57IoTRY9wnLyDC+z4cPnqOe2OkPXxf27wfLLvbwqOvt0zCPrJQiSyH/uun+Lvs2+PnabDx2vl8Xfncz5oR9xZphNNV8N6DE1SHfM1Vhq0XKpmfR+T7kxXGUrP3dUA+Cni41RlaL8uwNMP2pCRVeFQVO57hWqSapQUJXuPKaSDFIc5Ja5ikaFxXLCVA2oIs7SIj7GdEHCfCKUsR5nXKw6QiEm/jhQeCYF/0W2LF7gk9F4PfheHjXfMyIkWWxFkeb1hvAJkWKpmhirY6Jdq8RE13wTcQaKt4LNYFjHeLTueeNHVdCuPMKtoExzqFShqwlPcr4FnyZhzmTfUTUuZMwjnmB2x3mqOf9JpFxqTeOp5LZHcr9qI/CPpjfjUxGQZxahdx6JRUTk/hjOV/isswRhWNKZvS+8OvWwKkXBS1JUu2ULCjRPrCh0DopqHqgoWCtFNatS1JsLauwHJNgpqJHvNl2nSkEhW1AQnVhQ7jkIyvsvBeVVJKjuJQmq9M53ckW1Sm79gD+WuFfLkzbJNndaedtEawgzGiWqGigmicK7OVlU3bQ62jChYcj+5lVbyi/T5RHbnudaTkKtspO8HU5CVTnJr+owu6wLF0T1unC1z+E0Mxva3uPMr9VxBipLYlzUlQuAds3uXOAs0hjg0DwGqFciw8z7/d71MlW5NXtNBGeRygCH5jLqpqqqshndy34BPL2sziKhAQ7NaJg4rous3nMar6KqJ2l3BE+tqh1JjdGlJzUA8N8sq6Gqm1+4V7at/wmg6z8=</diagram></mxfile>
\ No newline at end of file
diff --git a/doc/figs/transactions.png b/doc/figs/transactions.png
new file mode 100644
index 000000000..adfa0d214
Binary files /dev/null and b/doc/figs/transactions.png differ
diff --git a/doc/transactions.md b/doc/transactions.md
new file mode 100644
index 000000000..261ab229e
--- /dev/null
+++ b/doc/transactions.md
@@ -0,0 +1,95 @@
+Transactions
+============
+Transactions is a concept that comes from SOFT, which allows to easy manage arbitrary long series of immutable (frozen) instances, while ensuring provenance.
+Conceptually, it shares many similarities with [git](https://git-scm.com).
+
+A basic usage for transactions is to take snapshots of the current state of your system.
+This is shown in Figure 1, where we assume that the instance `A` (which, e.g., could be a collection) describes the state of your system.
+
+We call an instance a *transaction* after we have taken a snapshot of it, i.e. when it contains a reference to an immutable previous ("parent") instance.
+
+![transactions](figs/transactions.png)
+
+**Figure 1**: *Creating a transaction by taking snapshots.
+(a) initial state of your system described by instance `A`.
+(b) creating a transaction by taking a snapshot of `A` at time `t1`.
+The snapshot, `A1`, stores an immutable (frozen) copy of `A` from the exact moment `t1` the snapshot was taken.
+(c) After another snapshot was taken at a later time `t2`.
+Blue circles represent immutable instances, red circles represent mutable instances, while arrows relate an instance to its (frozen) parent instance.*
+
+It is straight forward to create such snapshots using the Python and C APIs.
+In Python, the steps in Figure 1 can be produced by the following code:
+
+```python
+A = dlite.get_instance("A")  # Initial state (Fig. 1a)
+A.snapshot()                 # Make snapshot at time=t1 (Fig. 1b)
+# A is evolved...
+A.snapshot()                 # Make snapshot at time=t2 (Fig. 1c)
+```
+
+The corresponding code in C would be:
+
+```C
+DLiteInstance *A = dlite_instance_get("A");  // Initial state (Fig. 1a)
+dlite_instance_snapshot(A);                  // Make snapshot at time=t1 (Fig. 1b)
+// A is evolved...
+dlite_instance_snapshot(A);                  // Make snapshot at time=t2 (Fig. 1c)
+```
+
+The snapshots can be accessed using `dlite_instance_get_snapshot()`.
+For instance, accessing snapshot `A2` and creating a new branch from it can be achieved by the following four lines of Python code:
+
+```python
+A2 = A.get_snapshot()  # Access the most recent snapshot of A
+B = A2.copy()          # Create a mutable copy of A2
+B.set_parent(A2)       # Make B a transaction with A2 as parent. Shown in Fig. 2a.
+B.shapshot()           # Make a snapshop of B. Shown in Fig. 2b.
+```
+
+The corresponding C code is:
+
+```C
+// Access the most recent snapshot of instance A.
+// Note that A2 is a borrowed reference to the snapshot and should not be
+// dereferenced with dlite_instance_decref().
+const DLiteInstance *A2 = dlite_instance_get_snapshot(A, 1);
+
+// Create a mutable copy of A2.
+// Note that B is a new reference.  You should call dlite_instance_decref() when you
+// are done with it.
+DLiteInstance *B = dlite_instance_copy(A2);
+
+// Make B a transaction with A2 as parent. Shown in Fig. 2a.
+dlite_instance_set_parent(B, A2);
+
+// Make a snapshop of B. Shown in Fig. 2b.
+dlite_instance_snapshot(B);
+```
+The result of these commands are shown in Figure 2.
+
+![transactions-branch](figs/transactions-branch.png)
+
+**Figure 2**: *Creating a new branch, from a snapshot of a transaction.
+(a) Create a copy (`B`) of latest snapshot of transaction `A`.
+(b) Take a snapshot of `B`.*
+
+### Transaction parent and immutability
+
+All transactions start as a root instance with no parent instance.
+All other instances in a transaction has exactly **one** parent instance.
+All instances in a transaction that serve as a parent are immutable (that is, all instances except the leaves (the latest)).
+Non-root transaction instances store a [SHA-3](https://en.wikipedia.org/wiki/SHA-3) hash of their parent together with the parent UUID.
+This makes it possible to ensure that any of the ancestors of a transaction have not been changed - providing provenance.
+
+A transaction can be verified with the `verify_transaction()` method in Python and `dlite_instance_verify_transaction()` in C.
+
+### Memory management
+
+The number of snapshots can potentially be very large, hence it is important to be able to store them to disk in order to save memory.
+To support this, DLite implements the `pull_snapshot()` and `push_snapshot()` methods (`dlite_instance_pull_snapshot()` and `dlite_instance_push_snapshot()` in C).
+
+Calling `inst.push_snapshot(storage, n)` will push all ancestors of snapshot `n` of instance `inst` from memory to storage `storage`, where `n=0` corresponds to `inst`, `n=1` to the parent of `inst`, etc...  Hence, this will release memory.
+
+Calling `inst.pull_snapshot(storage, n)` will pull snapshot `n` of instance `inst` as well as all its descendants from storage `storage` to memory.  Hence, this is similar to `inst.get_snapshot(n)`, except that an explicit storage is used.
+
+Note that not all storages can be used with these functions, since whether an instance is a transactions or not, is not described by its metadata and hence requires special support by the storage plugin.
diff --git a/src/dlite-codegen.c b/src/dlite-codegen.c
index 94cfc1b99..4a1b95ea8 100644
--- a/src/dlite-codegen.c
+++ b/src/dlite-codegen.c
@@ -422,6 +422,7 @@ int dlite_instance_subs(TGenSubs *subs, const DLiteInstance *inst)
                       (unsigned long)meta->_headersize);
     tgen_subs_set_fmt(subs, "_init",        NULL, "NULL");
     tgen_subs_set_fmt(subs, "_deinit",      NULL, "NULL");
+    tgen_subs_set_fmt(subs, "_gethash",     NULL, "NULL");
     tgen_subs_set_fmt(subs, "_getdim",      NULL, "NULL");
     tgen_subs_set_fmt(subs, "_setdim",      NULL, "NULL");
     tgen_subs_set_fmt(subs, "_loadprop",    NULL, "NULL");
diff --git a/src/dlite-collection.c b/src/dlite-collection.c
index e51544971..63d2f6004 100644
--- a/src/dlite-collection.c
+++ b/src/dlite-collection.c
@@ -4,6 +4,8 @@
 #include <string.h>
 
 #include "utils/err.h"
+#include "utils/sha3.h"
+#include "utils/strutils.h"
 #include "dlite-macros.h"
 #include "dlite-store.h"
 #include "dlite-mapping.h"
@@ -52,6 +54,90 @@ int dlite_collection_deinit(DLiteInstance *inst)
   return 0;
 }
 
+/* Help function for dlite_collection_gethash().
+   Returns 1 if a > b, 0 if a == b and -1 if a < b. */
+int _cmp_triple(const void *a, const void *b) {
+  const Triple *t1=*(Triple **)a, *t2=*(Triple **)b;
+  int v;
+  if ((v = strcmp(t1->s, t2->s))) return v;
+  if ((v = strcmp(t1->p, t2->p))) return v;
+  if ((v = strcmp(t1->o, t2->o))) return v;
+  return 0;
+}
+
+/* Help function for dlite_collection_gethash().
+   Update the the hash with content of triple `t`. */
+void _sha3_update_triple(sha3_context *c, const Triple *t)
+{
+  sha3_Update(c, t->s, strlen(t->s));
+  sha3_Update(c, t->p, strlen(t->p));
+  sha3_Update(c, t->o, strlen(t->o));
+}
+
+/* Calculate hash of a collection. */
+int dlite_collection_gethash(const DLiteInstance *inst, uint8_t *hash,
+                             int hashsize)
+{
+  DLiteCollection *coll = (DLiteCollection *)inst;
+  sha3_context c;
+  const uint8_t *buf;
+  unsigned bitsize = hashsize * 8;
+  TripleState state;
+  const Triple *t, **triples=NULL;
+  size_t i=0, n=triplestore_length(coll->rstore);
+  int retval=1;
+
+  /* Initiate calculation of hash */
+  sha3_Init(&c, bitsize);
+  sha3_SetFlags(&c, SHA3_FLAGS_KECCAK);
+
+  if (coll->_parent) {
+    sha3_Update(&c, coll->_parent->uuid, DLITE_UUID_LENGTH);
+    sha3_Update(&c, coll->_parent->hash, DLITE_HASH_SIZE);
+  }
+  sha3_Update(&c, coll->meta->uri, strlen(coll->meta->uri));
+
+  /* Calculate the hash of a sorted copy of the relations */
+  triplestore_init_state(coll->rstore, &state);
+  if (!(triples = malloc(n * sizeof(Triple *))))
+    FAIL("allocation failure");
+  while ((t = triplestore_next(&state))) {
+    triples[i++] = t;
+  }
+  assert(i == n);
+  qsort(triples, n, sizeof(Triple *), _cmp_triple);
+  for (i=0; i<n; i++) {
+    if (strcmp(triples[i]->p, "_has-hash") == 0) continue;
+    if (strcmp(triples[i]->p, "_has-uuid") == 0) {
+      if ((t = triplestore_find_first(coll->rstore,
+                                      triples[i]->s, "_has-hash", NULL))) {
+        _sha3_update_triple(&c, t);
+      } else {
+        uint8_t hash[DLITE_HASH_SIZE];
+        DLiteInstance * inst;
+        char hex[2*DLITE_HASH_SIZE+1];
+        if (!(inst = dlite_instance_get(triples[i]->o))) goto fail;
+        if (dlite_instance_get_hash(inst, hash, DLITE_HASH_SIZE))
+          FAIL1("error calculating hash of instance '%s'", triples[i]->o);
+        if (strhex_encode(hex, sizeof(hex), hash, DLITE_HASH_SIZE) < 0)
+          FAIL1("failed hex-encoding hash of '%s'", triples[i]->o);
+        sha3_Update(&c, triples[i]->s, strlen(triples[i]->s));
+        sha3_Update(&c, "_has-hash", 9);
+        sha3_Update(&c, hex, 2*DLITE_HASH_SIZE);
+      }
+    }
+    _sha3_update_triple(&c, triples[i]);
+  }
+
+  buf = sha3_Finalize(&c);
+  memcpy(hash, buf, hashsize);
+  retval = 0;
+ fail:
+  triplestore_deinit_state(&state);
+  free(triples);
+  return retval;
+}
+
 /* Returns size of dimension number `i` or -1 on error. */
 int dlite_collection_getdim(const DLiteInstance *inst, size_t i)
 {
diff --git a/src/dlite-collection.h b/src/dlite-collection.h
index 98fb66894..077b7e0d6 100644
--- a/src/dlite-collection.h
+++ b/src/dlite-collection.h
@@ -23,6 +23,7 @@
   label   | "_is-a"         | "Instance"
   label   | "_has-uuid"     | uuid
   label   | "_has-meta"     | metadata uri
+  label   | "_has-hash"     | hash of instance
   label   | "_has-dimmap"   | relation-id  -> (instdim, "_maps-to", coldim)
   instdim | "_maps-to"      | colldim
   coldim  | "_has-size"     | size
@@ -77,6 +78,14 @@ int dlite_collection_init(DLiteInstance *inst);
  */
 int dlite_collection_deinit(DLiteInstance *inst);
 
+/**
+  Calculate hash of a collection.
+
+  Returns non-zero on error.
+*/
+int dlite_collection_gethash(const DLiteInstance *inst, uint8_t *hash,
+                             int hashsize);
+
 /**
   Returns size of dimension number `i` or -1 on error.
 */
diff --git a/src/dlite-entity.c b/src/dlite-entity.c
index 79e3816ff..1d9a1b6ad 100644
--- a/src/dlite-entity.c
+++ b/src/dlite-entity.c
@@ -558,6 +558,13 @@ static int dlite_instance_free(DLiteInstance *inst)
   /* Remove from instance cache */
   stat = _instance_store_remove(inst->uuid);
 
+  /* For transactions, decrease refcount of parent */
+  if (inst->_parent) {
+    if (inst->_parent->parent)
+      dlite_instance_decref((DLiteInstance *)(inst->_parent->parent));
+    free(inst->_parent);
+  }
+
   /* Standard free */
   nprops = meta->_nproperties;
   if (inst->uri) free((char *)inst->uri);
@@ -1223,6 +1230,10 @@ int dlite_instance_set_property_by_index(DLiteInstance *inst, size_t i,
   size_t nmemb=1;
   int j;
 
+  if (inst->_flags & dliteImmutable)
+    return err(1, "cannot set property on immutable instance: %s",
+               (inst->uri) ? inst->uri : inst->uuid);
+
   /* Count members */
   if (p->ndims)
     for (j=0; j<p->ndims; j++) nmemb *= DLITE_PROP_DIM(inst, i, j);
@@ -1671,6 +1682,10 @@ int dlite_instance_set_dimension_sizes(DLiteInstance *inst, const int *dims)
   size_t *oldpropdims=NULL;
   int *oldmembs=NULL;
 
+  if (inst->_flags & dliteImmutable)
+    return err(1, "cannot set property on immutable instance: %s",
+               (inst->uri) ? inst->uri : inst->uuid);
+
   if (!dlite_instance_is_data(inst))
     return err(1, "it is not possible to change dimensions of metadata");
 
@@ -1796,26 +1811,21 @@ int dlite_instance_set_dimension_size(DLiteInstance *inst, const char *name,
 DLiteInstance *dlite_instance_copy(const DLiteInstance *inst, const char *newid)
 {
   DLiteInstance *new=NULL;
-  size_t n;
-  int i;
+  size_t i;
   if (dlite_instance_sync_to_properties((DLiteInstance *)inst)) return NULL;
   if (!(new = dlite_instance_create(inst->meta, DLITE_DIMS(inst), newid)))
     return NULL;
-  for (n=0; n < inst->meta->_nproperties; n++) {
-    DLiteProperty *p = inst->meta->_properties + n;
-    void *src = dlite_instance_get_property_by_index(inst, n);
-    void *dst = dlite_instance_get_property_by_index(new, n);
-   if (p->ndims > 0) {
-      int nmembs=1;
-      for (i=0; i < p->ndims; i++)
-        nmembs *= DLITE_PROP_DIM(inst, n, i);
-      for (i=0; i < nmembs; i++)
-        if (!dlite_type_copy((char *)dst + i*p->size,
-                             (char *)src + i*p->size,
-                             p->type, p->size)) goto fail;
-    } else {
-      if (!dlite_type_copy(dst, src, p->type, p->size)) goto fail;
-    }
+  if (inst->_parent) {
+    if (!(new->_parent = malloc(sizeof(DLiteParent))))
+      FAIL("allocation failure");
+    memcpy(new->_parent, inst->_parent, sizeof(DLiteParent));
+    if (new->_parent->parent)
+      dlite_instance_incref((DLiteInstance *)new->_parent->parent);
+  }
+  for (i=0; i < inst->meta->_nproperties; i++) {
+    void *src = dlite_instance_get_property_by_index(inst, i);
+    assert(src);
+    if (dlite_instance_set_property_by_index(new, i, src)) goto fail;
   }
   return new;
  fail:
@@ -2012,16 +2022,26 @@ int dlite_instance_assign_casted_property_by_index(const DLiteInstance *inst,
   Return non-zero on error.
  */
 int dlite_instance_get_hash(const DLiteInstance *inst,
-                            unsigned char *hash, int hashsize)
+                            uint8_t *hash, int hashsize)
 {
   size_t i;
   sha3_context c;
-  const unsigned char *buf;
+  const uint8_t *buf;
   unsigned bitsize = hashsize * 8;
   int retval = 0;
+
+  /* If metadata defines a custom hash function, use that instead of
+     this implementation. */
+  if (inst->meta->_gethash)
+    return inst->meta->_gethash(inst, hash, hashsize);
+
   sha3_Init(&c, bitsize);
   sha3_SetFlags(&c, SHA3_FLAGS_KECCAK);
 
+  if (inst->_parent) {
+    sha3_Update(&c, inst->_parent->uuid, DLITE_UUID_LENGTH);
+    sha3_Update(&c, inst->_parent->hash, DLITE_HASH_SIZE);
+  }
   sha3_Update(&c, inst->meta->uri, strlen(inst->meta->uri));
   for (i=0; i<DLITE_NDIM(inst); i++) {
     size_t n = DLITE_DIM(inst, i);
@@ -2035,8 +2055,11 @@ int dlite_instance_get_hash(const DLiteInstance *inst,
     if (dlite_type_is_allocated(p->type)) {
       char *v = ptr;
       for (j=0; j<len; j++, v+=p->size)
-        if ((retval = dlite_type_update_sha3(&c, v, p->type, p->size)))
+        if ((retval = dlite_type_update_sha3(&c, v, p->type, p->size))) {
+          err(1, "error updating hash for property \"%s\" of instance \"%s\"",
+              p->name, (inst->uri) ? inst->uri : inst->uuid);
           break;
+        }
     } else {
       sha3_Update(&c, ptr, len*p->size);
     }
@@ -2047,6 +2070,355 @@ int dlite_instance_get_hash(const DLiteInstance *inst,
   return retval;
 }
 
+/********************************************************************
+ *  Transactions
+ ********************************************************************/
+
+/*
+  Mark the instance as immutable.  This can never be reverted.
+
+  **Note**
+  Immutability of the underlying data cannot be enforced in
+  C as long as the someone has a pointer to the instance.  However,
+  functions like dlite_instance_set_property() and
+  dlite_instance_set_dimension_size() will refuse to change the
+  instance if it is immutable.  Furthermore, if the instance is used
+  as a parent in a transaction, any changes to the underlying data
+  will be detected by calling dlite_instance_verify().
+ */
+void dlite_instance_freeze(DLiteInstance *inst)
+{
+  inst->_flags |= dliteImmutable;
+}
+
+/*
+  Returns non-zero if instance is immutable (frozen).
+ */
+int dlite_instance_is_frozen(const DLiteInstance *inst)
+{
+  return inst->_flags & dliteImmutable;
+}
+
+/* Number of random characters in shapshot id (sid) */
+#define SID_LEN 12
+
+/*
+  Make a snapshop of mutable instance `inst`.
+
+  The `inst` will be a transaction whos parent is the snapshot.  If
+  `inst` already has a parent, that will now be the parent of the
+  snapshot.
+
+  The reason that `inst` must be mutable, is that its hash will change
+  due to change in its parent.
+
+  The snapshot will be assigned an URI of the form
+  "snapshot-XXXXXXXXXXXX" (or inst->uri#snapshot-XXXXXXXXXXXX if
+  inst->uri is not NULL) where each X is replaces with a random
+  character.
+
+  On error non-zero is returned and `inst` is unchanged.
+ */
+int dlite_instance_snapshot(DLiteInstance *inst)
+{
+  DLiteInstance *snapshot=NULL;
+  int retval=1, i, c;
+  const char *id = (inst->uri) ? inst->uri : inst->uuid;
+  int len = strcspn(id, "#");
+  char *uri = NULL;
+  char sid[SID_LEN+1];
+
+  if (dlite_instance_is_frozen(inst))
+    FAIL1("cannot snapshot an immutable instance: %s", id);
+
+  /* Create a random snapshot id of graphical ASCII characters. */
+  for (i=0; i<SID_LEN; i++) {
+    do {
+      c = (rand() % (128 - 32)) + 32;  // below c=32 is not printable
+    } while (!isgraph(c) || strchr(" \"'", c));
+    sid[i] = c;
+  }
+  sid[SID_LEN] = '\0';
+  if (asprintf(&uri, "%.*s#snapshot-%s", len, id, sid) < 0)
+    FAIL1("error formatting uri for snapshot of %s", id);
+
+  if (!(snapshot = dlite_instance_copy(inst, uri))) goto fail;
+  dlite_instance_freeze(snapshot);
+  if (dlite_instance_set_parent(inst, snapshot)) goto fail;
+  retval = 0;
+ fail:
+  if (uri) free(uri);
+  if (snapshot) dlite_instance_decref(snapshot);
+  return retval;
+}
+
+/*
+  Returns a borrowed reference to shapshot number `n` of `inst`, where
+  `n` counts backward from `inst`.  Hence, `n=0` returns `inst`, `n=1`
+  returns the parent of `inst`, etc...
+
+  Snapshots may be pulled back into memory using dlite_instance_get().
+  Use dlite_instance_pull() if you know the storage that the snapshots
+  are stored in.
+
+  Returns NULL on error.
+ */
+const DLiteInstance *dlite_instance_get_snapshot(const DLiteInstance *inst,
+                                                 int n)
+{
+  int i;
+  const DLiteInstance *cur = inst;
+  if (n < 0) return err(1, "snapshot number must be positive"), NULL;
+  for (i=0; i<n; i++) {
+    DLiteParent *p = cur->_parent;
+    if (!p) return err(1, "snapshot index %d exceeds number of snapsshots: %d",
+                       n, i), NULL;
+    if (p->parent) {
+      cur = p->parent;
+    } else {
+      if (!(p->parent = dlite_instance_get(p->uuid))) return NULL;
+      cur = p->parent;
+    }
+  }
+  assert(cur);
+  return cur;
+}
+
+/*
+  Like dlite_instance_get_snapshot(), except that possible stored
+  snapshots are pulled from storage `s` to memory.
+
+  Returns a borrowed reference to snapshot `n` or NULL on error.
+ */
+const DLiteInstance *dlite_instance_pull_snapshot(const DLiteInstance *inst,
+                                                  DLiteStorage *s, int n)
+{
+  int i;
+  const DLiteInstance *cur = inst;
+  if (n < 0) return err(1, "snapshot number must be positive"), NULL;
+  if (!(s->flags & dliteTransaction))
+    return err(1, "storage does not support transactions"), NULL;
+  for (i=0; i<n; i++) {
+    DLiteParent *p = cur->_parent;
+    if (!p) return err(1, "snapshot index %d exceeds number of snapsshots: %d",
+                       n, i), NULL;
+    if (p->parent) {
+      cur = p->parent;
+    } else {
+      if (!(p->parent = dlite_storage_load(s, p->uuid))) return NULL;
+      cur = p->parent;
+    }
+  }
+  assert(cur);
+  return cur;
+}
+
+/*
+  Push all ancestors of snapshot `n` from memory to storage `s`,
+  where `n=0` corresponds to `inst`, `n=1` to the parent of `inst`, etc...
+
+  No snapshot is pulled back from storage, so if the snapshots are
+  already in storage, this function has no effect.
+
+  This function starts pushing closest to the root to ensure that the
+  transaction is in a consistent state at all times.
+
+  Returns zero on success or the (positive) index of the snapshot that
+  fails to push.
+*/
+int dlite_instance_push_snapshot(const DLiteInstance *inst,
+                                 DLiteStorage *s, int n)
+{
+  int m;
+  const DLiteInstance *p;
+  if (!(s->flags & dliteTransaction))
+    return err(1, "storage does not support transactions");
+  if (!inst->_parent || !(p = inst->_parent->parent)) return 0;
+  if ((m = dlite_instance_push_snapshot(p, s, n-1))) return m+1;
+  if (n > 0) return 0;
+  if (dlite_instance_save(s, p)) return 1;
+  inst->_parent->parent = NULL;
+  dlite_instance_decref((DLiteInstance *)p);
+  return 0;
+}
+
+/*
+  Prints transaction to stdout.  Returns non-zero on error.
+ */
+int dlite_instance_print_transaction(const DLiteInstance *inst)
+{
+  char *s=NULL;
+  size_t size=0;
+  int n=0, m;
+  const DLiteInstance *p = inst;
+  m = asnpprintf(&s, &size, n, "\n");
+  n += m;
+  do {
+    uint8_t hash[DLITE_HASH_SIZE];
+    char shash[DLITE_HASH_SIZE*2+1];
+    dlite_instance_get_hash(p, hash, sizeof(hash));
+    strhex_encode(shash, sizeof(shash), hash, sizeof(hash));
+    m = asnpprintf(&s, &size, n, "%s\n", (p->uri) ? p->uri : p->uuid);
+    n += m;
+    m = asnpprintf(&s, &size, n, "  - uuid: %s\n", p->uuid);
+    n += m;
+    m = asnpprintf(&s, &size, n, "  - hash: %s\n", shash);
+    n += m;
+  } while ((p = dlite_instance_get_snapshot(p, 1)));
+  printf("%s\n", s);
+  if(s) free(s);
+  return 0;
+}
+
+/*
+  Turn instance `inst` into a transaction node with parent `parent`.
+
+  This require that `inst` is mutable, and `parent` is immutable.
+
+  If `inst` already has a parent, it will be replaced.
+
+  Use dlite_instance_freeze() and dlite_instance_is_frozen() to make
+  and check that an instance is immutable, respectively.
+
+  Returns non-zero on error.
+ */
+int dlite_instance_set_parent(DLiteInstance *inst, const DLiteInstance *parent)
+{
+  DLiteParent *p = inst->_parent;
+  uint8_t hash[DLITE_HASH_SIZE];
+  if (inst->_flags & dliteImmutable)
+    return err(-1, "Parent cannot be added to immutable instance: %s",
+               (inst->uri) ? inst->uri : inst->uuid);
+  if (!(parent->_flags & dliteImmutable))
+    return err(-1, "Mutable instance \"%s\" cannot be added as parent",
+               (parent->uri) ? parent->uri : parent->uuid);
+  if (dlite_instance_get_hash(parent, hash, DLITE_HASH_SIZE))
+    return err(-1, "Error calculating hash of parent instance \"%s\"",
+               (parent->uri) ? parent->uri : parent->uuid);
+  if (p) {
+    if (p->parent) dlite_instance_decref((DLiteInstance *)p->parent);
+  } else {
+    if (!(p = calloc(1, sizeof(DLiteParent))))
+      return err(-1, "Allocation failure");
+    inst->_parent = p;
+  }
+  p->parent = parent;
+  strncpy(p->uuid, parent->uuid, DLITE_UUID_LENGTH+1);
+  memcpy(p->hash, hash, DLITE_HASH_SIZE);
+  dlite_instance_incref((DLiteInstance *)p->parent);
+  return 0;
+}
+
+
+
+///*
+//  Returns a new reference to the parent of instance `inst` or NULL on
+//  error or if `inst` has no parent.
+//
+//  Call dlite_instance_has_parent() to distinguish between error or that
+//  `inst` has no parent.
+// */
+//const DLiteInstance *dlite_instance_get_parent(const DLiteInstance *inst)
+//{
+//  if (!inst->_parent) return NULL;
+//  if (!inst->_parent->parent) {
+//    DLiteInstance *parent = dlite_instance_get(inst->_parent->uuid);
+//    uint8_t hash[DLITE_HASH_SIZE];
+//    if (!parent) return NULL;
+//
+//    /* Verify the parent when it is assigned with dlite_instance_get() */
+//    if (dlite_instance_get_hash(parent, hash, DLITE_HASH_SIZE))
+//      goto fail;
+//    if (memcmp(hash, inst->_parent->hash, DLITE_HASH_SIZE))
+//      FAIL1("Invalid hash for parent of instance \"%s\"",
+//            (inst->uri) ? inst->uri : inst->uuid);
+//
+//    inst->_parent->parent = parent;
+//  }
+//
+//  dlite_instance_incref((DLiteInstance *)inst->_parent->parent);
+//  return inst->_parent->parent;
+//
+// fail:
+//  dlite_instance_decref((DLiteInstance *)inst->_parent->parent);
+//  return NULL;
+//}
+
+/*
+  Returns non-zero if `inst` has a parent.
+ */
+int dlite_instance_has_parent(const DLiteInstance *inst)
+{
+  return (inst->_parent) ? 1 : 0;
+}
+
+/*
+  Verify that the hash of instance `inst`.
+
+  If `hash` is not NULL, this function verifies that the hash of
+  `inst` corresponds to the memory pointed to by `hash`.  The size of
+  the memory should be `DLITE_HASH_SIZE` bytes.
+
+  If `hash` is NULL and `inst` has a parent, this function will
+  instead verify that the parent hash stored in `inst` corresponds to
+  the value of the parent.
+
+  If `recursive` is non-zero, all ancestors of `inst` are also
+  included in the verification.
+
+  Returns zero if the hash is valid.  Otherwise non-zero is returned
+  and an error message is issued.
+
+  TODO:
+  If `recursive` is non-zero and `inst` is a collection or has
+  properties of type `dliteRef`, we should also verify the instances
+  that are referred to.  This is currently not implemented, because
+  we have to detect cyclic references to avoid infinite recursion.
+ */
+int dlite_instance_verify_hash(const DLiteInstance *inst, uint8_t *hash,
+                              int recursive)
+{
+  uint8_t calculated_hash[DLITE_HASH_SIZE];
+  const DLiteInstance *parent;
+  int stat=0;
+
+  if (hash) {
+    if (dlite_instance_get_hash(inst, calculated_hash, DLITE_HASH_SIZE))
+      return 2;
+    if (memcmp(hash, calculated_hash, DLITE_HASH_SIZE))
+      return err(1, "hash does not correspond to the value of instance \"%s\"",
+                 (inst->uri) ? inst->uri : inst->uuid);
+  }
+  if (!inst->_parent) return 0;
+  if (!(parent = inst->_parent->parent))
+    parent = dlite_instance_get(inst->_parent->uuid);
+
+  if (!parent)
+    stat = err(3, "cannot retrieve parent of instance \"%s\"",
+               (inst->uri) ? inst->uri : inst->uuid);
+  else if (recursive || !hash)
+    stat = dlite_instance_verify_hash(parent, inst->_parent->hash, recursive);
+
+  if (parent && !inst->_parent->parent)
+    dlite_instance_decref((DLiteInstance *)parent);
+
+  return stat;
+}
+
+
+/*
+  Verifies a transaction.
+
+  Equivalent to calling `dlite_instance_very_hash(inst, NULL, 1)`.
+
+  Returns zero if the hash is valid.  Otherwise non-zero is returned
+  and an error message is issued.
+ */
+int dlite_instance_verify_transaction(const DLiteInstance *inst)
+{
+  return dlite_instance_verify_hash(inst, NULL, 1);
+}
+
 
 
 /********************************************************************
diff --git a/src/dlite-entity.h b/src/dlite-entity.h
index 2028f9264..8ce4c91e3 100644
--- a/src/dlite-entity.h
+++ b/src/dlite-entity.h
@@ -129,6 +129,7 @@
 
 #include <stddef.h>
 #include "utils/boolean.h"
+#include "utils/integers.h"
 #include "utils/jsmnx.h"
 #include "dlite-misc.h"
 #include "dlite-type.h"
@@ -152,6 +153,14 @@ typedef int (*DLiteInit)(struct _DLiteInstance *inst);
     Returns non-zero on error. */
 typedef int (*DLiteDeInit)(struct _DLiteInstance *inst);
 
+/** Function used by extended metadata to replace the standard hash
+    calculation in dlite_instance_get_hash().  The calculated hash is
+    stored in `hash`, where `hashsize` is the size of `hash` in bytes.
+    It should be 32, 48 or 64.
+    Returns non-zero on error. */
+typedef int (*DLiteGetHash)(const DLiteInstance *inst, unsigned char *hash,
+                            int hashsize);
+
 /** Function for accessing internal dimension sizes of extended
     metadata.  If provided it will be called by dlite_instance_save(),
     dlite_instance_get_dimension_size() and related functions.
@@ -180,18 +189,40 @@ typedef int (*DLiteLoadProperty)(const DLiteInstance *inst, size_t i);
 typedef int (*DLiteSaveProperty)(DLiteInstance *inst, size_t i);
 
 
+/** Flags for describing the state of an instance.  This should be as
+    minimalistic as possible, but a flag for immutability is needed. */
+typedef enum _DLiteFlag {
+  dliteImmutable=1  /*!< Whether instance is immutable. */
+} DLiteFlag;
+
+/** The size in bytes of sha3 hash used by transactions.
+    Should be 32, 48 or 64. */
+#define DLITE_HASH_SIZE 32
+
+/** Reference to parent instance.  Used by transactions. */
+typedef struct _DLiteParent {
+  const struct _DLiteInstance *parent;  /*!< Pointer to parent instance.
+                                       May be NULL, in which case the
+                                       parent must be accessed via uuid. */
+  char uuid[DLITE_UUID_LENGTH+1]; /*!< UUID of parent instance. */
+  uint8_t hash[DLITE_HASH_SIZE];  /*!< Hash of parent instance. */
+} DLiteParent;
+
 
 /**
   Initial segment of all DLite instances.
 */
 #define DLiteInstance_HEAD                                              \
   char uuid[DLITE_UUID_LENGTH+1]; /* UUID for this instance. */         \
+  uint8_t _flags;                 /* OR'ed sum of DLiteFlag. */         \
   const char *uri;                /* Unique uri for this instance. */   \
                                   /* May be NULL. */                    \
   int _refcount;                  /* Number of references to this */    \
                                   /* instance. */                       \
   const struct _DLiteMeta *meta;  /* Pointer to the metadata descri- */ \
-                                  /* bing this instance. */
+                                  /* bing this instance. */             \
+  DLiteParent *_parent;           /* For transactions, reference to */  \
+                                  /* parent instance. May be NULL. */
 
 
 /**
@@ -224,6 +255,7 @@ typedef int (*DLiteSaveProperty)(DLiteInstance *inst, size_t i);
                           /* sizeof(DLiteMeta). */                      \
   DLiteInit _init;        /* Function initialising an instance. */      \
   DLiteDeInit _deinit;    /* Function deinitialising an instance. */    \
+  DLiteGetHash _gethash;  /* Function calculating instance hash. */     \
   DLiteGetDimension _getdim;   /* Gets dim. size from internal state.*/ \
   DLiteSetDimension _setdim;   /* Sets dim. size of internal state. */  \
   DLiteLoadProperty _loadprop; /* Loads internal state from prop. */    \
@@ -992,6 +1024,152 @@ int dlite_instance_get_hash(const DLiteInstance *inst,
                             unsigned char *hash, int hashsize);
 
 
+/** @} */
+/* ================================================================= */
+/**
+ * @name Transactions
+ */
+/* ================================================================= */
+/** @{ */
+
+/**
+  Make instance immutable.  This can never be reverted.
+
+  **Note**
+  Immutability of the underlying data cannot be enforced in
+  C as long as the someone has a pointer to the instance.  However,
+  functions like dlite_instance_set_property() and
+  dlite_instance_set_dimension_size() will refuse to change the
+  instance if it is immutable.  Furthermore, if the instance is used
+  as a parent in a transaction, any changes to the underlying data
+  will be detected by calling dlite_instance_verify_transaction().
+ */
+void dlite_instance_freeze(DLiteInstance *inst);
+
+/**
+  Returns non-zero if instance is immutable (frozen).
+ */
+int dlite_instance_is_frozen(const DLiteInstance *inst);
+
+/**
+  Make a snapshop of mutable instance `inst`.
+
+  The `inst` will be a transaction whos parent is the snapshot.  If
+  `inst` already has a parent, that will now be the parent of the
+  snapshot.
+
+  The reason that `inst` must be mutable, is that its hash will change
+  due to change in its parent.
+
+  The snapshot will be assigned an URI of the form
+  "snapshot-XXXXXXXXXXXX" (or inst->uri#snapshot-XXXXXXXXXXXX if
+  inst->uri is not NULL) where each X is replaces with a random
+  character.
+
+  On error non-zero is returned and `inst` is unchanged.
+ */
+int dlite_instance_snapshot(DLiteInstance *inst);
+
+/**
+  Returns a borrowed reference to shapshot number `n` of `inst`, where
+  `n` counts backward from `inst`.  Hence, `n=0` returns `inst`, `n=1`
+  returns the parent of `inst`, etc...
+
+  Snapshots may be pulled back into memory using dlite_instance_get().
+  Use dlite_instance_pull() if you know the storage that the snapshots
+  are stored in.
+
+  Returns NULL on error.
+ */
+const DLiteInstance *dlite_instance_get_snapshot(const DLiteInstance *inst,
+                                                 int n);
+
+/**
+  Like dlite_instance_get_snapshot(), except that stored snapshots are pulled
+  from `s` to memory.
+
+  Returns a borrowed reference to snapshot `n` or NULL on error.
+ */
+const DLiteInstance *dlite_instance_pull_snapshot(const DLiteInstance *inst,
+                                                  DLiteStorage *s, int n);
+
+/**
+  Push all ancestors of snapshot `n` from memory to storage `s`,
+  where `n=0` corresponds to `inst`, `n=1` to the parent of `inst`, etc...
+
+  No snapshot is pulled back from storage, so if the snapshots are
+  already in storage, this function has no effect.
+
+  This function starts pushing closest to the root to ensure that the
+  transaction is in a consistent state at all times.
+
+  Returns zero on success or the (positive) index of the snapshot that
+  fails to push.
+*/
+int dlite_instance_push_snapshot(const DLiteInstance *inst,
+                                 DLiteStorage *s, int n);
+
+/**
+  Prints transaction to stdout.  Returns non-zero on error.
+ */
+int dlite_instance_print_transaction(const DLiteInstance *inst);
+
+/**
+  Turn instance `inst` into a transaction node with parent `parent`.
+
+  This require that `inst` is mutable, and `parent` is immutable.
+
+  If `inst` already has a parent, it will be replaced.
+
+  Use dlite_instance_freeze() and dlite_instance_is_frozen() to make
+  and check that an instance is immutable, respectively.
+
+  Returns non-zero on error.
+ */
+int dlite_instance_set_parent(DLiteInstance *inst, const DLiteInstance *parent);
+
+/**
+  Returns non-zero if `inst` has a parent.
+ */
+int dlite_instance_has_parent(const DLiteInstance *inst);
+
+/**
+  Verify that the hash of instance `inst`.
+
+  If `hash` is not NULL, this function verifies that the hash of
+  `inst` corresponds to the memory pointed to by `hash`.  The size of
+  the memory should be `DLITE_HASH_SIZE` bytes.
+
+  If `hash` is NULL and `inst` has a parent, this function will
+  instead verify that the parent hash stored in `inst` corresponds to
+  the value of the parent.
+
+  If `recursive` is non-zero, all ancestors of `inst` are also
+  included in the verification.
+
+  Returns zero if the hash is valid.  Otherwise non-zero is returned
+  and an error message is issued.
+
+  @todo
+  If `recursive` is non-zero and `inst` is a collection or has
+  properties of type `dliteRef`, we should also verify the instances
+  that are referred to.  This is currently not implemented, because
+  we have to detect cyclic references to avoid infinite recursion.
+ */
+int dlite_instance_verify_hash(const DLiteInstance *inst, uint8_t *hash,
+                               int recursive);
+
+/**
+  Verifies a transaction.
+
+  Equivalent to calling `dlite_instance_very_hash(inst, NULL, 1)`.
+
+  Returns zero if the hash is valid.  Otherwise non-zero is returned
+  and an error message is issued.
+ */
+int dlite_instance_verify_transaction(const DLiteInstance *inst);
+
+
 /** @} */
 /* ================================================================= */
 /**
diff --git a/src/dlite-json.c b/src/dlite-json.c
index 376de6484..9ffa222ba 100644
--- a/src/dlite-json.c
+++ b/src/dlite-json.c
@@ -74,6 +74,7 @@ int dlite_json_sprint(char *dest, size_t size, const DLiteInstance *inst,
   int n=0, ok=0, m, j;
   size_t i;
   char *in = malloc(indent + 1);
+  char *prop_comma = (inst->_parent && !(flags & dliteJsonNoParent)) ? "," : "";
   memset(in, ' ', indent);
   in[indent] = '\0';
 
@@ -109,7 +110,7 @@ int dlite_json_sprint(char *dest, size_t size, const DLiteInstance *inst,
       n += m;
       PRINT1("%s\n", c);
     }
-    PRINT1("%s  }\n", in);
+    PRINT2("%s  }%s\n", in, prop_comma);
 
   } else if (flags & dliteJsonArrays) {  // metadata: soft5 format
     DLiteMeta *met = (DLiteMeta *)inst;
@@ -163,7 +164,7 @@ int dlite_json_sprint(char *dest, size_t size, const DLiteInstance *inst,
         DLiteRelation *r = met->_relations + i;
         PRINT4("%s    [\"%s\", \"%s\", \"%s\"]\n", in, r->s, r->p, r->o);
       }
-      PRINT1("%s  ]\n", in);
+      PRINT2("%s  ]%s\n", in, prop_comma);
     }
 
   } else {  // metadata: soft7 format
@@ -214,9 +215,21 @@ int dlite_json_sprint(char *dest, size_t size, const DLiteInstance *inst,
         DLiteRelation *r = met->_relations + i;
         PRINT4("%s    [\"%s\", \"%s\", \"%s\"]\n", in, r->s, r->p, r->o);
       }
-      PRINT1("%s  ]\n", in);
+      PRINT2("%s  ]%s\n", in, prop_comma);
     }
   }
+
+  if (inst->_parent && !(flags & dliteJsonNoParent)) {
+    char hex[DLITE_HASH_SIZE*2 + 1];
+    if (strhex_encode(hex, sizeof(hex),
+                      inst->_parent->hash, DLITE_HASH_SIZE) < 0)
+      FAIL1("cannot encode hash of parent: %s", inst->_parent->uuid);
+    PRINT1("%s  \"parent\": {\n", in);
+    PRINT2("%s    \"uuid\": \"%s\",\n", in, inst->_parent->uuid);
+    PRINT2("%s    \"hash\": \"%s\"\n", in, hex);
+    PRINT1("%s  }\n", in);
+  }
+
   PRINT1("%s}", in);
 
   ok = 1;
diff --git a/src/dlite-json.h b/src/dlite-json.h
index f3f9cfbb2..1e98c6dbd 100644
--- a/src/dlite-json.h
+++ b/src/dlite-json.h
@@ -14,13 +14,14 @@
 
 /** Flags for controlling serialisation */
 typedef enum {
-  dliteJsonSingle=1,    /*!< single-entity format */
+  dliteJsonSingle=1,    /*!< Single-entity format */
   dliteJsonUriKey=2,    /*!< Use uri (if it exists) as json key in multi-
                              entity format. */
-  dliteJsonWithUuid=4,  /*!< include uuid in output */
-  dliteJsonWithMeta=8,  /*!< always include "meta" (even for metadata) */
-  dliteJsonArrays=16,   /*!< write metadata dimension and properties as
+  dliteJsonWithUuid=4,  /*!< Include uuid in output */
+  dliteJsonWithMeta=8,  /*!< Always include "meta" (even for metadata) */
+  dliteJsonArrays=16,   /*!< Write metadata dimension and properties as
                              json arrays (old format) */
+  dliteJsonNoParent=32, /*!< Do not write transaction parent info */
 } DLiteJsonFlag;
 
 /** Enum indicating whether a JSON string is formatted as data and metadata. */
diff --git a/src/dlite-schemas.c b/src/dlite-schemas.c
index 2db2517c7..71374e071 100644
--- a/src/dlite-schemas.c
+++ b/src/dlite-schemas.c
@@ -115,9 +115,11 @@ static struct _BasicMetadataSchema {
 } basic_metadata_schema = {
   /* -- header */
   "a8194052-7d3b-530f-ba1e-7e82fd51bf31",        /* uuid (corresponds to uri) */
+  0,                                             /* _flags */
   DLITE_BASIC_METADATA_SCHEMA,                   /* uri */
   1,                                             /* _refcount, never free */
   (DLiteMeta *)&basic_metadata_schema,           /* meta */
+  NULL,                                          /* _parent */
 
   3,                                             /* _ndimensions */
   7,                                             /* _nproperties */
@@ -130,6 +132,7 @@ static struct _BasicMetadataSchema {
   0,                                             /* _headersize */
   NULL,                                          /* _init */
   NULL,                                          /* _deinit */
+  NULL,                                          /* _gethash */
   NULL,                                          /* _getdim */
   NULL,                                          /* _setdim */
   NULL,                                          /* _loadprop */
@@ -270,9 +273,11 @@ static struct _EntitySchema {
 } entity_schema = {
   /* -- header */
   "46168985-705c-5029-b856-3ee1cccccefc",     /* uuid (corresponds to uri) */
+  0,                                          /* _flags */
   DLITE_ENTITY_SCHEMA,                        /* uri */
   1,                                          /* _refcount, never free */
   (DLiteMeta *)&basic_metadata_schema,        /* meta */
+  NULL,                                       /* _parent */
 
   2,                                          /* _ndimensions */
   6,                                          /* _nproperties */
@@ -285,6 +290,7 @@ static struct _EntitySchema {
   0,                                          /* _headersize */
   NULL,                                       /* _init */
   NULL,                                       /* _deinit */
+  NULL,                                       /* _gethash */
   NULL,                                       /* _getdim */
   NULL,                                       /* _setdim */
   NULL,                                       /* _loadprop */
@@ -367,9 +373,11 @@ static struct _CollectionEntity {
 } collection_entity = {
   /* -- header */
   "96f31fc3-3838-5cb8-8d90-eddee6ff59ca",        /* uuid (corresponds to uri) */
+  0,                                             /* _flags */
   DLITE_COLLECTION_ENTITY,                       /* uri */
   1,                                             /* _refcount, never free */
-  (DLiteMeta *)&entity_schema,                   /* meta */
+  (DLiteMeta *)&entity_schema,                   /* _meta */
+  NULL,                                          /* _parent */
 
   1,                                             /* _ndimensions */
   1,                                             /* _nproperties */
@@ -382,6 +390,7 @@ static struct _CollectionEntity {
   offsetof(DLiteCollection, nrelations),         /* _headersize */
   dlite_collection_init,                         /* _init */
   dlite_collection_deinit,                       /* _deinit */
+  dlite_collection_gethash,                      /* _gethash */
   dlite_collection_getdim,                       /* _getdim */
   NULL,                                          /* _setdim */
   dlite_collection_loadprop,                     /* _loadprop */
diff --git a/src/dlite-storage-plugins.h b/src/dlite-storage-plugins.h
index 01f481468..c71b22b8a 100644
--- a/src/dlite-storage-plugins.h
+++ b/src/dlite-storage-plugins.h
@@ -68,9 +68,10 @@ typedef struct _DLiteStoragePluginIter DLiteStoragePluginIter;
 
 /** Storage flags */
 typedef enum _DLiteStorageFlags {
-  dliteReadable=1,  /*!< Whether the storage is readable */
-  dliteWritable=2,  /*!< Whether the storage is writable */
-  dliteGeneric=4    /*!< Whether the storage may hold both data and metadata */
+  dliteReadable=1,    /*!< Whether storage is readable */
+  dliteWritable=2,    /*!< Whether storage is writable */
+  dliteGeneric=4,     /*!< Whether storage may hold both data and metadata */
+  dliteTransaction=8, /*!< Whether storage supports transactions */
 } DLiteStorageFlags;
 
 /** Base definition of a DLite storage, that all plugin storage
@@ -510,6 +511,9 @@ struct _DLiteStoragePlugin {
   LoadInstance       loadInstance;     /*!< Returns new instance from storage */
   SaveInstance       saveInstance;     /*!< Stores an instance */
 
+  //Delete
+  //Flush
+
   /* DataModel API */
   DataModel          dataModel;        /*!< Creates new data model */
   DataModelFree      dataModelFree;    /*!< Frees a data model */
diff --git a/src/dlite-type.c b/src/dlite-type.c
index 0ba023691..7cbbd29cc 100644
--- a/src/dlite-type.c
+++ b/src/dlite-type.c
@@ -1274,9 +1274,9 @@ int dlite_type_update_sha3(sha3_context *c, const void *ptr,
   case dliteRelation:
     {
       const DLiteRelation *rel = ptr;
-      sha3_Update(c, rel->s, strlen(rel->s));
-      sha3_Update(c, rel->p, strlen(rel->p));
-      sha3_Update(c, rel->o, strlen(rel->o));
+      if (rel->s) sha3_Update(c, rel->s, strlen(rel->s));
+      if (rel->p) sha3_Update(c, rel->p, strlen(rel->p));
+      if (rel->o) sha3_Update(c, rel->o, strlen(rel->o));
     }
     break;
 
diff --git a/src/utils/sha1.h b/src/utils/sha1.h
index 374d13edc..f06b8e901 100644
--- a/src/utils/sha1.h
+++ b/src/utils/sha1.h
@@ -4,6 +4,9 @@
  *
  * Distributed under terms of the MIT license.
  */
+/*
+ * Minor modifications by Jesper Friis (2017, 2020)
+ */
 
 #ifndef __sha1_h__
 #define __sha1_h__
@@ -32,21 +35,27 @@ void SHA1Transform(
     const unsigned char buffer[64]
     );
 
+/** Initialize new context */
 void SHA1Init(
     SHA1_CTX * context
     );
 
+/** Updates `context` with `data` or length `len`. */
 void SHA1Update(
     SHA1_CTX * context,
     const unsigned char *data,
     uint32_t len
     );
 
+/** Writes digest to `digest`. */
 void SHA1Final(
     unsigned char digest[20],
     SHA1_CTX * context
     );
 
+/** Convenience function. Writes digest of `str` (of size `len`) to
+    `hash_out`.  `hash_out` must be a string with length of at least 21
+    bytes. */
 void SHA1(
     char *hash_out,
     const char *str,
diff --git a/src/utils/sha3.c b/src/utils/sha3.c
index 10339025e..9c77d4e8b 100644
--- a/src/utils/sha3.c
+++ b/src/utils/sha3.c
@@ -117,7 +117,7 @@ keccakf(uint64_t s[25])
 
 /* For Init or Reset call these: */
 sha3_return_t
-sha3_Init(void *priv, unsigned bitSize) {
+sha3_Init(sha3_context *priv, unsigned bitSize) {
     sha3_context *ctx = (sha3_context *) priv;
     if( bitSize != 256 && bitSize != 384 && bitSize != 512 )
         return SHA3_RETURN_BAD_PARAMS;
@@ -127,25 +127,25 @@ sha3_Init(void *priv, unsigned bitSize) {
 }
 
 void
-sha3_Init256(void *priv)
+sha3_Init256(sha3_context *priv)
 {
     sha3_Init(priv, 256);
 }
 
 void
-sha3_Init384(void *priv)
+sha3_Init384(sha3_context *priv)
 {
     sha3_Init(priv, 384);
 }
 
 void
-sha3_Init512(void *priv)
+sha3_Init512(sha3_context *priv)
 {
     sha3_Init(priv, 512);
 }
 
 enum SHA3_FLAGS
-sha3_SetFlags(void *priv, enum SHA3_FLAGS flags)
+sha3_SetFlags(sha3_context *priv, enum SHA3_FLAGS flags)
 {
     sha3_context *ctx = (sha3_context *) priv;
     flags &= SHA3_FLAGS_KECCAK;
@@ -155,7 +155,7 @@ sha3_SetFlags(void *priv, enum SHA3_FLAGS flags)
 
 
 void
-sha3_Update(void *priv, void const *bufIn, size_t len)
+sha3_Update(sha3_context *priv, void const *bufIn, size_t len)
 {
     sha3_context *ctx = (sha3_context *) priv;
 
@@ -249,7 +249,7 @@ sha3_Update(void *priv, void const *bufIn, size_t len)
  * bytes are always present, but they can be the same byte.
  */
 void const *
-sha3_Finalize(void *priv)
+sha3_Finalize(sha3_context *priv)
 {
     sha3_context *ctx = (sha3_context *) priv;
 
diff --git a/src/utils/sha3.h b/src/utils/sha3.h
index 6e0339959..fa5cbb330 100644
--- a/src/utils/sha3.h
+++ b/src/utils/sha3.h
@@ -58,17 +58,17 @@ enum SHA3_RETURN {
 typedef enum SHA3_RETURN sha3_return_t;
 
 /* For Init or Reset call these: */
-sha3_return_t sha3_Init(void *priv, unsigned bitSize);
+sha3_return_t sha3_Init(sha3_context *priv, unsigned bitSize);
 
-void sha3_Init256(void *priv);
-void sha3_Init384(void *priv);
-void sha3_Init512(void *priv);
+void sha3_Init256(sha3_context *priv);
+void sha3_Init384(sha3_context *priv);
+void sha3_Init512(sha3_context *priv);
 
-enum SHA3_FLAGS sha3_SetFlags(void *priv, enum SHA3_FLAGS);
+enum SHA3_FLAGS sha3_SetFlags(sha3_context *priv, enum SHA3_FLAGS);
 
-void sha3_Update(void *priv, void const *bufIn, size_t len);
+void sha3_Update(sha3_context *priv, void const *bufIn, size_t len);
 
-void const *sha3_Finalize(void *priv);
+void const *sha3_Finalize(sha3_context *priv);
 
 /* Single-call hashing */
 sha3_return_t sha3_HashBuffer(
diff --git a/tools/templates/c-source.txt b/tools/templates/c-source.txt
index 75ee2ef31..da3379e4c 100644
--- a/tools/templates/c-source.txt
+++ b/tools/templates/c-source.txt
@@ -31,6 +31,8 @@
  *         Name of _init() function
  *     _deinit
  *         Name of _deinit() function
+ *     _gethash
+ *         Name of _gethash() function
  *     _getdim
  *         Name of _getdim() function
  *     _setdim
@@ -121,10 +123,12 @@ static DLiteProperty {name%u}_properties[] = {{
 static {basename}Struct {name%u} = {{
 #ifdef HAVE_DLITE
   /* -- {basename}Struct_HEAD */
-  "{_uuid}",  {@52}/* _uuid */
-  "{_uri}",   {@52}/* _uri */
+  "{_uuid}",  {@52}/* uuid */
+  0,          {@52}/* _flags */
+  "{_uri}",   {@52}/* uri */
   1,                        {@52}/* _refcount */
-  NULL,                     {@52}/* _meta */
+  NULL,                     {@52}/* meta */
+  NULL,                     {@52}/* _parent */
 {@if:{ismeta}=1}
   {_ndimensions},           {@52}/* _ndimensions */
   {_nproperties},           {@52}/* _nproperties */
@@ -137,6 +141,7 @@ static {basename}Struct {name%u} = {{
   offsetof({basename}, {dim0}), {@52}/* _headersize */
   {_init},                  {@52}/* _init */
   {_deinit},                {@52}/* _deinit */
+  {_gethash},               {@52}/* _gethash */
   {_getdim},                {@52}/* _getdim */
   {_setdim},                {@52}/* _setdim */
   {_loadprop},              {@52}/* _loadprop */


Test patch:
diff --git a/bindings/python/tests/CMakeLists.txt b/bindings/python/tests/CMakeLists.txt
index a34fb9fe9..dedc26fa1 100644
--- a/bindings/python/tests/CMakeLists.txt
+++ b/bindings/python/tests/CMakeLists.txt
@@ -14,6 +14,7 @@ set(tests
   test_global_dlite_state
   test_property_mappings
   test_datamodel
+  test_transaction
   test_mapping
   test_rdf
   test_triplestore
diff --git a/bindings/python/tests/test_collection.py b/bindings/python/tests/test_collection.py
index a75cc1661..3b1db8448 100755
--- a/bindings/python/tests/test_collection.py
+++ b/bindings/python/tests/test_collection.py
@@ -51,9 +51,9 @@
 assert data[2] == data[0]
 
 # Load
-with dlite.Storage('json', str(outdir / 'coll0.json'), 'mode=r') as s:
+with dlite.Storage('json', outdir / 'coll0.json', 'mode=r') as s:
     coll0 = dlite.Collection.load(s, id='mycoll')
-coll1 = dlite.Collection.load('json', str(outdir / 'coll1.json'), 'mode=r',
+coll1 = dlite.Collection.load('json', outdir / 'coll1.json', 'mode=r',
                               id=coll.uuid)
 coll2 = dlite.Collection.load(f'json://{outdir}/coll2.json?mode=r#mycoll')
 assert coll0 == coll
diff --git a/bindings/python/tests/test_transaction.py b/bindings/python/tests/test_transaction.py
new file mode 100644
index 000000000..73c5f27ea
--- /dev/null
+++ b/bindings/python/tests/test_transaction.py
@@ -0,0 +1,85 @@
+from pathlib import Path
+
+import dlite
+
+
+# Configure paths
+thisdir = Path(__file__).parent.absolute()
+
+dlite.storage_path.append(thisdir / '*.json')
+Person = dlite.get_instance('http://onto-ns.com/meta/0.1/Person')
+
+person = Person(dims={"N": 4})
+person.name = "Knud H. Thomsen"
+person.age = 30
+person.skills = ["writing", "humor", "history", "human knowledge"]
+
+# Update person 6 times and create a snapshot before each change
+for i in range(6):
+    person.snapshot()
+    person.age += 5
+
+assert person.age == 30 + 6 * 5  # 60
+for i in range(7):
+    assert person.get_snapshot(i).age == 30 + (6 - i) * 5
+try:
+    person.get_snapshot(7)
+except dlite.DLiteError:
+    pass
+else:
+    raise Exception(
+        "Should've failed test (getting non-existant snapshot), but didn't."
+    )
+assert person._get_parent_uuid() == person.get_snapshot(1).uuid
+assert person._get_parent_hash() == person.get_snapshot(1).get_hash()
+
+# Check mutability
+assert person.is_frozen() == False
+assert person.get_snapshot(1).is_frozen() == True
+assert person.get_snapshot(2).is_frozen() == True
+
+frozen = person.get_snapshot(1)
+assert frozen.is_frozen() == True
+try:
+    frozen.age = 77
+except dlite.DLiteError as exc:
+    assert str(exc) == "frozen instance does not support attribute assignment"
+else:
+    raise Exception("frozen instance should not accept attribute assignment")
+
+try:
+    frozen.skills[0] = "skiing"
+except ValueError:
+    pass
+else:
+    raise Exception("frozen property array should not accept assignment")
+
+# Create branch
+inst = person.copy()
+inst.set_parent(person.get_snapshot(3))
+inst.age = 50
+inst.snapshot()
+inst.age = 55
+for i in range(6):
+    assert inst.get_snapshot(i).age == 55 - i*5
+
+for i in range(4):
+    assert inst.get_snapshot(i + 2).age == person.get_snapshot(3 + i).age
+
+try:
+    inst.get_snapshot(6)
+except dlite.DLiteError:
+    pass
+else:
+    raise Exception(
+        "Should've failed test (getting non-existant snapshot), but didn't"
+    )
+
+# Push to storage
+db = thisdir / "output" / "db.json"
+if db.exists():
+    db.unlink()  # Make sure that the db is empty
+
+with dlite.Storage("json", db, "mode=w") as storage:
+    #person.push_snapshot(storage, 1)
+    storage.save(person)
diff --git a/src/tests/test_entity.c b/src/tests/test_entity.c
index 3ab5afa65..f09bc0d7a 100644
--- a/src/tests/test_entity.c
+++ b/src/tests/test_entity.c
@@ -61,22 +61,22 @@ MU_TEST(test_meta_create)
   mu_assert_int_eq(1, dlite_instance_is_meta((DLiteInstance *)entity));
   mu_assert_int_eq(0, dlite_instance_is_metameta((DLiteInstance *)entity));
 
-  dlite_instance_debug((DLiteInstance *)entity);
+  //dlite_instance_debug((DLiteInstance *)entity);
 
   /* be careful here.. the expected values are for a memory-aligned 64 bit
      system */
 #if (__GNUC__ && SIZEOF_VOID_P == 8)
-  mu_assert_int_eq(64, sizeof(DLiteInstance));
-  mu_assert_int_eq(64, entity->_dimoffset);
-  mu_assert_int_eq(64, entity->_headersize);
-  mu_assert_int_eq(80, entity->_propoffsets[0]);
-  mu_assert_int_eq(88, entity->_propoffsets[1]);
-  mu_assert_int_eq(96, entity->_propoffsets[2]);
-  mu_assert_int_eq(104, entity->_propoffsets[3]);
-  mu_assert_int_eq(112, entity->_propoffsets[4]);
-  mu_assert_int_eq(120, entity->_reloffset);
-  mu_assert_int_eq(120, entity->_propdimsoffset);
-  mu_assert_int_eq(152, entity->_propdimindsoffset);
+  mu_assert_int_eq(72, sizeof(DLiteInstance));
+  mu_assert_int_eq(72, entity->_dimoffset);
+  mu_assert_int_eq(72, entity->_headersize);
+  mu_assert_int_eq(88, entity->_propoffsets[0]);
+  mu_assert_int_eq(96, entity->_propoffsets[1]);
+  mu_assert_int_eq(104, entity->_propoffsets[2]);
+  mu_assert_int_eq(112, entity->_propoffsets[3]);
+  mu_assert_int_eq(120, entity->_propoffsets[4]);
+  mu_assert_int_eq(128, entity->_reloffset);
+  mu_assert_int_eq(128, entity->_propdimsoffset);
+  mu_assert_int_eq(160, entity->_propdimindsoffset);
 #endif
 }
 
@@ -389,6 +389,139 @@ MU_TEST(test_instance_get_hash)
 }
 
 
+MU_TEST(test_transactions)
+{
+  int stat;
+  size_t dims[] = {1, 3};
+  DLiteInstance *inst = dlite_instance_get("mydata");
+  DLiteInstance *inst2 = dlite_instance_create(entity, dims, NULL);
+  DLiteInstance *inst3 = dlite_instance_create(entity, dims, NULL);
+  mu_check(inst);
+  mu_check(inst2);
+  mu_check(inst3);
+  dlite_errclr();
+
+  stat = dlite_instance_set_parent(inst2, inst);
+  mu_assert_int_eq(-1, stat);
+  dlite_errclr();
+
+  mu_assert_int_eq(0, dlite_instance_is_frozen(inst));
+  dlite_instance_freeze(inst);
+  mu_assert_int_eq(1, dlite_instance_is_frozen(inst));
+  stat = dlite_instance_set_parent(inst2, inst);
+  mu_assert_int_eq(0, stat);
+
+  dlite_instance_freeze(inst2);
+  stat = dlite_instance_set_parent(inst3, inst2);
+  mu_assert_int_eq(0, stat);
+
+  mu_assert_int_eq(0, dlite_instance_verify_hash(inst3, NULL, 1));
+  mu_assert_int_eq(0, dlite_instance_verify_hash(inst2,inst3->_parent->hash,0));
+  mu_assert_int_eq(0, dlite_instance_verify_hash(inst,inst2->_parent->hash, 0));
+  mu_assert_int_eq(1, dlite_instance_verify_hash(inst,inst3->_parent->hash, 0));
+
+  const DLiteInstance *parent = dlite_instance_get_snapshot(inst2, 1);
+  mu_assert_ptr_eq(parent, inst);
+
+  dlite_instance_decref(inst3);
+  dlite_instance_decref(inst2);
+  dlite_instance_decref(inst);
+}
+
+MU_TEST(test_snapshot)
+{
+  /* Create a simple transaction */
+  char *path = STRINGIFY(dlite_BINARY_DIR) "/src/tests/transaction_store.json";
+  const DLiteInstance *snapshot;
+  DLiteInstance *inst = dlite_instance_get("mydata");
+  DLiteStorage *s = dlite_storage_open("json", path, "mode=w");
+  mu_check(s);
+  mu_check(inst);
+  mu_assert_int_eq(0, dlite_instance_snapshot(inst));
+  mu_assert_int_eq(0, dlite_instance_snapshot(inst));
+  mu_assert_int_eq(0, dlite_instance_snapshot(inst));
+
+  snapshot = dlite_instance_get_snapshot(inst, 0);
+  mu_assert_ptr_eq(snapshot, inst);
+
+  snapshot = dlite_instance_get_snapshot(inst, 1);
+  mu_assert_ptr_eq(snapshot, inst->_parent->parent);
+  printf("\n");
+  printf("*** snapshot: %s\n", snapshot->uri);
+
+  snapshot = dlite_instance_get_snapshot(inst, 3);
+  dlite_json_print(snapshot);
+  printf("*** snapshot: %s\n", snapshot->uri);
+
+  mu_assert_int_eq(0, dlite_instance_verify_hash(inst, NULL, 1));
+  dlite_instance_print_transaction(inst);
+
+  /*
+  mu_assert_int_eq(0, dlite_instance_push_snapshot(inst, s, 1));
+  snapshot = inst->_parent->parent;
+  mu_check(snapshot);
+  mu_assert_string_eq("8405895c-cc7f-5b91-aee7-0b679987394d",
+                   snapshot->_parent->uuid);
+  mu_assert_ptr_eq(NULL, snapshot->_parent->parent);
+
+  snapshot2 = dlite_instance_pull_snapshot(inst, s, 1);
+  mu_assert_ptr_eq(snapshot2, snapshot);
+  mu_assert_ptr_eq(NULL, snapshot2->_parent->parent);
+
+  // BIG ISSUE: hash and uuid of transaction parents are not described
+  // by the metadata.  Hence, is this info not serialised to the
+  // storage and is therefore not restored when the data is loaded.
+  snapshot2 = dlite_instance_pull_snapshot(inst, s, 2);
+  mu_assert_ptr_eq(snapshot2, snapshot->_parent->parent);
+  mu_assert_ptr_eq(NULL, snapshot2->_parent->parent);
+
+
+
+
+
+  printf("\n");
+  printf("snapshot 1: %s (%p)\n", snapshot->uuid, (void *)inst->_parent->parent);
+  printf("snapshot 2: %s (%p)\n", snapshot->_parent->uuid,
+         (void *)snapshot->_parent->parent);
+  */
+
+
+  //dlite_instance_save(s, inst);
+
+  //mu_assert_int_eq(0, dlite_instance_snapshot(inst));
+  //mu_assert_int_eq(0, dlite_instance_snapshot(inst));
+
+
+  /*
+  DLiteInstance *snapshot1 = dlite_instance_snapshot(inst);
+  DLiteInstance *snapshot2 = dlite_instance_snapshot(snapshot1);
+  uint8_t h0[DLITE_HASH_SIZE], h1[DLITE_HASH_SIZE], h2[DLITE_HASH_SIZE];
+
+  mu_assert_int_eq(0, dlite_instance_is_frozen(inst));
+  mu_assert_int_eq(1, dlite_instance_is_frozen(snapshot1));
+  mu_assert_int_eq(1, dlite_instance_is_frozen(snapshot2));
+
+  mu_check(snapshot1 != inst);
+  mu_assert_ptr_eq(snapshot1, snapshot2);
+
+  mu_assert_int_eq(0, dlite_instance_get_hash(inst, h0, DLITE_HASH_SIZE));
+  mu_assert_int_eq(0, dlite_instance_get_hash(snapshot1, h1, DLITE_HASH_SIZE));
+  mu_assert_int_eq(0, dlite_instance_get_hash(snapshot2, h2, DLITE_HASH_SIZE));
+
+  mu_assert_int_eq(1, inst->_refcount);
+  mu_assert_int_eq(2, snapshot1->_refcount);
+  mu_assert_int_eq(2, snapshot2->_refcount);
+
+  dlite_instance_decref(snapshot1);
+  dlite_instance_decref(snapshot2);
+  */
+  dlite_storage_close(s);
+  dlite_instance_decref(inst);
+
+}
+
+
+
 MU_TEST(test_meta_save)
 {
   DLiteStorage *s;
@@ -467,6 +600,8 @@ MU_TEST_SUITE(test_suite)
   MU_RUN_TEST(test_instance_snprint);
   MU_RUN_TEST(test_instance_get);
   MU_RUN_TEST(test_instance_get_hash);
+  MU_RUN_TEST(test_transactions);
+  MU_RUN_TEST(test_snapshot);
 
   MU_RUN_TEST(test_meta_save);
   MU_RUN_TEST(test_meta_load);