Here you can find documentation on how to use snarky, a DSL for using zk-SNARKS, as well as snarkyjs-crypto a companion JavaScript library providing a suite of cryptographic primitives suitable for use with snarky.
The structure of a SNAPPa
A SNAPP (or snarkified app) has two parts:
- The definition of your zk-SNARK program. This part will be built using snarky and specifically the snarky-universe standard library.
-
The rest of the application, which calls into snarky to create and verify proofs. There are currently APIs for Node.js and OCaml/ReasonML for interacting with snarky programs in this way.
We'll use two Node.js libraries:
- snarkyjs-crypto, which gives us access to cryptographic hashes, merkle-trees, and signatures that mirror those in snarky-universe
- snarkyjs which gives us methods for creating and verifying SNARK proofs.
An example appa
Let's build a simple app for proving we know a pre-image to a hash function. You can find the completed app here.
Building the SNARKa
This first step in building our SNAPP is to define our SNARK. In this case, our SNARK will prove, given a hash value h
I know a field element x such that hash(x) = h.
where hash is the Poseidon hash function provided in snarky-universe.
The snarky component -- defined in this file -- is as follows:
module Universe = (val Snarky_universe.default());
open! Universe.Impl;
open! Universe;
let input = InputSpec.[(module Hash)];
module Witness = Field;
let main = (preimage: Witness.t, h, ()) =>
Field.assertEqual(Hash.hash([|preimage|]), h);
runMain(input, (module Witness), main);
Let's break down this file bit by bit. The top 3 lines
module Universe = (val Snarky_universe.default());
open! Universe.Impl;
open! Universe;
are just a preamble which brings in scope all the functions we need. It uses the "default" SNARK construction backend, which is the Groth16 SNARK instantiated using the bn128 curve.
The next line
let input = InputSpec.[(module Hash)];
declares that the public input to our SNARK will be a hash. The "public input" (or "statement") is the value that our SNARK is checked against.
The next line
module Witness = Field;
states that our top-level "witness" (that is, the thing we're proving we know) will be a single field element, as described above.
Next, we have the main
function, which really defines our SNARK
let main = (preimage: Witness.t, h, ()) =>
Field.assertEqual(Hash.hash([|preimage|]), h);
Here we write just what we described above: main
computes the hash of the witness and asserts that it is
equal to the public input h
.
The arguments to main
must be
- The top level witness value.
- Any public inputs. Here we just have one,
h
. - A dummy
()
argument.
The final line
runMain(input, (module Witness), main);
sets up our program to work with the Node.js API.
Using the SNARKa
Now we're ready to use our SNARK. We're going to use the Node.js API.
The API is fairly straightforward. We can create a "snarky" object which has 2 methods:
prove
, which takes a statement and a witness and returns a promise of a proof.verify
, which takes a statement and a proof and returns a promise of a bool.
Here is how it all comes together:
const { bn128 } = require('snarkyjs-crypto');
const Snarky = require('snarkyjs');
const snarky = new Snarky("./ex_preimage.exe");
const preImage = bn128.Field.ofInt(5);
const statement = bn128.Hash.hash([ preImage ]);
snarky.prove({
statement: [ statement ],
witness: preImage
}).then((proof) => {
console.log("Created proof:\n" + proof + "\n");
return snarky.verify({
"statement": [ statement ],
"proof": proof
});
}, console.log).then((verified) => {
console.log("Was the proof verified? " + verified);
if (verified) {
process.exit(0);
} else {
process.exit(1);
}
}, () => { process.exit(1); });