packages feed

MultiChor-1.0.0.0: examples/Lottery.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

module Lottery where

import CLI
import Choreography
import Choreography.Network.Http
import Control.Exception (Exception, throwIO)
import Control.Monad (unless)
import Control.Monad.Cont (MonadIO, liftIO)
import Crypto.Hash (Digest)
import Crypto.Hash qualified as Crypto
import Data (TestArgs, reference)
import Data.Bifunctor (first)
import Data.Binary qualified as Binary
import Data.ByteString (toStrict)
import Data.FiniteField (primeField)
import Data.Foldable (toList)
import GHC.TypeLits (KnownSymbol)
import System.Environment (getArgs)
import System.Random (Random, random, randomIO, randomR, randomRIO)
import Test.QuickCheck (Arbitrary, arbitrary, arbitraryBoundedEnum)

-- | Arbitrary "large" prime
newtype Fp = Fp $(primeField 999983) -- AFAICT the hint warning needs a newer version of hlint to go away:
  deriving (Bounded, Enum, Eq, Ord, Num) -- https://github.com/ndmitchell/hlint/issues/1226

instance Read Fp where
  readsPrec _ s = [(Fp (read s), "")]

instance Show Fp where
  show (Fp x) = show x

instance Random Fp where
  random g = toEnum `first` randomR (fromEnum @Fp minBound, fromEnum @Fp maxBound) g
  randomR (l, h) g = toEnum `first` randomR (fromEnum l, fromEnum h) g

instance Arbitrary Fp where
  arbitrary = arbitraryBoundedEnum

data LotteryError = CommitmentCheckFailed deriving (Show)

instance Exception LotteryError

data Args = Args
  { secrets :: (Fp, Fp, Fp, Fp, Fp), -- we lock our test to the case of five clients and three servers
    randomIs :: (Int, Int, Int)
  }
  deriving (Eq, Show, Read)

instance TestArgs Args Fp where
  reference Args {secrets = (c1, c2, c3, c4, c5), randomIs = (s1, s2, s3)} =
    let i = (s1 + s2 + s3) `mod` 5
     in [c1, c2, c3, c4, c5] !! i

instance Arbitrary Args where
  arbitrary =
    Args
      <$> ((,,,,) <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary)
      <*> ((,,) <$> arbitrary <*> arbitrary <*> arbitrary)

-- | Federated Lottery example from DPrio https://www.semanticscholar.org/paper/DPrio%3A-Efficient-Differential-Privacy-with-High-for-Keeler-Komlo/ae1b2a4e5beaaa850183ad37e0880bb70ae34f4e
lottery ::
  forall clients servers analyst census m _serv1 _serv2 _servTail _client1 _client2 _clientTail.
  ( KnownSymbols clients,
    KnownSymbols servers,
    KnownSymbol analyst,
    MonadIO m,
    (_serv1 ': _serv2 ': _servTail) ~ servers, -- There must be at least be two servers
    (_client1 ': _client2 ': _clientTail) ~ clients -- There must be at least be two clients
  ) =>
  Subset clients census ->
  Subset servers census ->
  Member analyst census ->
  Choreo census (CLI m) ()
lottery clients servers analyst = do
  secret <- _parallel clients (getInput @Fp "secret:")
  clientShares <-
    clients `parallel` \client un ->
      ( case tySpine @servers of
          TyCons -> do
            -- I guess this explains to GHC that we have KnownSymbols _servTail? IDK
            freeShares <- liftIO $ sequence $ pure $ randomIO @Fp
            return $ (viewFacet un client secret - sum freeShares) `qCons` freeShares
      )
  serverShares <-
    fanOut
      ( \server ->
          -- Probably I've already got a nicer way to write this on hand; idk.
          fanIn
            (inSuper servers server @@ nobody)
            ( \client -> do
                serverShare <-
                  inSuper clients client `locally` \un ->
                    pure $ viewFacet un client clientShares `getLeaf` server
                (inSuper clients client, serverShare) ~> inSuper servers server @@ nobody
            )
      )
  -- 1) Each server selects a random number; τ is some multiple of the number of clients.
  ρ <- _parallel servers (getInput "A random number from 1 to τ:")
  -- Salt value
  ψ <- _parallel servers (randomRIO (2 ^ (18 :: Int), 2 ^ (20 :: Int)))
  -- 2) Each server computes and publishes the hash α = H(ρ, ψ) to serve as a commitment
  α <- parallel servers \server un -> pure $ hash (viewFacet un server ψ) (viewFacet un server ρ)
  α' <- gather servers servers α
  -- 3) Every server opens their commitments by publishing their ψ and ρ to each other
  ψ' <- gather servers servers ψ
  ρ' <- gather servers servers ρ
  -- 4) All servers verify each other's commitment by checking α = H(ρ, ψ)
  parallel_
    servers
    ( \server un ->
        unless
          (un server α' == (hash <$> un server ψ' <*> un server ρ'))
          (liftIO $ throwIO CommitmentCheckFailed)
    )
  -- 5) If all the checks are successful, then sum random values to get the random index.
  ω <- servers `congruently` (\un -> sum (un refl ρ') `mod` length (toLocs clients))
  chosenShares <- servers `parallel` (\server un -> pure $ toList (viewFacet un server serverShares) !! un server ω)
  -- Servers forward shares to an analyist.
  allShares <- gather servers (analyst @@ nobody) chosenShares
  analyst `locally_` \un -> putOutput "The answer is:" $ sum $ un singleton allShares
  where
    hash :: Int -> Int -> Digest Crypto.SHA256
    hash ρ ψ = Crypto.hash $ toStrict (Binary.encode ρ <> Binary.encode ψ)

main :: IO ()
main = do
  [loc] <- getArgs
  let clientProof :: Subset '["client1", "client2"] '["client1", "client2", "server1", "server2", "analyst"]
      clientProof = explicitSubset
      serverProof :: Subset '["server1", "server2"] '["client1", "client2", "server1", "server2", "analyst"]
      serverProof = explicitSubset
      analystProof :: Member "analyst" '["client1", "client2", "server1", "server2", "analyst"]
      analystProof = explicitMember
  _ <- case loc of
    "client1" -> runCLIIO $ runChoreography config (lottery clientProof serverProof analystProof) "client1"
    "client2" -> runCLIIO $ runChoreography config (lottery clientProof serverProof analystProof) "client2"
    "server1" -> runCLIIO $ runChoreography config (lottery clientProof serverProof analystProof) "server1"
    "server2" -> runCLIIO $ runChoreography config (lottery clientProof serverProof analystProof) "server2"
    "analyst" -> runCLIIO $ runChoreography config (lottery clientProof serverProof analystProof) "analyst"
    _ -> error "unknown party"
  return ()
  where
    config =
      mkHttpConfig
        [ ("client1", ("localhost", 5000)),
          ("client2", ("localhost", 5001)),
          ("server1", ("localhost", 5002)),
          ("server2", ("localhost", 5003)),
          ("analyst", ("localhost", 5004))
        ]