snarkl
snarkl copied to clipboard
Snårkl
An Embedded DSL for Verifiable Computing
Background
Snårkl ("Snorkel") is a high-level language and compiler for verifiable computing. For an introduction to verifiable computing (VC), see the fine ACM review article by Walfish and Blumberg. For details on libsnark, the C++ library targeted by the Snårkl compiler, see, e.g. SNARKs for C by Ben-Sasson et al.
If you use Snårkl in your research, we'd appreciate it if you cite the following paper:
@conference{stewart2018padl,
author = {Gordon Stewart and Samuel Merten and Logan Leland},
title = {{Sn\aa rkl: Somewhat Practical, Pretty Much Declarative Verifiable Computing in Haskell}},
booktitle = {PADL'18: The 20th International Symposium on Practical Aspects of Declarative Languages},
series = {LNCS},
publisher = {Springer},
doi = {https://doi.org/10.1007/978-3-319-73305-0_3},
year = {2018}
}
Quickstart
git clone https://github.com/gstew5/snarkl.git snarkl
cd snarkl
./prepare-depends.sh
make
The ./prepare-depends.sh script installs libsnark in /usr. If you don't want this,
modify ./prepare-depends.sh and cppsrc/Makefile to use an alternate installation site.
Dependencies:
- GHC version >= 7.10.1;
- Cabal version >= 1.22.2.
Things may work with earlier versions, but this hasn't been tested.
To build libsnark (dependency), you'll need the following packages (Ubuntu 16.04):
sudo apt-get install build-essential git libgmp3-dev libprocps4-dev libgtest-dev libboost-all-dev libssl-dev python-markdown
Limitations
Snårkl is a preliminary research prototype undergoing active development. Pull requests welcome!
Directory Structure
snarkl/
cppsrc/ -- a C++ wrapper around 'libsnark'
scripts/ -- shell scripts
src/
Toplevel.hs -- compiler
...
examples/ -- some example Snårkl programs that exercise inductive types
Peano.hs
List.hs
Tree.hs
Lam.hs
Keccak.hs
tests/
testsuite/ -- unit tests
benchmarks/ -- microbenchmarks
Compiler Overview
Languages
-
Syntax.hs: Shallowly embedded source language
-
TExpr.hs: A simple embedded intermediate language (statically typed)
-
Expr.hs: Like
TExprbut types have been erased -
Constraints.hs: The intermediate arithmetic constraint language
-
R1CS.hs: Rank-1 constraint systems
Compiler Phases
Source
SourcetoTExprSyntax.hs
TExpr
TExprtoExprTExpr.hs
Expr
ExprtoConstraintsCompile.hs
Constraints
-
Constraint MinimizationSimplify.hs -
Dataflow AnalysisDataflow.hs -
RenumberingConstraints.hs -
ConstraintstoR1CSConstraints.hs
R1CS
Development Environment
Snårkl programs are shallowly embedded in Haskell through the use of
- Snårkl's
SyntaxandSyntaxMonadmodules, which define the combinators of the Snårkl language, and - the Snårkl
Toplevel, which exposes the API of the Snårkl compiler.
The easiest way to interact with these libraries is in Emacs, using Haskell's Interactive Mode. Assuming you have a modern cabal, set your REPL to cabal-repl by adding the following to your .emacs:
(custom-set-variables
'(haskell-process-type 'cabal-repl))
Navigate to the source directory of the repository and open one of the Snårkl examples:
emacs src/examples/Basic.hs
Type C-c C-l and follow the prompts. In the bottom-left status bar of your Emacs window, you should see a bunch of libraries compiling, followed by OK. If you don't see OK, something is probably misconfigured.
Programming
The Snårkl source language supports a number of expressive features, including: higher-order functions and parametric polymorphism (via the embedding in Haskell) as well as user-defined inductive datatypes.
In this section, we'll step through src/examples/Basic.hs and src/examples/Tree.hs, which give examples of many of these features in action.
Preliminaries
From the base of the Snårkl repository, start emacs and open src/examples/Basic.hs (C-x C-f src/examples/Basic.hs).
At the top of the file, you'll see the declarations:
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE DataKinds #-}
These turn on two GHC Haskell extensions required by Snårkl. They must appear in every Snårkl program.
RebindableSyntaxgives us a clean slate to re-implement basic Haskell syntax (e.g., Haskell's monadic>>=bind). This way, we're able to overload Haskell's monadicdo(examples below).DataKindsfacilitates the use of Snårkl's GADT-style typed expression language (src/TExpr.hs). The details here are unimportant.
Next comes the module declaration and imports:
module Basic where
import Prelude hiding
( (>>), (>>=), (+), (-), (*), (/), (&&)
, return, fromRational, negate
)
import Syntax
import SyntaxMonad
import TExpr
import Toplevel
When importing the Haskell Prelude we hide the standard definitions of functions re-implemented by Snårkl (you'll find the definitions of these functions in Syntax and SyntaxMonad). TExpr gives the deeply embedded typed expression language manipulated by high-level Snårkl programs. Toplevel imports the Snårkl compiler and its API.
Builtin Types
Out of the box, Snårkl supports arithmetic over (bounded) rationals. The following program
mult_ex :: TExp 'TField Rational -> TExp 'TField Rational-> Comp 'TField
mult_ex x y = return (x * y)
takes two Snårkl expressions as arguments (TExp 'TField Rational is the type of Snårkl expressions of type 'TField ["field element"], specialized to underlying field Rational) and returns their product. The naked expression x * y, of type TExp 'TField Rational is wrapped inside a return, coercing it to a Snårkl computation (Comp 'TField ["Snårkl computation returning a field element"]).
Snårkl also supports array-structured computation:
arr_ex :: TExp 'TField Rational -> Comp 'TField
arr_ex x = do
a <- arr 2
forall [0..1] (\i -> set (a,i) x)
y <- get (a,0)
z <- get (a,1)
return $ y + z
Here we sequence imperative computation steps using Haskell's do notation.
- The first line
a <- arr 2allocates an array of size2, bound in the rest of the computation to variablea. forall [0..1] (\i -> set (a,i) x)assigns the expressionxto every index of the array. In general, theforallcombinator takes as arguments a list of integersl, which may or may not be contiguous, and a function from integers to Snårkl computations.y <- get (a,0)dereferences the array at index0.- And likewise for
z <- ...at1. - Finally, we return the sum of
yandz.
Desugaring
The Toplevel provides support for "desugaring" high-level Snårkl computations.
p1 = arr_ex 1.0
λ> texp_of_comp p1
TExpPkg {comp_num_vars = 4, comp_input_vars = [], comp_texp = var 2 := 1 % 1; var 3 := 1 % 1; var 2+var 3}
Calling texp_of_comp on a Snårkl computation (e.g., of type Comp 'TField) produces a TExpPkg record that gives:
- the number of variables generated during desugaring;
- the number of "input" variables (these are variables that must be instantiated by the user when the expression is compiled to R1CS form);
- the resulting expression.
In the case of p1 = arr_ex 1.0, the expression assigns var 2 := 1 % 1 (1 % 1 is Haskell syntax for the rational 1/1), var 3 := 1 % 1, and returns the result of var 2 + var 3.
Interpreter
Snårkl computations can also be interpreted.
λ> comp_interp p1 []
2 % 1
Under the hood, comp_interp desugars the computation (e.g., p1) and interprets the resulting TExp. The second (here the empty list []) is the sequence of input values to be assigned to the program's input variables (if any).
R1CS Inputs
Snårkl computations may require inputs (fresh_input), in addition to function arguments. At the R1CS-level, inputs correspond to constraint variables that are instantiated by the user.
p2 = do
x <- fresh_input
return $ x + x
When desugared, p2 generates the TExpPkg
TExpPkg {comp_num_vars = 1, comp_input_vars = [0], comp_texp = var 0+var 0}
in which variable 0 is marked as an explicit input. When interpreting an expression with inputs, it's necessary to supply values for each input variable.
λ> comp_interp p2 []
*** Exception: unbound var 0 in environment fromList []
λ> comp_interp p2 [256]
512 % 1
Compiling and Evaluating Rank-1 Constraints
The Snårkl toplevel makes it relatively easy to compile a Snårkl computation to R1CS-form (using r1cs_of_comp) and then to "execute" the result (snarkify_comp).
For example,
λ> r1cs_of_comp p2
([fromList [(-1,1 % 1)]*fromList [(-1,0 % 1),(0,2 % 1),(1,(-1) % 1)]==fromList [(-1,0 % 1)]],2,[0],[1])
compiles p2 to the R1CS whose serialization reads: 1 * (0 + 2*x_0 - 1*x_1) = 0:
x_0is the input variable;x_1is the result;x_1 = x_0 + x_0if and only if the equation (on polynomials)1 * (0 + 2*x_0 - 1*x_1) = 0is satisfiable. (For a detailed introduction to rank-1 constraints, see libsnark and associated papers.)
Running p2 through libsnark yields success!
λ> snarkify_comp "p2" p2 [1.0]
Verification Succeeded.
ExitSuccess
The effect of this command is to
- compile
p2to Snårkl's internal R1CS abstract syntax; - serialize the resulting R1CS into a text file that libsnark understands;
- use
libsnarkto generate, from the input R1CS configuration, a proving key and a verification key; - calculate a satisfying assignment for the R1CS;
- use
libsnarkto generate a cryptographic proof for the R1CS, given the satisfying assignment; - finally, run the
libsnarkverifier on the resulting proof object and report the result.
Most of the libsnark-related steps above are carried out by a scripts/run-r1cs.sh.
Advanced Features
User-Defined Inductive Types
The Snårkl repository contains a number of examples of user-defined inductive types (src/examples/Peano.hs, src/examples/List.hs, src/examples/Tree.hs). In each of these files, the inductive types (for natural numbers, lists, and trees, respectively) are built against the following pattern:
- Encode the generating functor of the inductive type as a (deeply embedded) type-level function.
- Define the inductive type as the least fixed point (LFP) of the generating functor.
- Define introduction and elimination forms, for interacting with values of the inductive type.
Generating Functors
For example, the following type TF defines the generating functor of the type of binary rational trees.
type TF = 'TFSum ('TFConst 'TUnit) ('TFProd ('TFConst 'TField) ('TFProd 'TFId 'TFId))
In more standard notation:
TF(T) = unit + (Rational x (T x T))
The deeply embedded syntax of type-level functions is given by:
data TFunct where
TFConst :: Ty -> TFunct
TFId :: TFunct
TFProd :: TFunct -> TFunct -> TFunct
TFSum :: TFunct -> TFunct -> TFunct
TFComp :: TFunct -> TFunct -> TFunct
deriving Typeable
TFComp is functor composition.
Least Fixed Points
To take the LFP, we use Snårkl's μ ('TMu) type constructor:
type RatTree = 'TMu TF
We can also define polymorphic inductive types, as Haskell type-level functions:
type TF' a = 'TFSum ('TFConst 'TUnit) ('TFProd ('TFConst a) ('TFProd 'TFId 'TFId))
type TTree a = 'TMu (TF' a)
type Rat = TExp 'TField Rational
type Tree a = TExp (TTree a) Rational
This pattern is especially convenient when writing libraries.
Introduction and Elimination Forms
To construct and destruct values of the inductive type, we define
- the constructors of the new type (the introduction forms);
- elimination, or case-analysis functions, which make it possible to use values of the inductive type.
Constructors
For example, here is the leaf constructor for the type TTree given above:
leaf :: Typeable a => Comp (TTree a)
leaf = do
t <- inl unit
roll t
The polymorphic version of this type has mathematical form:
TF'(α, T) = unit + (α x (T x T))
The type itself is the LFP of the functor TF', or 'TMu TF'. To construct a "leaf" value (of type unit), we:
- use the
inlconstructor of the sum type to take a value of typeunitto a value of typeunit + (α x (TTree α x TTree α)); - "roll" this value to type
TTree αusing Snårkl'srollcombinator, which has type
roll :: (Typeable f, Typeable (Rep f ('TMu f)))
=> TExp (Rep f ('TMu f)) Rational
-> Comp ('TMu f)
In more standard notation, roll takes a value of type F (μ F), for generating functor F, and produces a value of type μ F. There is also the symmetric unroll, which takes a value of type μ F and produces a value of type F (μ F).
The node constructor is similar to leaf, except we take three arguments instead of none: v the node value, t1 the left subtree, and t2 the right subtree.
node :: Typeable a => TExp a Rational -> Tree a -> Tree a -> Comp (TTree a)
node v t1 t2 = do
p <- pair t1 t2
p' <- pair v p
t <- inr p'
roll t
Instead of inl, we use the symmetric inr sum-type constructor to build a value of the right-side type (α x (TTree α x TTree α)). pair is the standard constructor for product types.
Eliminators
The following is the sole eliminator for trees.
case_tree :: ...
=> Tree a
-> Comp a1
-> (TExp a Rational -> Tree a -> Tree a -> Comp a1)
-> Comp a1
case_tree t f_leaf f_node = do
t' <- unroll t
case_sum (\_ -> f_leaf) go t'
where go p' = do
v <- fst_pair p'
p <- snd_pair p'
t1 <- fst_pair p
t2 <- snd_pair p
f_node v t1 t2
It takes three arguments:
- the tree
t; - a computation
f_leafto run whenthas formleaf; - a computation
f_nodeto run, on argumentsv,t1, andt2, whenthas formnode v t1 t2.
The implementation of the function is in terms of the eliminators of the underlying representation types: case_sum, for eliminating values of sum type (to distinguish between leaves and nodes), and fst_pair and snd_pair for products (to further destruct node values).
The eliminator case_tree comes in handy when defining higher-level functions on trees, such as fmap:
map_tree :: ...
=> (TExp a Rational -> State Env (TExp a1 Rational))
-> TExp (TTree a) Rational
-> Comp (TTree a1)
map_tree f t
= fix go t
where go self t0 = do
case_tree t0
leaf
(\v t1 t2 -> do
v' <- f v
t1' <- self t1
t2' <- self t2
node v' t1' t2')
fix is Snårkl's fixpoint combinator. It has type:
Typeable ty2 => ((TExp ty1 Rational -> Comp ty2) -> TExp ty1 Rational -> Comp ty2)
-> TExp ty1 Rational
-> Comp ty2