You can use a Merkleized binary trie.
You first hash all the elements of your set individually. In this example, I use a 3-bit hash function rather than 256-bit.
Let's say you have 5 elements in your set, and they hash to:A: 011B: 101C: 111D: 001E: 010
Now you arrange them in a tree, by using the bits of the key hash as split conditions:
root
- 0
- 0
- 1: D
- 1
- 0: E
- 1: A
- 0
- 1
- 0
- 1: B
- 1
- 1: C
- 0
Now you associate every leaf node with the full key hash, and every internal node with the hash of its concatenated children.
The resulting root is:
- Fast to update: the number of hash operations for an add or delete is proportional to the number of bits in the hash function (and does not depend on the number of elements in the tree).
- Authentic: the root commits to the entire tree structure, so indirectly to all its leaves.
- Deterministic: the order of insert/delete operations does not affect the tree structure.
A possible optimization is to compact branches constructed from internal nodes with 1 child. This leads to the following structure:
root
- 0
- 0: D
- 1
- 0: E
- 1: A
- 1
- 0: B
- 1: C
Now operations are on average logarithmic in the number of elements, and all other properties remain.