Usage guide

Overview

Skipchain. hippiepug.chain implements a blockchain that only requires logarithmic-size proofs of inclusion. Each block of such blockchain has not one but many hash-pointers to previous blocks.

Key-value Merkle tree. hippiepug.tree implements a verifiable dictionary as a key-value Merkle tree that guarantees unique resolution. For each lookup key, one can produce a proof of inclusion of the key in the tree. Moreover, unique resolution ensures that a proof of inclusion also proves that no other value with the same lookup key exists somewhere else in the tree.

Object store. hippiepug.store implements a content-addressable key-value store in which keys are cryptographic hashes of values. Using such storage with hippiepug data structures is convenient, because a creator of a chain or a tree only needs to provide a querier with a hash of a chain head or a tree root. That is, there is no need to explicitly produce and transmit inclusion proofs. Queriers will be able to verify inclusion on the fly, provided the storage is available. See section “Producing and verifying proofs” for more.

Using object stores

hippiepug includes an instantation of an in-memory content-addressable storage that uses SHA256 for hashes: hippiepug.store.Sha256DictStore. By default, the hashes are truncated to 8 bytes.

from hippiepug.store import Sha256DictStore

store = Sha256DictStore()
obj = b'dummy'
obj_hash = store.hash_object(obj)  # 'b5a2c96250612366'

store.add(obj) == obj_hash  # True

The store verifies hashes internally on each lookup.

obj_hash in store  # True
store.get(obj_hash) == obj   # True

If you want to use external storage, you can provide a dict-like facade to it and pass as a backend parameter:

class CustomBackend(object):

    def get(self, k):
        return 'stub'

    def __setitem__(self, k, v):
        pass

store = Sha256DictStore(backend=CustomBackend())

To change the hash function, subclass hippiepug.store.BaseDictStore, and implement the hash_object method.

You can also define a completely different store by implementing abstract base hippiepug.store.BaseStore.

Building the data structures

Chain

To append a new block to a chain, first obtain an existing chain, or initialize a new empty hippiepug.chain.Chain object:

from hippiepug.chain import Chain

chain = Chain(store)
chain.head  # None

Then, add chain blocks ony by one.

from hippiepug.chain import BlockBuilder

block_builder = BlockBuilder(chain)
block_builder.payload = b'This is the first block!'
block_builder.commit()

chain.head  # '154bdee593d8c9b2'

You can continue adding blocks using the same builder instance.

block_builder.payload  # None
block_builder.payload = b'This is the second block!'
block_builder.commit()

chain.head  # '48e399de59796ab1'

The builder automatically fills all the skipchain special block attributes, like hashes of previous blocks.

Tree

Unlike chains, hippiepug trees can not be extended. To build a new tree, initialize the tree builder on a store, and set the key-value pairs to be committed.

from hippiepug.tree import TreeBuilder

tree_builder = TreeBuilder(store)
tree_builder['foo'] = b'bar'
tree_builder['baz'] = b'wow'

Once all key-value pairs are added, commit them to store and obtain a view of the committed tree:

tree = tree_builder.commit()
tree.root  # '150cc8da6d6cfa17'

Querying the data structures

Chain

To get a queryable view of a chain, you need to specify the storage where its blocks reside, and the head of the chain (hash of the latest block). You can then retrieve blocks by their indices, or iterate.

chain = Chain(store, head='48e399de59796ab1')
first_block = chain[0]
first_block.payload  # b'This is the first block!'

for block in chain:
    print(block.index)  # will print 1, and then 0

You can also get the latest view of a current chain while building a block in block_builder.chain.

Tree

Similarly, to get a view of a tree, you need to specify the storage, and the root of the tree (hash of the root node). You can then retrieve stored values by corresponding lookup keys.

from hippepug.tree import Tree

tree = Tree(store, root='150cc8da6d6cfa17')
tree['foo']  # b'bar'
'baz' in tree  # True

Producing and verifying proofs

When the creator of a data structure and the querier use the same storage (e.g., external database), no additional work regarding inclusion proofs needs to be done, since queries produce inclusion proofs on the fly. This scenario, however, is not always possible. In such case, hippiepug allows to produce and verify proofs explicitly.

Chain

You can get the proof of block inclusion from a chain view:

block, proof = chain.get_block_by_index(0, return_proof=True)

A proof is a subset of blocks between head block and the requested block.

To verify the proof, the querier needs to locally reproduce a store, populating it with the blocks in the proof, and then query the chain in the reproduced store normally. A convenience utility hippiepug.chain.verify_chain_inclusion_proof() does all of this internally, and only returns the verification result:

from hippiepug.chain import verify_chain_inclusion_proof

verification_store = Sha256DictStore()
verify_chain_inclusion_proof(verification_store,
                             chain.head, block, proof)  # True.

Tree

You can get the proof of value and lookup key inclusion from a tree view:

value, proof = tree.get_value_by_lookup_key('foo', return_proof=True)

For trees, a proof is the list of nodes on the path from root to the leaf containing the lookup key.

The mechanism of verifying an explicit proof is the same as with chains: locally reproduce a store populating it with all the nodes in the proof, and then query normally the tree in the reproduced store. Similarly, a utility hippiepug.tree.verify_tree_inclusion_proof() does this internally and returns the verification result:

from hippiepug.tree import verify_tree_inclusion_proof

verification_store = Sha256DictStore()
verify_tree_inclusion_proof(verification_store, tree.root,
                            lookup_key='foo', value=b'bar',
                            proof=proof)  # True.

Serialization

hippiepug includes default binary serialization using msgpack library.

from hippiepug.pack import decode, encode

block = chain[0]
decode(encode(block)) == block  # True

If you want to define custom serializers, be sure to check the documentation of hippiepug.pack. You need to be careful with custom encoders to not jeopardize security of the data structure.

Once you have defined a custom encoder and decoder, you can set them to global defaults like this:

from hippiepug.pack import EncodingParams

my_params = EncodingParams()
my_params.encoder = lambda obj: b'encoded!'
my_params.decoder = lambda encoded: b'decoded!'

EncodingParams.set_global_default(my_params)

Alternatively, you can also limit their usage to a specific context:

with my_params.as_default():
    encode(b'stub')  # b'encoded!'