keelung-0.9.2: src/Keelung.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Keelung is a DSL for building zero-knowledge proofs
module Keelung
( module Keelung.Syntax,
module Keelung.Field,
module Keelung.Heap,
module Keelung.Monad,
module Keelung.Data.Bits,
run,
compile,
compileO0,
compileO2,
compileWithOpts,
rtsoptProf,
rtsoptMemory,
generate,
verify,
genCircuit,
genWitness,
genInputs,
interpret_,
interpret,
gf181,
bn128,
b64,
elaborateAndEncode,
Encode,
GaloisField,
keelungVersion,
)
where
import Control.Monad.Except
import Data.ByteString.Char8 qualified as BS
import Data.Field.Galois (GaloisField)
import Data.List (intercalate)
import Data.Serialize (Serialize)
import Data.Serialize qualified as Serialize
import Data.String (IsString (fromString))
import Keelung.Constraint.R1CS (R1CS)
import Keelung.Data.Bits
import Keelung.Data.Struct (Struct (..))
import Keelung.Error
import Keelung.Field
import Keelung.Heap
import Keelung.Monad
import Keelung.Syntax
import Keelung.Syntax.Encode
import Keelung.Syntax.Encode.Syntax qualified as Encoding
import System.Directory qualified as Path
import System.IO.Error qualified as IO
import System.Info qualified
import System.Process qualified as Process
import Text.Read (readMaybe)
--------------------------------------------------------------------------------
-- | Compile a program to a 'R1CS' constraint system.
compile :: Encode t => FieldType -> Comp t -> IO (Either Error (R1CS Integer))
compile = compileWithOpts 1 [] []
-- | Compile a program to a 'R1CS' constraint system with optimization level 0.
compileO0 :: Encode t => FieldType -> Comp t -> IO (Either Error (R1CS Integer))
compileO0 = compileWithOpts 0 [] []
-- | Compile a program to a 'R1CS' constraint system with optimization level 2.
compileO2 :: Encode t => FieldType -> Comp t -> IO (Either Error (R1CS Integer))
compileO2 = compileWithOpts 2 [] []
-- | Compile a program to a 'R1CS' constraint system with optimization level and RTS options as arguments.
compileWithOpts :: Encode t => Int -> [String] -> [String] -> FieldType -> Comp t -> IO (Either Error (R1CS Integer))
compileWithOpts level opts rtsopts fieldType prog = runM $ do
elab <- liftEither (elaborateAndEncode prog)
let opts' = "protocol" : optOptimize level : opts <> ["+RTS"] <> rtsopts <> ["-RTS"]
case fieldType of
GF181 -> convertFieldElement (wrapper opts' (fieldType, elab) :: M (R1CS GF181))
BN128 -> convertFieldElement (wrapper opts' (fieldType, elab) :: M (R1CS BN128))
B64 -> convertFieldElement (wrapper opts' (fieldType, elab) :: M (R1CS B64))
where
optOptimize :: Int -> String
optOptimize i = "O" <> show i
-- | Default RTS options for profiling
rtsoptProf :: [String]
rtsoptProf = ["-p"]
-- | Helper function for compiling RTS options
-- Memory size in GB for RTS options -M, -H and in MB for -A
-- Try to increase if keelungc produces segmentation fault.
-- https://downloads.haskell.org/ghc/latest/docs/users_guide/runtime_control.html
rtsoptMemory :: Int -> Int -> Int -> [String]
rtsoptMemory m h a = ["-M" <> show m <> "G", "-H" <> show h <> "G", "-A" <> show a <> "M"]
--------------------------------------------------------------------------------
-- | Generate a proof
generate_ :: (Serialize n, Integral n, Encode t) => FieldType -> Comp t -> [n] -> [n] -> IO (Either Error (FilePath, String))
generate_ fieldType prog publicInput privateInput = runM $ do
(cmd, args) <- findAuroraProver
_ <- genCircuit fieldType prog
_ <- genWitness_ fieldType prog publicInput privateInput
proofPath <- lift $ Path.makeAbsolute "proof"
genParameters
genInputs publicInput
lift $ do
let arguments =
args
++ [ "--r1cs_filepath",
"circuit.jsonl",
"--input_filepath",
"witness.jsonl",
"--parameter_filepath",
"parameter.json",
"--output_filepath",
proofPath
]
msg <- Process.readProcess cmd arguments mempty
return (proofPath, msg)
-- | Generate a proof
generate :: Encode t => FieldType -> Comp t -> [Integer] -> [Integer] -> IO ()
generate fieldType prog publicInput privateInput = do
result <- generate_ fieldType prog publicInput privateInput
case result of
Left err -> print err
Right (_, msg) -> putStr msg
-- | Generate and verify a proof
verify_ :: IO (Either Error String)
verify_ = runM $ do
(cmd, args) <- findAuroraVerifier
genParameters
lift $ do
let arguments =
args
++ [ "--r1cs_filepath",
"circuit.jsonl",
"--input_filepath",
"witness.jsonl",
"--parameter_filepath",
"parameter.json",
"--proof_filepath",
"proof"
]
Process.readProcess cmd arguments mempty
-- | Verify a proof
verify :: IO ()
verify = do
result <- verify_
case result of
Left err -> print err
Right msg -> putStr msg
-- | Compile a program as R1CS and write it to circuit.jsonl.
genCircuit :: Encode t => FieldType -> Comp t -> M (R1CS Integer)
genCircuit fieldType prog = do
elab <- liftEither (elaborateAndEncode prog)
case fieldType of
GF181 -> convertFieldElement (wrapper ["protocol", "toJSON"] (fieldType, elab) :: M (R1CS GF181))
BN128 -> convertFieldElement (wrapper ["protocol", "toJSON"] (fieldType, elab) :: M (R1CS BN128))
B64 -> convertFieldElement (wrapper ["protocol", "toJSON"] (fieldType, elab) :: M (R1CS B64))
-- | Generate witnesses for a program with inputs and write them to witness.jsonl.
genWitness_ :: (Serialize n, Integral n, Encode t) => FieldType -> Comp t -> [n] -> [n] -> M [n]
genWitness_ fieldType prog publicInput privateInput = do
elab <- liftEither (elaborateAndEncode prog)
wrapper ["protocol", "genWitness"] (fieldType, elab, map toInteger publicInput, map toInteger privateInput)
-- | Generate parameters for a program and write them to parameter.json.
genParameters :: M ()
genParameters = lift $ BS.writeFile "parameter.json" "{\"security_level\": 128, \"heuristic_ldt_reducer_soundness\": true, \"heuristic_fri_soundness\": true, \"bcs_hash_type\": \"blake2b_type\", \"make_zk\": false, \"parallel\": true, \"field_size\": 181, \"is_multiplicative\": true}"
-- | For generating witness.jsonl
genWitness :: Encode t => FieldType -> Comp t -> [Integer] -> [Integer] -> IO [Integer]
genWitness fieldType prog publicInput privateInput = runM (genWitness_ fieldType prog publicInput privateInput) >>= printErrorInstead
-- | For generating inputs.jsonl
genInputs :: (Integral n) => [n] -> M ()
genInputs inputs = do
let inputs' = intercalate "," $ map ((\x -> "\"" ++ x ++ "\"") . show . toInteger) inputs
lift $ BS.writeFile "inputs.jsonl" $ fromString $ "{\"inputs\":[" ++ inputs' ++ "]}"
--------------------------------------------------------------------------------
-- | Interpret a program
interpret_ :: (Serialize n, Integral n, Encode t) => FieldType -> Comp t -> [n] -> [n] -> IO (Either Error [n])
interpret_ fieldType prog publicInput privateInput = runM $ do
elab <- liftEither (elaborateAndEncode prog)
wrapper ["protocol", "interpret"] (fieldType, elab, map toInteger publicInput, map toInteger privateInput)
printErrorInstead :: Show e => Either e [a] -> IO [a]
printErrorInstead (Left err) = do
print err
return []
printErrorInstead (Right values) = return values
-- | Interpret a program with public and private inputs
run :: Encode t => Comp t -> [Integer] -> [Integer] -> IO [Integer]
run prog publicInput privateInput = interpret_ GF181 prog publicInput privateInput >>= printErrorInstead
-- | Interpret a program with public and private inputs
interpret :: Encode t => FieldType -> Comp t -> [Integer] -> [Integer] -> IO [Integer]
interpret fieldType prog publicInput privateInput = interpret_ fieldType prog publicInput privateInput >>= printErrorInstead
-- | A specialized version of 'interpret' that outputs numbers as 'N GF181'
gf181 :: Encode t => Comp t -> [GF181] -> [GF181] -> IO [N GF181]
gf181 prog publicInput privateInput = map N <$> (interpret_ GF181 prog publicInput privateInput >>= printErrorInstead)
-- | A specialized version of 'interpret' that outputs numbers as 'N B64'
b64 :: Encode t => Comp t -> [B64] -> [B64] -> IO [N B64]
b64 prog publicInput privateInput = map N <$> (interpret_ B64 prog publicInput privateInput >>= printErrorInstead)
-- | A specialized version of 'interpret' that outputs numbers as 'N BN128'
bn128 :: Encode t => Comp t -> [BN128] -> [BN128] -> IO [N BN128]
bn128 prog publicInput privateInput = map N <$> (interpret_ BN128 prog publicInput privateInput >>= printErrorInstead)
--------------------------------------------------------------------------------
-- | Elaborate a program and encode it
elaborateAndEncode :: Encode t => Comp t -> Either Error Encoding.Elaborated
elaborateAndEncode prog = encodeElaborated <$> elaborate prog
where
encodeElaborated :: Encode t => Elaborated t -> Encoding.Elaborated
encodeElaborated (Elaborated expr comp) = runHeapM (compHeap comp) $ do
let Computation counters _addrSize _heap eb assertions divModRelsU = comp
in Encoding.Elaborated
<$> encode expr
<*> ( Encoding.Computation
counters
<$> (Struct <$> mapM encode' (structF eb) <*> mapM encode' (structB eb) <*> pure (structU eb))
<*> mapM encode assertions
<*> pure divModRelsU
)
--------------------------------------------------------------------------------
-- | Internal function for handling data serialization
wrapper :: (Serialize a, Serialize b) => [String] -> a -> M b
wrapper args' payload = do
(cmd, args) <- findKeelungc
version <- readKeelungVersion cmd args
checkCompilerVersion version
blob <- lift $ Process.readProcess cmd (args ++ args') (BS.unpack $ Serialize.encode payload)
let result = Serialize.decode (BS.pack blob)
case result of
Left err -> throwError (DecodeError err)
Right (Left err) -> throwError (CompileError err)
Right (Right x) -> return x
-- | Locate the Keelung compiler
-- 1. see if "keelungc" is in PATH
-- 2. if not, try to run "docker run banacorn/keelung"
-- Returns the command and arguments to run when found
findKeelungc :: M (String, [String])
findKeelungc = do
keelungcExists <- checkCmd "keelungc"
if keelungcExists
then return ("keelungc", [])
else do
dockerExists <- checkCmd "docker"
if dockerExists
then case System.Info.arch of
"x86_64" -> return ("docker", ["run", "-i", "btqag/keelungc"])
-- insert "--platform=linux/amd64" when we are not on a x86 machine
_ -> return ("docker", ["run", "-i", "--platform=linux/amd64", "btqag/keelungc"])
else throwError CannotLocateKeelungC
findAuroraProver :: M (String, [String])
findAuroraProver = do
auroraExists <- checkCmd "aurora_prove"
if auroraExists
then return ("aurora_prove", [])
else do
dockerExists <- checkCmd "docker"
-- let currentDirector = Path.getCurrentDirectory
if dockerExists
then case System.Info.arch of
"x86_64" -> return ("docker", ["run", "-i", "btqag/aurora-prove"])
-- insert "--platform=linux/amd64" when we are not on a x86 machine
_ -> return ("docker", ["run", "-i", "--platform=linux/amd64", "btqag/aurora-prove"])
else throwError CannotLocateProver
findAuroraVerifier :: M (String, [String])
findAuroraVerifier = do
auroraExists <- checkCmd "aurora_verify"
if auroraExists
then return ("aurora_verify", [])
else do
dockerExists <- checkCmd "docker"
if dockerExists
then case System.Info.arch of
"x86_64" -> return ("docker", ["run", "-i", "btqag/aurora-verify"])
-- insert "--platform=linux/amd64" when we are not on a x86 machine
_ -> return ("docker", ["run", "-i", "--platform=linux/amd64", "btqag/aurora-verify"])
else throwError CannotLocateVerifier
-- | Check the version of the Keelung compiler
readKeelungVersion :: FilePath -> [String] -> M (Int, Int, Int)
readKeelungVersion cmd args = do
-- trying to read version with `keelungc --version`
rawString <-
catchIOError
CannotReadVersionError
(Process.readProcess cmd (args ++ ["--version"]) mempty)
-- parse see if the version number is well-formed
let parseResult = case splitAt 9 rawString of
("Keelung v", versionString) -> parseVersion versionString
_ -> Nothing
-- throws CannotReadVersionError if it's not well-formed
case parseResult of
Nothing -> throwError CannotReadVersionError
Just x -> return x
where
parseVersion :: String -> Maybe (Int, Int, Int)
parseVersion versionString = do
(major, minor, patch) <- case span (/= '.') versionString of
(m, '.' : rest) -> case span (/= '.') rest of
(n, '.' : p) -> Just (m, n, p)
_ -> Nothing
_ -> Nothing
(,,) <$> readMaybe major <*> readMaybe minor <*> readMaybe patch
checkCompilerVersion :: (Int, Int, Int) -> M ()
checkCompilerVersion (major, minor, patch) = do
if major == 0 && minor >= 9 && minor < 10 && patch >= 0
then return ()
else throwError (VersionMismatchError major minor patch)
--------------------------------------------------------------------------------
-- | Check if a command exists
checkCmd :: String -> M Bool
checkCmd cmd =
lift $
IO.catchIOError
(Process.readProcess whichCmd [cmd] mempty >> return True)
(\_ -> return False)
where
-- decide the command for locating executables
whichCmd :: String
whichCmd = case System.Info.os of
"mingw32" -> "where" -- Windows uses "where"
_ -> "which" -- Unix uses "which"
--------------------------------------------------------------------------------
-- | String of Keelung version exposed to the user
keelungVersion :: String
keelungVersion = let (major, minor, patch) = keelungVersion_ in show major ++ "." ++ show minor ++ "." ++ show patch
where
-- | The version of Keelung is a triple of three numbers, we're not going full semver yet
keelungVersion_ :: (Int, Int, Int)
keelungVersion_ = (0, 9, 2)
--------------------------------------------------------------------------------
type M = ExceptT Error IO
runM :: M a -> IO (Either Error a)
runM = runExceptT
-- liftEitherT :: IO (Either Error a) -> M a
-- liftEitherT f = do
-- result <- lift f
-- case result of
-- Left err -> throwError err
-- Right x -> return x
-- | Handle 'IO' Exceptions in the 'M' Monad
catchIOError :: Error -> IO a -> M a
catchIOError err f = lift (IO.catchIOError (Right <$> f) (const (return (Left err)))) >>= liftEither
-- | Prettify and convert all field elements to 'Integer' in a 'R1CS'
convertFieldElement :: (GaloisField a, Integral a) => M (R1CS a) -> M (R1CS Integer)
convertFieldElement = fmap (fmap (toInteger . N))