Public API

Initialization

Although pymerkle comes with concrete tree implementations, its primary purpose is to provide an abstract base class that encapsulates the cryptographic functionality of a Merkle-tree:

from pymerkle import BaseMerkleTree

Concrete implementations should inherit from this class and implement its internal abstract interface. This amounts to customizing leaf storage according to any desired application logic.

Superclass initialization

Initialization of BaseMerkleTree accepts the options shown below:

class MerkleTree(BaseMerkleTree):

    def __init__(self, *args, **kwargs):
        ...

        super().__init__(
            algorithm='sha256',
            disable_security=False,
            disable_optimizations=False,
            disable_cache=False,
            threshold=128,
            capacity=1024 ** 3
        )

    ...
  • algorithm: specifies the hash function used by the tree. Defaults to sha256.

  • disable_security: if True, resistance against second-preimage attack will be deactivated. Use it only for testing or debugging purposes. Defaults to False.

  • disable_optimizations: if True, low-level computations will fallback to recursive unoptimized functions, similar to those described in RFC 9162. Use it for comparison purposes. Defaults to False.

  • disable_cache: if True, the results of optimized low-level computations will not be cached. Use it for comparison purposes. Defaults to False.

  • threshold: specifies which outputs of a low-level computation must be cached depending on the input of the computation. Refer here for the exact meaning of this parameter. Defaults to 128.

  • capacity: cache capacity in bytes, i.e., approximate memory occupied by cached digests along with their bookkeeping. Defaults to 1GiB, which should be ample for most applications.

See here to see how to implement a Merkle-tree in detail.

Supported hash functions

sha224, sha256, sha384, sha512, sha3_224, sha3_256, sha3_384, sha3_512

Support for Keccak beyond SHA3

Installing the keccak extra, which pulls in safe-pysha3 (a maintained fork of pysha3),

pip3 install pymerkle[keccak]

makes available the following hash functions:

keccak_224, keccak_256, keccak_384, keccak_512

Warning

Requesting anything except for these raises a ValueError.

Concrete classes

Pymerkle provides two concrete implementations of BaseMerkleTree out of the box.

InmemoryTree is a non-persistent implementation where nodes are stored at runtime, intended for investigating and visualising the tree structure:

from pymerkle import InmemoryTree

tree = InmemoryTree(algorithm='sha256')

SqliteTree is a persistent implementation using a SQLite database as storage, intended for lightweight local applications:

from pymerkle import SqliteTree

tree = SqliteTree('merkle.db', algorithm='sha256')

This will open a connection to the specified database file (after creating it if not already existent). Alternatively, you can create an in-memory database as follows:

tree = SqliteTree(':memory:', algorithm='sha256')

Both trees are designed to accept data in binary format and hash it without further processing. See here for more details on these classes.

Entries

Entries are appended to the tree as leaves with contiguously increasing index. The exact type of entries depends on the particular implementation.

Note

In what follows, it is assumed without loss of generality that the tree accepts data in binary format and hashes it without further processing.

Appending an entry returns the index of the corresponding leaf (counting from one):

>>> tree.append_entry(b'foo')
1
>>> tree.append_entry(b'bar')
2

Note

The examples below refer to a tree with the entries b'foo', b'bar', b'baz', b'qux' and b'quux' appended in this order.

The index of a leaf can be used to retrieve the corresponding hash value:

>>> tree.get_leaf(1)
b'\x1d 9\xfayq\xf4\xbf\x01\xa1\xc2\x0c\xb2\xa3\xfez\xf4he\xca\x9c\xd9\xb8@\xc2\x06=\xf8\xfe\xc4\xffu'
>>>
>>> tree.get_leaf(2)
b'HY\x04\x12\x9b\xdd\xa5\xd1\xb5\xfb\xc6\xbcJ\x82\x95\x9e\xcf\xb9\x04-\xb4M\xc0\x8f\xe8~6\x0b\n?%\x01'

Hash computation

Sometimes it is useful to be able to compute independently the hash value assigned to a data entry. For example, in order to verify the inclusion proof for an entry (see below) we need to know its hash value, which can be computed without querying the tree directly (provided that its binary format can be inferred according to some known contract).

To do so, we need to configure a standalone hasher that uses the same hash function as the tree and applies the same security policy:

from pymerkle.hasher import MerkleHasher

hasher = MerkleHasher(tree.algorithm, tree.security)

The commutation between index and entry is

assert tree.get_leaf(1) == hasher.hash_buff(b'foo')

Size

The size of the tree is the current number of leaves (i.e., data entries):

>>> tree.get_size()
5

It coincides with the index of the last appended leaf.

State

The state of the tree is uniquely determined by its current root-hash. This can be retrieved as follows:

>>> tree.get_state()
b'\xdcRj\xc4\x98\x81&}\x10\xf4<\x80\x8e\xc5\x92\xa1r\x08\xefxs<\xfa\x06""\xbeS[\xc7O"'

The root-hash of any intermediate state can be retrieved by providing the corresponding size:

>>> tree.get_state(2)
b"9(jJU1b'Q\xd6\x84[\xb8\xef\xb4\xcf3\xbe\xc2\xc5\xf3\xf8C\ru\x84\x87Cq\xa3[\xda"

By convention, the empty tree state is the hash of the empty string:

>>> tree.get_state(0) == tree.hash_empty()
True

Proofs

Pymerkle is capable of generating proofs of inclusion and proofs of consistency, modeled by the verifiable InclusionProof and ConsistencyProof objects respectively.

Inclusion

Given any intermediate state, an inclusion proof is a path of hashes proving that a certain data entry has been appended at some previous moment and that the tree has not been afterwards tampered. Below the inclusion proof for the 3-rd entry against the state corresponding to the first 5 leaves:

proof = tree.prove_inclusion(3, 5)

The second argument is optional and defaults to the current tree size. Verification proceeds as follows:

from pymerkle import verify_inclusion

base = tree.get_leaf(3)
root = tree.get_state(5)

verify_inclusion(base, root, proof, index=3, size=5)

This checks that the structure of the proof is the one determined by the leaf index and size it refers to, that the path of hashes is indeed based on the acclaimed hash and that it resolves to the acclaimed state. Passing index and size ensures that the proof refers to the expected leaf position and state. Proofs are resolved with resistance against second-preimage attack unless security=False is passed; pass algorithm as well to ensure that the expected hash function is used. Trying to verify against a forged base or state would raise an InvalidProof error:

>>> from pymerkle.hasher import MerkleHasher
>>>
>>> hasher = MerkleHasher(tree.algorithm, tree.security)
>>> forged = hasher.hash_raw(b'random')
>>>
>>> verify_inclusion(forged, root, proof)
Traceback (most recent call last):
...
pymerkle.proof.InvalidProof: Base hash does not match
>>>
>>> verify_inclusion(base, forged, proof)
Traceback (most recent call last):
...
pymerkle.proof.InvalidProof: State does not match

Consistency

Given any two intermediate states, a consistency proof is a path of hashes proving that the second is a valid later state of the first, i.e., that the tree has not been tampered with in the meanwhile. Below the consistency proof for the states with three and five leaves respectively:

proof = tree.prove_consistency(3, 5)

The second argument is optional and defaults to the current tree size. Verification proceeds as follows:

from pymerkle import verify_consistency

state1 = tree.get_state(3)
state2 = tree.get_state(5)

verify_consistency(state1, state2, proof, size1=3, size2=5)

This checks that the structure of the proof is the one determined by the sizes it refers to, that an appropriate subpath of the included path of hashes resolves to the acclaimed prior state and the path of hashes as a whole resolves to the acclaimed later state. Passing size1 and size2 ensures that the proof refers to the expected states; security and algorithm behave as in the inclusion case. Trying to verify against forged states would raise an InvalidProof error:

>>> from pymerkle.hasher import MerkleHasher
>>>
>>> hasher = MerkleHasher(tree.algorithm, tree.security)
>>> forged = hasher.hash_raw(b'random')
>>>
>>> verify_consistency(forged, state2, proof)
Traceback (most recent call last):
...
pymerkle.proof.InvalidProof: Prior state does not match
>>>
>>> verify_consistency(state1, forged, proof)
Traceback (most recent call last):
...
pymerkle.proof.InvalidProof: Later state does not match

Serialization

Proofs are InclusionProof and ConsistencyProof objects respectively, both deriving from MerkleProof. They can be serialized as follows:

data = proof.serialize()

This yields a JSON entity similar to this one:

{
  "type": "inclusion",
  "metadata": {
      "algorithm": "sha256",
      "security": true,
      "size": 5,
      "index": 3
  },
  "rule": [
      0,
      1,
      0,
      0
  ],
  "path": [
      "b06d695869f105fffa5f68c4b9628d58a1aff469a3c62c8c74ddb2af47b178ef",
      "5a43bc145b8c045b535615392edfba2bd2c4556dfdb13f968bd0668c042ecb35",
      "39286a4a5531622751d6845bb8efb4cf33bec2c5f3f8430d7584874371a35bda",
      "2e219794e81e9bb22e230e7f40906eecb4cfcc289e1af253d88b23634266c5a2"
  ]
}

The metadata section contains the hash configuration of the tree (algorithm and security), which verification checks against the one expected by the verifier (see above), along with the size of the state against which the proof was requested (size). The latter can be used in order to request the acclaimed state needed for proof verification (if not otherwise available). Inclusion proofs also contain the index of the leaf in question (index), whereas consistency proofs contain the size of the prior state (prior_size). Rule determines parenthetization of hashes during path resolution. Consistency proofs additionally contain a subset entry, which selects the hashes resolving to the acclaimed prior state.

The verifiable proof-object can be retrieved as follows:

from pymerkle import MerkleProof

proof = MerkleProof.deserialize(data)

This returns a proof of the serialized type. Use InclusionProof.deserialize or ConsistencyProof.deserialize instead to also ensure that the proof is of the expected type. Proofs can also be verified by means of their verify method, e.g., proof.verify(base, root, index=3, size=5).

Malformed data raise an InvalidProof error.