{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
module Protocol.Election where

import Control.Monad (Monad(..), join, mapM, zipWithM)
import Control.Monad.Morph (MFunctor(..))
import Control.Monad.Trans.Class (MonadTrans(..))
import Data.Bool
import Data.Either (either)
import Data.Eq (Eq(..))
import Data.Foldable (Foldable, foldMap, and)
import Data.Function (($), id, const)
import Data.Functor (Functor, (<$>))
import Data.Functor.Identity (Identity(..))
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Ord (Ord(..))
import Data.Semigroup (Semigroup(..))
import Data.Text (Text)
import Data.Traversable (Traversable(..))
import Data.Tuple (fst, snd, uncurry)
import GHC.Natural (minusNaturalMaybe)
import Numeric.Natural (Natural)
import Prelude (fromIntegral)
import Text.Show (Show(..))
import qualified Control.Monad.Trans.Except as Exn
import qualified Control.Monad.Trans.State.Strict as S
import qualified Data.ByteString as BS
import qualified Data.List as List

import Protocol.Arithmetic
import Protocol.Credential

-- * Type 'Encryption'
-- | ElGamal-like encryption.
-- Its security relies on the /Discrete Logarithm problem/.
--
-- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
-- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
-- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
-- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
-- to enable the additive homomorphism.
--
-- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
-- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
data Encryption q = Encryption
 { encryption_nonce :: G q
   -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
   -- equal to @('groupGen' '^'encNonce)@
 , encryption_vault :: G q
   -- ^ Encrypted 'clear' text, equal to @('pubKey' '^'r '*' 'groupGen' '^'clear)@
 } deriving (Eq,Show)

-- | Additive homomorphism.
-- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
instance SubGroup q => Additive (Encryption q) where
        zero = Encryption one one
        x+y = Encryption
         (encryption_nonce x * encryption_nonce y)
         (encryption_vault x * encryption_vault y)

-- *** Type 'EncryptionNonce'
type EncryptionNonce = E

-- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
--
-- WARNING: the secret encryption nonce (@encNonce@)
-- is returned alongside the 'Encryption'
-- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
-- but this secret @encNonce@ MUST be forgotten after that,
-- as it may be used to decipher the 'Encryption'
-- without the secret key associated with 'pubKey'.
encrypt ::
 Monad m => RandomGen r => SubGroup q =>
 PublicKey q -> E q ->
 S.StateT r m (EncryptionNonce q, Encryption q)
encrypt pubKey clear = do
        encNonce <- random
        -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
        return $ (encNonce,)
                Encryption
                 { encryption_nonce = groupGen^encNonce
                 , encryption_vault = pubKey  ^encNonce * groupGen^clear
                 }

-- * Type 'Proof'
-- | 'Proof' of knowledge of a discrete logarithm:
-- @(secret == logBase base (base^secret))@.
data Proof q = Proof
 { proof_challenge :: Challenge q
   -- ^ 'Challenge' sent by the verifier to the prover
   -- to ensure that the prover really has knowledge
   -- of the secret and is not replaying.
   -- Actually, 'proof_challenge' is not sent to the prover,
   -- but derived from the prover's 'Commitment's and statements
   -- with a collision resistant 'hash'.
   -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
 , proof_response :: E q
   -- ^ A discrete logarithm sent by the prover to the verifier,
   -- as a response to 'proof_challenge'.
   --
   -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@
   -- where:
   --
   -- * @statement@ is a serialization of a tag, 'base' and 'basePowSec',
   -- * @(commitment '==' 'commit' proof base basePowSec '=='
   --   base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge')@,
   -- * and @(basePowSec '==' base'^'sec)@,
   --
   -- then, with overwhelming probability due to the 'hash' function:
   -- @(commitment '==' base'^'nonce)@.
   -- Therefore by expanding 'commitment':
   -- @('proof_response' '==' logBase base (base'^'nonce) '-' logBase basePowSec (basePowSec '^' 'proof_challenge'))@,
   -- which means that the prover must have known 'nonce' and 'sec'
   -- to compute 'proof_response' efficiently with:
   -- @('proof_response' '==' nonce '-' sec '*' 'proof_challenge')@,
   --
   -- The 'nonce' is introduced to ensure each 'prove' does not reveal
   -- any information regarding the prover's secret 'sec',
   -- by being randomly chosen by the prover.
 } deriving (Eq,Show)

-- ** Type 'ZKP'
-- | Zero-knowledge proof
--
-- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
--      A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
--
-- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
newtype ZKP = ZKP BS.ByteString

-- ** Type 'Challenge'
type Challenge = E

-- ** Type 'Oracle'
-- An 'Oracle' returns the 'Challenge' of the 'Commitment's
-- by 'hash'ing them (eventually with other 'Commitment's).
--
-- Used in 'prove' it enables a Fiat-Shamir transformation
-- of an /interactive zero-knowledge/ (IZK) proof
-- into a /non-interactive zero-knowledge/ (NIZK) proof.
-- That is to say that the verifier does not have
-- to send a 'Challenge' to the prover.
-- Indeed, the prover now handles the 'Challenge'
-- which becomes a (collision resistant) 'hash'
-- of the prover's commitments (and statements to be a stronger proof).
type Oracle list q = list (Commitment q) -> Challenge q

-- | @('prove' sec commitBases oracle)@
-- returns a 'Proof' that @sec@ is known.
--
-- The 'Oracle' is given the 'commitBases'
-- raised to the power of the secret nonce of the 'Proof',
-- as those are the 'commitBases' that the verifier will obtain
-- when composing the 'proof_challenge' and 'proof_response' together
-- (in 'commit').
--
-- NOTE: 'sec' is @secKey@ in 'signature_proof' or @encNonce@ in 'proveEncryption'.
--
-- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
-- the statement must be included in the 'hash' (not only the commitments).
--
-- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
-- does not reveal any information regarding the secret 'sec'.
prove ::
 Monad m => RandomGen r => SubGroup q => Functor list =>
 E q -> list (Commitment q) -> Oracle list q -> S.StateT r m (Proof q)
prove sec commitBases oracle = do
        nonce <- random
        let proof_challenge = oracle $ (^ nonce) <$> commitBases
        return Proof
         { proof_challenge
         , proof_response = nonce - sec*proof_challenge
         }

-- ** Type 'Commitment'
type Commitment = G

-- | @('commit' proof base basePowSec)@ returns a 'Commitment'
-- from the given 'Proof' with the knowledge of the verifier.
commit :: SubGroup q => Proof q -> G q -> G q -> Commitment q
commit Proof{..} base basePowSec =
        base^proof_response *
        basePowSec^proof_challenge
  -- NOTE: Contrary to some textbook presentations,
  -- @('*')@ is used instead of @('/')@ to avoid the performance cost
  -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
  -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
{-# INLINE commit #-}

-- * Type 'Disjunction'
-- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
-- it's used in 'proveEncryption' to generate a 'Proof'
-- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
type Disjunction = G

booleanDisjunctions :: SubGroup q => [Disjunction q]
booleanDisjunctions = List.take 2 groupGenInverses

intervalDisjunctions :: SubGroup q => Opinion q -> Opinion q -> [Disjunction q]
intervalDisjunctions mini maxi =
        List.genericTake (fromMaybe 0 $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
        List.genericDrop (nat mini) $
        groupGenInverses

-- ** Type 'Opinion'
-- | Index of a 'Disjunction' within a list of them.
-- It is encrypted as an 'E'xponent by 'encrypt'.
type Opinion = E

-- ** Type 'DisjProof'
-- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
-- is indexing a 'Disjunction' within a list of them,
-- without revealing which 'Opinion' it is.
newtype DisjProof q = DisjProof [Proof q]
 deriving (Eq,Show)

-- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
-- returns a 'DisjProof' that 'enc' 'encrypt's
-- the 'Disjunction's between 'prevDisjs' and 'nextDisjs'.
--
-- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
proveEncryption ::
 forall m r q.
 Monad m => RandomGen r => SubGroup q =>
 PublicKey q -> ZKP ->
 ([Disjunction q],[Disjunction q]) ->
 (EncryptionNonce q, Encryption q) ->
 S.StateT r m (DisjProof q)
proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
        -- Fake proofs for all values except the correct one.
        prevFakes <- fakeProof `mapM` prevDisjs
        nextFakes <- fakeProof `mapM` nextDisjs
        let prevProofs = fst <$> prevFakes
        let nextProofs = fst <$> nextFakes
        let challengeSum =
                sum (proof_challenge <$> prevProofs) +
                sum (proof_challenge <$> nextProofs)
        let statement = encryptionStatement voterZKP enc
        correctProof <- prove encNonce [groupGen, elecPubKey] $
         -- 'Oracle'
         \correctCommitments ->
                let commitments =
                        foldMap snd prevFakes <>
                        correctCommitments <>
                        foldMap snd nextFakes in
                hash statement commitments - challengeSum
        return $ DisjProof $ prevProofs <> (correctProof : nextProofs)
        where
        fakeProof :: Disjunction q -> S.StateT r m (Proof q, [Commitment q])
        fakeProof disj = do
                -- Returns 'Commitment's verifiables by the verifier,
                -- but computed from random 'proof_challenge' and 'proof_response'
                -- instead of correct ones.
                proof_challenge <- random
                proof_response  <- random
                let proof = Proof{..}
                return (proof, encryptionCommitments elecPubKey enc (disj, proof))

verifyEncryption ::
 Monad m =>
 SubGroup q =>
 PublicKey q -> ZKP ->
 [Disjunction q] ->
 (Encryption q, DisjProof q) ->
 Exn.ExceptT ErrorValidateEncryption m Bool
verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs)
 | List.length proofs /= List.length disjs =
        Exn.throwE $ ErrorValidateEncryption_InvalidProofLength
         (fromIntegral $ List.length proofs)
         (fromIntegral $ List.length disjs)
 | otherwise = return $ challengeSum == hash (encryptionStatement voterZKP enc) commitments
        where
        challengeSum = sum (proof_challenge <$> proofs)
        commitments = foldMap
         (encryptionCommitments elecPubKey enc)
         (List.zip disjs proofs)

-- ** Hashing
encryptionStatement :: SubGroup q => ZKP -> Encryption q -> BS.ByteString
encryptionStatement (ZKP voterZKP) Encryption{..} =
        "prove|"<>voterZKP<>"|"
         <> bytesNat encryption_nonce<>","
         <> bytesNat encryption_vault<>"|"
        -- NOTE: the commitment base 'elecPubKey' is notably absent here
        -- despite it being used in 'encryptionCommitments',
        -- maybe this is not necessary because it is already known
        -- by every participant.

-- | @('encryptionCommitments' elecPubKey enc (disj,proof))@
-- returns the 'Commitment's with only the knowledge of the verifier.
--
-- The 'Proof' comes from 'prove' of @fakeProof@ in 'proveEncryption'.
encryptionCommitments ::
 SubGroup q =>
 PublicKey q -> Encryption q ->
 (Disjunction q, Proof q) -> [G q]
encryptionCommitments elecPubKey Encryption{..} (disj, proof) =
        [ commit proof groupGen encryption_nonce
          -- == groupGen ^ nonce if 'Proof' comes from 'prove'
        , commit proof elecPubKey (encryption_vault*disj)
          -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
          -- and 'encryption_vault' encrypts (- logBase groupGen disj).
        ]

-- ** Type 'ErrorValidateEncryption'
-- | Error raised by 'verifyEncryption'.
data ErrorValidateEncryption
 =   ErrorValidateEncryption_InvalidProofLength Natural Natural
     -- ^ When the number of proofs is different than
     -- the number of 'Disjunction's.
 deriving (Eq,Show)

-- * Type 'Question'
data Question q = Question
 { question_text    :: Text
 , question_choices :: [Text]
 , question_mini    :: Opinion q
 , question_maxi    :: Opinion q
 -- , question_blank :: Maybe Bool
 } deriving (Eq, Show)

-- * Type 'Answer'
data Answer q = Answer
 { answer_opinions :: [(Encryption q, DisjProof q)]
   -- ^ Encrypted 'Opinion' for each 'question_choices'
   -- with a 'DisjProof' that they belong to [0,1].
 , answer_sumProof :: DisjProof q
   -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
   -- is an element of @[mini..maxi]@.
 -- , answer_blankProof ::
 } deriving (Eq,Show)

-- | @('encryptAnswer' elecPubKey zkp quest opinions)@
-- returns an 'Answer' validable by 'verifyAnswer',
-- unless an 'ErrorAnswer' is returned.
encryptAnswer ::
 Monad m => RandomGen r => SubGroup q =>
 PublicKey q -> ZKP ->
 Question q -> [Bool] ->
 S.StateT r (Exn.ExceptT ErrorAnswer m) (Answer q)
encryptAnswer elecPubKey zkp Question{..} opinionsBools
 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
        lift $ Exn.throwE $
                ErrorAnswer_WrongSumOfOpinions
                 (nat opinionsSum)
                 (nat question_mini)
                 (nat question_maxi)
 | List.length opinions /= List.length question_choices =
        lift $ Exn.throwE $
                ErrorAnswer_WrongNumberOfOpinions
                 (fromIntegral $ List.length opinions)
                 (fromIntegral $ List.length question_choices)
 | otherwise = do
        encryptions <- encrypt elecPubKey `mapM` opinions
        individualProofs <- zipWithM
         (\opinion -> proveEncryption elecPubKey zkp $
                if opinion
                then ([booleanDisjunctions List.!!0],[])
                else ([],[booleanDisjunctions List.!!1]))
         opinionsBools encryptions
        sumProof <- proveEncryption elecPubKey zkp
         ((List.tail <$>) $ List.genericSplitAt (nat (opinionsSum - question_mini)) $
                intervalDisjunctions question_mini question_maxi)
         ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
         , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
         )
        return $ Answer
         { answer_opinions = List.zip
                 (snd <$> encryptions) -- NOTE: drop encNonce
                 individualProofs
         , answer_sumProof = sumProof
         }
 where
        opinionsSum = sum opinions
        opinions = (\o -> if o then one else zero) <$> opinionsBools

verifyAnswer ::
 SubGroup q =>
 PublicKey q -> ZKP ->
 Question q -> Answer q -> Bool
verifyAnswer elecPubKey zkp Question{..} Answer{..}
 | List.length question_choices /= List.length answer_opinions = False
 | otherwise = either (const False) id $ Exn.runExcept $ do
        validOpinions <-
                verifyEncryption elecPubKey zkp booleanDisjunctions
                 `traverse` answer_opinions
        validSum <- verifyEncryption elecPubKey zkp
         (intervalDisjunctions question_mini question_maxi)
         ( sum (fst <$> answer_opinions)
         , answer_sumProof )
        return (and validOpinions && validSum)

-- ** Type 'ErrorAnswer'
-- | Error raised by 'encryptAnswer'.
data ErrorAnswer
 =   ErrorAnswer_WrongNumberOfOpinions Natural Natural
     -- ^ When the number of opinions is different than
     -- the number of choices ('question_choices').
 |   ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
     -- ^ When the sum of opinions is not within the bounds
     -- of 'question_mini' and 'question_maxi'.
 deriving (Eq,Show)

-- * Type 'Election'
data Election q = Election
 { election_name        :: Text
 , election_description :: Text
 , election_publicKey   :: PublicKey q
 , election_questions   :: [Question q]
 , election_uuid        :: UUID
 , election_hash        :: Hash -- TODO: serialize to JSON to calculate this
 } deriving (Eq,Show)

-- ** Type 'Hash'
newtype Hash = Hash Text
 deriving (Eq,Ord,Show)

-- * Type 'Ballot'
data Ballot q = Ballot
 { ballot_answers       :: [Answer q]
 , ballot_signature     :: Maybe (Signature q)
 , ballot_election_uuid :: UUID
 , ballot_election_hash :: Hash
 }

-- | @('encryptBallot' elec ('Just' secKey) opinionsByQuest)@
-- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
-- where 'opinionsByQuest' is a list of 'Opinion's
-- on each 'question_choices' of each 'election_questions'.
encryptBallot ::
 Monad m => RandomGen r => SubGroup q =>
 Election q -> Maybe (SecretKey q) -> [[Bool]] ->
 S.StateT r (Exn.ExceptT ErrorBallot m) (Ballot q)
encryptBallot Election{..} secKeyMay opinionsByQuest
 | List.length election_questions /= List.length opinionsByQuest =
        lift $ Exn.throwE $
                ErrorBallot_WrongNumberOfAnswers
                 (fromIntegral $ List.length opinionsByQuest)
                 (fromIntegral $ List.length election_questions)
 | otherwise = do
        let (voterKeys, voterZKP) =
                case secKeyMay of
                 Nothing -> (Nothing, ZKP "")
                 Just secKey ->
                        ( Just (secKey, pubKey)
                        , ZKP (bytesNat pubKey) )
                        where pubKey = publicKey secKey
        ballot_answers <-
                hoist (Exn.withExceptT ErrorBallot_Answer) $
                        zipWithM (encryptAnswer election_publicKey voterZKP)
                         election_questions opinionsByQuest
        ballot_signature <- case voterKeys of
         Nothing -> return Nothing
         Just (secKey, signature_publicKey) -> do
                signature_proof <-
                        prove secKey (Identity groupGen) $
                         \(Identity commitment) ->
                                hash
                                 -- NOTE: the order is unusual, the commitments are first
                                 -- then comes the statement. Best guess is that
                                 -- this is easier to code due to their respective types.
                                 (signatureCommitments voterZKP commitment)
                                 (signatureStatement ballot_answers)
                return $ Just Signature{..}
        return Ballot
         { ballot_answers
         , ballot_election_hash = election_hash
         , ballot_election_uuid = election_uuid
         , ballot_signature
         }

verifyBallot :: SubGroup q => Election q -> Ballot q -> Bool
verifyBallot Election{..} Ballot{..} =
        ballot_election_uuid == election_uuid &&
        ballot_election_hash == election_hash &&
        List.length election_questions == List.length ballot_answers &&
        let (isValidSign, zkpSign) =
                case ballot_signature of
                 Nothing -> (True, ZKP "")
                 Just Signature{..} ->
                        let zkp = ZKP (bytesNat signature_publicKey) in
                        (, zkp) $
                                proof_challenge signature_proof == hash
                                 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
                                 (signatureStatement ballot_answers)
        in
        and $ isValidSign :
                List.zipWith (verifyAnswer election_publicKey zkpSign)
                 election_questions ballot_answers

-- ** Type 'Signature'
-- | Schnorr-like signature.
--
-- Used by each voter to sign his/her encrypted 'Ballot'
-- using his/her 'Credential',
-- in order to avoid ballot stuffing.
data Signature q = Signature
 { signature_publicKey :: PublicKey q
   -- ^ Verification key.
 , signature_proof     :: Proof q
 }

-- *** Hashing

-- | @('signatureStatement' answers)@
-- returns the encrypted material to be signed:
-- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
signatureStatement :: Foldable f => SubGroup q => f (Answer q) -> [G q]
signatureStatement =
        foldMap $ \Answer{..} ->
                (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
                        [encryption_nonce, encryption_vault]

-- | @('signatureCommitments' voterZKP commitment)@
signatureCommitments :: SubGroup q => ZKP -> Commitment q -> BS.ByteString
signatureCommitments (ZKP voterZKP) commitment =
        "sig|"<>voterZKP<>"|"<>bytesNat commitment<>"|"

-- ** Type 'ErrorBallot'
-- | Error raised by 'encryptBallot'.
data ErrorBallot
 =   ErrorBallot_WrongNumberOfAnswers Natural Natural
     -- ^ When the number of answers
     -- is different than the number of questions.
 |   ErrorBallot_Answer ErrorAnswer
     -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
 deriving (Eq,Show)

-- * Type 'DecryptionShare'
-- | A decryption share. It is computed by a trustee from his/her
-- private key share and the encrypted tally,
-- and contains a cryptographic 'Proof' that he/she didn't cheat.
data DecryptionShare q = DecryptionShare
 { decryptionShare_factors :: [[DecryptionFactor q]]
 , decryptionShare_proofs  :: [[Proof q]]
   -- ^ 'Proof's that 'decryptionShare_factors' were correctly computed.
 } deriving (Eq,Show)

computeDecryptionShare ::
 Monad m => SubGroup q => RandomGen r =>
 SecretKey q -> [[Encryption q]] -> S.StateT r m (DecryptionShare q)
computeDecryptionShare secKey encs = do
        res <- mapM (mapM (decryptionFactor secKey)) encs
        return $ uncurry DecryptionShare $ List.unzip (List.unzip <$> res)

decryptionFactor ::
 Monad m => SubGroup q => RandomGen r =>
 SecretKey q -> Encryption q -> S.StateT r m (DecryptionFactor q, Proof q)
decryptionFactor secKey Encryption{..} = do
        proof <- prove secKey [groupGen, encryption_nonce] (hash zkp)
        return (encryption_nonce^secKey, proof)
        where zkp = decryptionStatement (publicKey secKey)

decryptionStatement :: SubGroup q => PublicKey q -> BS.ByteString
decryptionStatement pubKey =
        "decrypt|"<>bytesNat pubKey<>"|"

-- ** Type 'DecryptionFactor'
type DecryptionFactor = G

-- ** Type 'ErrorDecryptionShare'
data ErrorDecryptionShare
 =   ErrorDecryptionShare_Invalid
 deriving (Eq,Show)

-- | @('checkDecryptionShare' encTally pubKey decShare)@
-- checks that 'decShare'
-- (supposedly submitted by a trustee whose public key is 'pubKey')
-- is valid with respect to the encrypted tally 'encTally'.
checkDecryptionShare ::
 Monad m => SubGroup q => RandomGen r =>
 [[Encryption q]] -> PublicKey q -> DecryptionShare q ->
 Exn.ExceptT ErrorDecryptionShare m Bool
checkDecryptionShare encTally pubKey DecryptionShare{..}
 | len <- List.length encTally
 , len == List.length decryptionShare_factors
 , len == List.length decryptionShare_proofs =
        Exn.throwE ErrorDecryptionShare_Invalid
 | otherwise =
        return $ and $ join $ List.zipWith3 (List.zipWith3
         (\encFactor proof Encryption{..} ->
                hash zkp
                 [ commit proof groupGen pubKey
                 , commit proof encryption_nonce encFactor
                 ] == proof_challenge proof
         )) decryptionShare_factors decryptionShare_proofs encTally
        where zkp = decryptionStatement pubKey

{-
computeElectionResult ::
 Natural ->
 [Encryption q] ->
 [DecryptionShare q] ->
 ElectionResult q
computeElectionResult numBallots encTally decShares
-}