6 minute read


Hashing is a basic security concept. Hashing is a fundamental security concept that, when combined with zk-SNARKs, opens up new possibilities for enriching your (decentralised) application. In this article you will learn:

  • What a hash is and how it’s used in the context of zk-SNARKs.
  • Create a SHA256 hash with Python and prove your knowledge of the preimage of a hash with ZoKrates.

Remarks

The content of this tutorial is partly taken from the ZoKrates Documentation. There you can find more information about the language, the standard library, supported proof backends and much more. Make sure you have followed the instructions in the Getting Started chapter and are able to run the “Hello World” example.
All the material used in this tutorial can be found in our tutorials repository. Please note that all the source code displayed on this page was compiled using ZoKrates 0.8.6. We do not guarantee its compatibility with future versions.

Notice: It’s important to note that not all hash functions are suitable for all purposes. In particular, cryptographic hash functions have additional security properties and are designed to resist certain attacks, making them suitable for secure applications such as password hashing and digital signatures. In this tutorial we will use the common cryptographic hash function SHA-256. For SHA-256 to be secure, it requires messages to be multiple of 512 as specified in RFC 4636 - SHAs and HMAC-SHAs. In this post, we will use padding to achieve the necessary length.

Introduction

A hash function is a mathematical function that takes an input (or “message”) and returns a fixed-size string of bytes, typically a hexadecimal number or string of characters. The output, often referred to as the “hash value” or “hash code”, is typically of a fixed length regardless of the size or length of the input. Hashing functions are widely used in computer science and cryptography for various purposes. Hashing functions are an integral part of blockchain technology, serving to ensure data integrity, provide security mechanisms, enable consensus algorithms such as PoW, and optimise data structures for efficient blockchain operations. They play a fundamental role in maintaining the trustworthiness and security of blockchain networks.

Application

Hashing is a well-established cryptographic mechanism, which in combination with zk-SNARKs becomes a powerful mechanism. It is used in a variety of contexts, such as:

  • Commitment Schemes: A prover can publicly commit to a secret value by writing its hash on the blockchain. Later, the same prover can demonstrate knowledge of the preimage of the hash without revealing the value using zero-knowledge proofs. One such scheme is described in this tutorial.

  • Merkle Proofs: Hashing allows the construction of Merkle trees, where each non-leaf node in the tree is the hash of its children, and the leaf nodes are hashes of input data chunks. Merkle proofs can be used to efficiently prove that a data chunk is part of the total set of chunks. Such membership proofs can be constructed using ZoKrates, as described in this tutorial.

  • Uniqueness Proofs: Zk-SNARKs and hashing can be used to create unique pseudonymous identifiers based on concatenated identity attributes. Within a zk-SNARK, the authenticity of multiple identity attributes is first proven, then the attributes are concatenated and hashed. This hash is bound to its owner by the identity attributes, which are never revealed. For more information, see the Verifiable Credential course and this paper.

Proving knowledge of a hash preimage

We’ll implement an operation that is very typical in blockchain use cases: proving knowledge of the preimage for a given hash digest. In particular, we’ll show how ZoKrates can be used to allow a prover, let’s call her Peggy, to prove to a verifier, let’s call him Victor, that she knows the initial value of a secret committed to the blockchain. However, Peggy does not necessarily trust Victor and therefore does not want to trust him with such information.

Creating a hash with Python

Peggy creates a 512-bit secret known only to her, and uses the popular SHA-256 hash algorithm to encrypt it. She uses the following Python code to generate the secret and the resulting hash:

from hashlib import sha256


def str_to_512bits(value: str) -> bytes:
    bin_str: int = int(''.join(format(i, '08b') for i in value.encode("utf-8")), base=2)
    padded_bytes: bytes = bin_str.to_bytes(64, "big")
    return padded_bytes


preimage: bytes = str_to_512bits("super secret value")
hash: bytes = sha256(preimage).digest()

print(f"preimage: 0x{preimage.hex()}")
print(f"hash: 0x{hash.hex()}")

Output:

preimage: 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007375706572207365637265742076616c7565
hash: 0xef9711ef2ad4aafa92a3e813af681fdee525dee6678b804dcdf98d7801d0d7a9 

Asserting a hash with ZoKrates

We will start this tutorial by using ZoKrates to compute the hash for an arbitrary preimage. This can be done in ZoKrates as follows:

import "hashes/sha256/512bitPadded" as sha256;


def main(private u32[2][8] msg, u32[8] hash) {

    u32[8] digest = sha256(msg[0], msg[1]);
    assert(digest == hash);
}

The first line imports the function that securely hashes a 512-bit value using SHA-256 from the ZoKrates standard library. The preimage is kept as private input, as this is the information that Peggy wants to protect from being leaked. The resulting hash is kept public, as it is the information that will convince Victor that Peggy is who she says she is (as only her can know the seret image).

Notice: How both parties agree on the exact hash depends on the later usecase, e.g. Victor writes the hash on-chain, hardcodes the hash into the proving program, or directly sends the hash to Peggy.

Having Peggy and Victor agree on the statement (“I know the secret preimage of a given hash”), which will later be proven, is specified in the ZoKrates program above. Victor and Peggy then run the offline phase of zk-SNARKs to derive the arithmetic circuit and the proof and verification keys. For the zk-SNARK scheme to be secure, a trusted setup as shown here is required. For simplicity, we just use the following commands:

$ zokrates compile -i hashexample.zok
$ zokrates setup

Proving knowledge of a preimage

With the arithmetic circuit and proof keys derived in compile and setup, Peggy can now generate a proof, but first she needs to generate a valid witness. All she has to do is format the preimage and hash values generated earlier into the appropriate data type supported by ZoKrates:

def bytes_to_u32(val: bytes) -> [int]:
    b0 = [str(int.from_bytes(val[i:i+4], "big")) for i in range(0,len(val), 4)]
    return " ".join(b0)


print(f"preimage as u32 array: {bytes_to_u32(preimage)}")
print(f"hash as u32 array: {bytes_to_u32(hash)}")

Output:

preimage as u32 array: 0 0 0 0 0 0 0 0 0 0 0 29557 1885696544 1936024434 1702109302 1634497893
hash as u32 array: 4019655151 718580474 2460215315 2942836702 3844464358 1737195597 3455683960 30463913

Peggy can now add the arguments in the correct order to compute the witness and later generate the proof:

$ zokrates compute-witness -a 0 0 0 0 0 0 0 0 0 0 0 29557 1885696544 1936024434 1702109302 1634497893 4019655151 718580474 2460215315 2942836702 3844464358 1737195597 3455683960 30463913
$ zokrates generate-proof

The generate-proof command creates a file, proof.json, containing the finished zk-SNARK proof.
The final proof consists of the public inputs declared in the ZoKrates program and three elliptic curve points. Since the preimage was declared private in ZoKrates, it does not appear in the proof, thanks to the zero-knowledge property of the protocol.

Verifying knowledge of a preimage

Verification requires the verification.key generated in setup and `proof.json Verification of the zk-SNARK proof can be done in two places:

  1. On the chain. Victor can use ZoKrates to create a smart contract with $ zokrates export-verifier as a Solidity file (verifier.sol). Victor deploys this smart contract to the Ethereum network. Verification can be triggered by calling the verifyTx function of the smart contract.
  2. Local. Victor can verify Peggy’s proof.json locally by calling $ zokrates verify.

Regardless of whether the verification takes place on- or off-chain, it is important that Victor confirms that the `proof.json’ received from Peggy matches the intended commit. This is easily checked, as the hash is kept as public input in the proof program.

Conclusion

At this point, you have successfully proved the knowledge of the preimage for a given hash digest in a zero-knowledge manner. You have also learned how to create sha256 hashes with Python and are familiar with the ZoKrates workflow. Congratulations!

Leave a comment