Initial commit
This commit is contained in:
commit
482967b019
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Revision history for iohk-challenge
|
||||||
|
|
||||||
|
## 0.1.0.0 -- YYYY-mm-dd
|
||||||
|
|
||||||
|
* First version. Released on an unsuspecting world.
|
310
Tasks.hs
Normal file
310
Tasks.hs
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
{-# OPTIONS_HADDOCK show-extensions #-}
|
||||||
|
{-# OPTIONS_GHC -Wno-dodgy-exports #-}
|
||||||
|
{-# OPTIONS_GHC -Wno-missing-methods #-}
|
||||||
|
{-# OPTIONS_GHC -Wno-unused-imports #-}
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Module : HDT.Tasks
|
||||||
|
Description : IOHK Haskell Developer Test
|
||||||
|
Copyright : (c) Lars Brünjes, 2020
|
||||||
|
Maintainer : lars.bruenjes@iohk.io
|
||||||
|
Stability : experimental
|
||||||
|
Portability : portable
|
||||||
|
|
||||||
|
This module contains the IOHK Haskell Developer Test. Please complete all the
|
||||||
|
to-do's!
|
||||||
|
-}
|
||||||
|
module HDT.Tasks
|
||||||
|
( -- * @Agent@s
|
||||||
|
-- | We start by implementing a free monad,
|
||||||
|
-- which we will use to model a concurrent agent than
|
||||||
|
-- can communicate and coordinate with other agents by
|
||||||
|
-- exchanging broadcast messages.
|
||||||
|
Agent (..)
|
||||||
|
, delay
|
||||||
|
, broadcast
|
||||||
|
, receive
|
||||||
|
-- * Ping-pong example
|
||||||
|
-- |As an example of using agents, agents @'ping'@ and @'pong'@ defined below
|
||||||
|
-- keep sending @'Ping'@ and @'Pong'@ messages back and forth between themselves.
|
||||||
|
, PingPongMessage (..)
|
||||||
|
, ping
|
||||||
|
, pong
|
||||||
|
-- * An @'IO'@-interpreter for agents.
|
||||||
|
-- |In order to actually /run/ agents, we need to /interpret/ the free
|
||||||
|
-- @'Agent'@ monad. We start with an interpreter in @'IO'@,
|
||||||
|
-- which runs each agent in a list of agents in its own thread
|
||||||
|
-- and implements messaging by utilizing a shared @'TChan'@ broadcast channel.
|
||||||
|
--
|
||||||
|
-- We will then be able to run our ping-pong example:
|
||||||
|
--
|
||||||
|
-- >>> runIO [ping, pong]
|
||||||
|
-- Ping
|
||||||
|
-- Pong
|
||||||
|
-- Ping
|
||||||
|
-- Pong
|
||||||
|
-- Ping
|
||||||
|
-- ...
|
||||||
|
, runIO
|
||||||
|
-- * Ouroboros Bft
|
||||||
|
-- | We use agents to implement a simplified version of the
|
||||||
|
-- <https://iohk.io/en/research/library/papers/ouroboros-bfta-simple-byzantine-fault-tolerant-consensus-protocol/ Ouroboros-BFT>
|
||||||
|
-- blockchain consensus protocol.
|
||||||
|
--
|
||||||
|
-- Ouroboros-BFT works as follows: A fixed number of @n@ /nodes/ participate in the
|
||||||
|
-- protocol. They collaborate on building a /blockchain/ by adding /blocks/,
|
||||||
|
-- and the protocol ensures that the nodes will agree on a /common prefix/
|
||||||
|
-- (all nodes agree on the prefix of the chain, but may disagree on a few blocks
|
||||||
|
-- towards the end).
|
||||||
|
-- This will work as long as at least two thirds of the nodes follow the protocol.
|
||||||
|
--
|
||||||
|
-- Time is divided in /slots/ of a fixed length, and in each slot, one node is the
|
||||||
|
-- /slot leader/ with the right to create the next block.
|
||||||
|
-- Slots leaders are determined in a round-robin fashion: Node 0
|
||||||
|
-- can create a block in Slot 0, Node 1 in Slot 1, Node @(n-1)@ in Slot @(n-1)@,
|
||||||
|
-- Node 0 in Slot @n@ and so on.
|
||||||
|
-- When a node is slot leader, it adds a block to its current chain and
|
||||||
|
-- broadcasts the new chain to the other nodes.
|
||||||
|
--
|
||||||
|
-- Each node holds on to a current chain (all nodes start with the chain
|
||||||
|
-- just consisting of the /genesis block/). When a node receives a new chain
|
||||||
|
-- from another node, it checks the new chain for /validity/ and
|
||||||
|
-- adopts it as its own chain /if it is longer than its own chain/.
|
||||||
|
--
|
||||||
|
-- A chain is /valid/ if
|
||||||
|
--
|
||||||
|
-- * The timestamps are stricly increasing,
|
||||||
|
-- * All blocks have been created by the slot leader of that slot and
|
||||||
|
-- * The newest block's timestamp is not from the future.
|
||||||
|
--
|
||||||
|
-- In reality, nodes would use digital signatures to sign the blocks
|
||||||
|
-- they create, and each block would contain a /payload/, but we
|
||||||
|
-- want to keep matters as simple as possible.
|
||||||
|
--
|
||||||
|
-- >>> runIO $ clock : [node 3 nid | nid <- [0,1,2]]
|
||||||
|
-- Time 0
|
||||||
|
-- Time 1
|
||||||
|
-- NewChain (Genesis :> {1 1})
|
||||||
|
-- Time 2
|
||||||
|
-- NewChain (Genesis :> {1 1} :> {2 2})
|
||||||
|
-- Time 3
|
||||||
|
-- NewChain (Genesis :> {1 1} :> {2 2} :> {3 0})
|
||||||
|
-- Time 4
|
||||||
|
-- NewChain (Genesis :> {1 1} :> {2 2} :> {3 0} :> {4 1})
|
||||||
|
-- Time 5
|
||||||
|
-- NewChain (Genesis :> {1 1} :> {2 2} :> {3 0} :> {4 1} :> {5 2})
|
||||||
|
-- Time 6
|
||||||
|
-- NewChain (Genesis :> {1 1} :> {2 2} :> {3 0} :> {4 1} :> {5 2} :> {6 0})
|
||||||
|
-- ...
|
||||||
|
, Slot
|
||||||
|
, NodeId
|
||||||
|
, Block (..)
|
||||||
|
, Chain (..)
|
||||||
|
, chainLength
|
||||||
|
, slotLeader
|
||||||
|
, chainValid
|
||||||
|
, clock
|
||||||
|
, node
|
||||||
|
-- * A /pure/ interpreter for agents.
|
||||||
|
-- |We also want to be able to interpret a list of agents in a
|
||||||
|
-- /pure and deterministic/ fashion.
|
||||||
|
--
|
||||||
|
-- When we try this with our ping-pong example, we will be able to do:
|
||||||
|
--
|
||||||
|
-- >>> take 5 $ runPure [ping, pong]
|
||||||
|
-- [(1,Ping),(2,Pong),(3,Ping),(4,Pong),(5,Ping)]
|
||||||
|
, runPure
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Concurrent.STM
|
||||||
|
import Numeric.Natural (Natural)
|
||||||
|
import Text.Printf (printf)
|
||||||
|
|
||||||
|
-- |An @'Agent' msg a@ is an abstract process that can send and receive broadcast
|
||||||
|
-- messages of type @msg@ and will eventually return a result of type @a@.
|
||||||
|
-- Define the type such that @'Agent' msg@ is a /free monad/ supporting the operations
|
||||||
|
-- @'delay'@, @'broadcast'@ and @'receive'@ below.
|
||||||
|
--
|
||||||
|
-- Agents can be used to model concurrent agents that communicate and coordinate
|
||||||
|
-- via message exchange over a broadcast channel.
|
||||||
|
data Agent msg a
|
||||||
|
|
||||||
|
-- |__TODO:__ Provide a @'Functor'@ instance for @'Agent' msg@.
|
||||||
|
instance Functor (Agent msg) where
|
||||||
|
|
||||||
|
-- |__TODO:__ Provide an @'Applicative'@ instance for @'Agent' msg@.
|
||||||
|
instance Applicative (Agent msg) where
|
||||||
|
|
||||||
|
-- |__TODO:__ Provide a @'Monad'@ instance for @'Agent' msg@.
|
||||||
|
-- The resulting monad should be /free/ and support operations
|
||||||
|
-- @'delay'@, @'broadcast'@ and @'receive'@ described below.
|
||||||
|
instance Monad (Agent msg) where
|
||||||
|
|
||||||
|
-- |Delay for one timestep.
|
||||||
|
-- __TODO:__ Implement @'delay'@.
|
||||||
|
delay :: Agent msg ()
|
||||||
|
delay = error "TODO: implement delay"
|
||||||
|
|
||||||
|
-- |Broadcast a message.
|
||||||
|
-- __TODO:__ Implement @'broadcast'@.
|
||||||
|
broadcast :: msg -- ^The message to broadcast.
|
||||||
|
-> Agent msg ()
|
||||||
|
broadcast = error "TODO: implement broadcast"
|
||||||
|
|
||||||
|
-- |Wait for a broadcast and return the received message.
|
||||||
|
-- __TODO:__ Implement @'receive'@.
|
||||||
|
receive :: Agent msg msg
|
||||||
|
receive = error "TODO: implement receive"
|
||||||
|
|
||||||
|
-- |The message type used by agents @'ping'@ and @'pong'@.
|
||||||
|
data PingPongMessage =
|
||||||
|
Ping -- ^Message used by the @'ping'@ agent, which the @'pong'@ agent waits for.
|
||||||
|
| Pong -- ^Message used by the @'pong'@ agent, which the @'ping'@ agent waits for.
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
-- |Agent @'ping'@ starts by broadcasting a @'Ping'@ message, then
|
||||||
|
-- waits for a @'Pong'@ message and repeats.
|
||||||
|
-- Note how it guards against the possibility of receiving its own broadcasts!
|
||||||
|
ping :: Agent PingPongMessage ()
|
||||||
|
ping = delay >> broadcast Ping >> go
|
||||||
|
where
|
||||||
|
go = do
|
||||||
|
msg <- receive
|
||||||
|
case msg of
|
||||||
|
Ping -> go
|
||||||
|
Pong -> ping
|
||||||
|
|
||||||
|
-- |Agent @'pong'@ waits for a @'Ping'@ message, the broadcasts a @'Pong'@ message
|
||||||
|
-- and repeats.
|
||||||
|
pong :: Agent PingPongMessage ()
|
||||||
|
pong = do
|
||||||
|
msg <- receive
|
||||||
|
case msg of
|
||||||
|
Ping -> delay >> broadcast Pong >> pong
|
||||||
|
Pong -> pong
|
||||||
|
|
||||||
|
-- |Function @'runIO' agents@ runs each agent in the given list
|
||||||
|
-- concurrently in the @'IO'@-monad.
|
||||||
|
-- Broadcast should be realized using a @'TChan' msg@ which is shared amongst
|
||||||
|
-- the threads running each agent,
|
||||||
|
-- and @'runIO'@ should only return if and when every agent has returned.
|
||||||
|
--
|
||||||
|
-- The operations should be interpreted as follows:
|
||||||
|
--
|
||||||
|
-- * @'delay'@ should delay execution for one second.
|
||||||
|
-- * @'broadcast' msg@ should broadcast @msg@ via the shared @'TChan'@
|
||||||
|
-- and additionally log the message to the console. Doing this naively
|
||||||
|
-- could lead to garbled output, so care must be taken to ensure sequential access
|
||||||
|
-- to the console.
|
||||||
|
-- * @'receive'@ should block the thread until a message is received on the
|
||||||
|
-- shared @'TChan'@.
|
||||||
|
--
|
||||||
|
-- __TODO:__ Implement @'runIO'@.
|
||||||
|
runIO :: Show msg
|
||||||
|
=> [Agent msg ()] -- ^The agents to run concurrently.
|
||||||
|
-> IO ()
|
||||||
|
runIO = error "TODO: implement runIO"
|
||||||
|
|
||||||
|
-- |Time is divided into @'Slot'@s.
|
||||||
|
type Slot = Natural
|
||||||
|
|
||||||
|
-- |Each node has a @'NodeId'@.
|
||||||
|
-- If there are @n@ nodes, their id's will be 0, 1, 2,...,@(n-1)@.
|
||||||
|
type NodeId = Natural
|
||||||
|
|
||||||
|
-- |The blockchain is built from @'Block'@s.
|
||||||
|
data Block = Block
|
||||||
|
{ slot :: Slot -- ^Timestamp indicating when the block was created.
|
||||||
|
, creator :: NodeId -- ^Identifies the node that created the block.
|
||||||
|
}
|
||||||
|
|
||||||
|
instance Show Block where
|
||||||
|
show b = printf "{%d %d}" (slot b) (creator b)
|
||||||
|
|
||||||
|
infixl 5 :>
|
||||||
|
|
||||||
|
-- |A blockchain can either be empty (just the genesis block) or contain @'Block'@s.
|
||||||
|
data Chain =
|
||||||
|
Genesis
|
||||||
|
| Chain :> Block
|
||||||
|
|
||||||
|
instance Show Chain where
|
||||||
|
showsPrec _ Genesis = showString "Genesis"
|
||||||
|
showsPrec d (c :> b) = showParen (d > 10) $ showsPrec 0 c . showString " :> " . showString (show b)
|
||||||
|
|
||||||
|
-- |Computes the length of a @'Chain'@. __TODO:__ Implement @'chainLength'@.
|
||||||
|
--
|
||||||
|
-- >>> chainLength Genesis
|
||||||
|
-- 0
|
||||||
|
-- >>> chainLength $ Genesis :> Block 2 2 :> Block 3 0
|
||||||
|
-- 2
|
||||||
|
chainLength :: Chain -> Int
|
||||||
|
chainLength = error "TODO: implement chainLength"
|
||||||
|
|
||||||
|
-- |Computes the slot leader. __TODO:__ Implement @'slotLeader'@.
|
||||||
|
--
|
||||||
|
-- >>> slotLeader 3 0
|
||||||
|
-- 0
|
||||||
|
-- >>> slotLeader 3 1
|
||||||
|
-- 1
|
||||||
|
-- >>> slotLeader 3 3
|
||||||
|
-- 0
|
||||||
|
slotLeader :: Int -- ^Total number of nodes.
|
||||||
|
-> Slot -- ^The @'Slot'@.
|
||||||
|
-> NodeId -- ^Identifies the node that has the right to create a block
|
||||||
|
-- in the given @'Slot'@.
|
||||||
|
slotLeader = error "TODO: implement slotLeader"
|
||||||
|
|
||||||
|
-- |Determines whether a chain is valid. __TODO:__ Implement @'chainValid'@.
|
||||||
|
--
|
||||||
|
-- >>> chainValid 3 4 $ Genesis :> Block 10 1
|
||||||
|
-- False
|
||||||
|
-- >>> chainValid 3 14 $ Genesis :> Block 10 1
|
||||||
|
-- True
|
||||||
|
-- >>> chainValid 3 14 $ Genesis :> Block 10 2
|
||||||
|
-- False
|
||||||
|
-- >>> chainValid 3 14 $ Genesis :> Block 3 1 :> Block 10 1
|
||||||
|
-- False
|
||||||
|
-- >>> chainValid 3 14 $ Genesis :> Block 3 0 :> Block 10 1
|
||||||
|
-- True
|
||||||
|
chainValid :: Int -- ^Total number of nodes.
|
||||||
|
-> Slot -- ^Current slot.
|
||||||
|
-> Chain -- ^Chain to validate.
|
||||||
|
-> Bool
|
||||||
|
chainValid = error "TODO: implement chainValid"
|
||||||
|
|
||||||
|
-- |The type of messages used for communication in the BFT-protocol.
|
||||||
|
data BftMessage =
|
||||||
|
Time Slot -- ^Message used by the @'clock'@ to broadcast the current time.
|
||||||
|
| NewChain Chain -- ^Message used by a @'node'@ to announce a new @'Chain'@.
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
-- |The nodes do not keep track of time by themselves, but instead rely on
|
||||||
|
-- the @'clock'@ agent, which broadcasts the beginning of each new @'Slot'@
|
||||||
|
-- using @'Time'@-messages. The agent should start with @'Slot' 0@ and run forever.
|
||||||
|
-- __TODO:__ Implement @'clock'@.
|
||||||
|
clock :: Agent BftMessage a
|
||||||
|
clock = error "TODO: implement clock"
|
||||||
|
|
||||||
|
-- |A @'node'@ participating in the BFT-protocol. It should start with the @'Genesis'@
|
||||||
|
-- chain at @'Slot' 0@ and run forever.
|
||||||
|
-- __TODO:__ Implement @'node'@.
|
||||||
|
node :: Int -- ^Total number of nodes.
|
||||||
|
-> NodeId -- ^Identifier of /this/ node.
|
||||||
|
-> Agent BftMessage a
|
||||||
|
node = error "TODO: implement node"
|
||||||
|
|
||||||
|
-- |Interprets a list of agents in a /pure/ fashion,
|
||||||
|
-- returning the list of all broadcasts (with their timestamps).
|
||||||
|
--
|
||||||
|
-- __Hint:__ It might be helpful to keep track of
|
||||||
|
--
|
||||||
|
-- * all active agents,
|
||||||
|
-- * all delayed agents and
|
||||||
|
-- * all agents waiting for a broadcast.
|
||||||
|
--
|
||||||
|
-- __TODO:__ Implement @'runPure'@.
|
||||||
|
runPure :: [Agent msg ()] -- ^The agents to run.
|
||||||
|
-> [(Natural, msg)] -- ^A list of all broadcasts, represented by
|
||||||
|
-- pairs containing a timestamp and the message that was sent.
|
||||||
|
runPure = error "TODO: implement runPure"
|
56
iohk-challenge.cabal
Normal file
56
iohk-challenge.cabal
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
cabal-version: 3.0
|
||||||
|
name: iohk-challenge
|
||||||
|
version: 0.1.0.0
|
||||||
|
-- synopsis:
|
||||||
|
-- description:
|
||||||
|
-- bug-reports:
|
||||||
|
license: LGPL-3.0-only
|
||||||
|
author: Julian Ospald
|
||||||
|
maintainer: hasufell@posteo.de
|
||||||
|
-- copyright:
|
||||||
|
-- category:
|
||||||
|
extra-source-files: CHANGELOG.md
|
||||||
|
|
||||||
|
common async
|
||||||
|
build-depends: async ^>= 2.2.2
|
||||||
|
|
||||||
|
common base
|
||||||
|
build-depends: base ^>= 4.13.0.0
|
||||||
|
|
||||||
|
common containers
|
||||||
|
buid-depends: containers ^>= 0.6.2.1
|
||||||
|
|
||||||
|
common doctest
|
||||||
|
build-depends: doctest ^>= 0.17
|
||||||
|
|
||||||
|
common stm
|
||||||
|
build-depends: stm ^>= 2.5.0.0
|
||||||
|
|
||||||
|
common config
|
||||||
|
default-language: Haskell2010
|
||||||
|
ghc-options:
|
||||||
|
-Wall -fwarn-tabs -fwarn-incomplete-uni-patterns
|
||||||
|
-fwarn-incomplete-record-updates -threaded
|
||||||
|
default-extensions:
|
||||||
|
LambdaCase
|
||||||
|
MultiWayIf
|
||||||
|
RecordWildCards
|
||||||
|
ScopedTypeVariables
|
||||||
|
TupleSections
|
||||||
|
|
||||||
|
library
|
||||||
|
import: config,
|
||||||
|
async,
|
||||||
|
base,
|
||||||
|
containers,
|
||||||
|
stm
|
||||||
|
exposed-modules: HDT.Tasks
|
||||||
|
hs-source-dirs: lib
|
||||||
|
|
||||||
|
test-suite doctests
|
||||||
|
import: config,
|
||||||
|
async,
|
||||||
|
base,
|
||||||
|
doctest
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
main-is: test/doctest.hs
|
443
lib/HDT/Tasks.hs
Normal file
443
lib/HDT/Tasks.hs
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
{-# OPTIONS_HADDOCK show-extensions #-}
|
||||||
|
{-# OPTIONS_GHC -Wno-dodgy-exports #-}
|
||||||
|
{-# OPTIONS_GHC -Wno-missing-methods #-}
|
||||||
|
{-# OPTIONS_GHC -Wno-unused-imports #-}
|
||||||
|
{-# LANGUAGE KindSignatures #-}
|
||||||
|
{-# LANGUAGE BangPatterns #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Module : HDT.Tasks
|
||||||
|
Description : IOHK Haskell Developer Test
|
||||||
|
Copyright : (c) Lars Brünjes, 2020
|
||||||
|
Maintainer : lars.bruenjes@iohk.io
|
||||||
|
Stability : experimental
|
||||||
|
Portability : portable
|
||||||
|
|
||||||
|
This module contains the IOHK Haskell Developer Test. Please complete all the
|
||||||
|
to-do's!
|
||||||
|
-}
|
||||||
|
module HDT.Tasks
|
||||||
|
( -- * @Agent@s
|
||||||
|
-- | We start by implementing a free monad,
|
||||||
|
-- which we will use to model a concurrent agent than
|
||||||
|
-- can communicate and coordinate with other agents by
|
||||||
|
-- exchanging broadcast messages.
|
||||||
|
Agent(..)
|
||||||
|
, delay
|
||||||
|
, broadcast
|
||||||
|
, receive
|
||||||
|
-- * Ping-pong example
|
||||||
|
-- |As an example of using agents, agents @'ping'@ and @'pong'@ defined below
|
||||||
|
-- keep sending @'Ping'@ and @'Pong'@ messages back and forth between themselves.
|
||||||
|
, PingPongMessage(..)
|
||||||
|
, ping
|
||||||
|
, pong
|
||||||
|
-- * An @'IO'@-interpreter for agents.
|
||||||
|
-- |In order to actually /run/ agents, we need to /interpret/ the free
|
||||||
|
-- @'Agent'@ monad. We start with an interpreter in @'IO'@,
|
||||||
|
-- which runs each agent in a list of agents in its own thread
|
||||||
|
-- and implements messaging by utilizing a shared @'TChan'@ broadcast channel.
|
||||||
|
--
|
||||||
|
-- We will then be able to run our ping-pong example:
|
||||||
|
--
|
||||||
|
-- ->>> runIO [ping, pong]
|
||||||
|
-- Ping
|
||||||
|
-- Pong
|
||||||
|
-- Ping
|
||||||
|
-- Pong
|
||||||
|
-- Ping
|
||||||
|
-- ...
|
||||||
|
, runIO
|
||||||
|
-- * Ouroboros Bft
|
||||||
|
-- | We use agents to implement a simplified version of the
|
||||||
|
-- <https://iohk.io/en/research/library/papers/ouroboros-bfta-simple-byzantine-fault-tolerant-consensus-protocol/ Ouroboros-BFT>
|
||||||
|
-- blockchain consensus protocol.
|
||||||
|
--
|
||||||
|
-- Ouroboros-BFT works as follows: A fixed number of @n@ /nodes/ participate in the
|
||||||
|
-- protocol. They collaborate on building a /blockchain/ by adding /blocks/,
|
||||||
|
-- and the protocol ensures that the nodes will agree on a /common prefix/
|
||||||
|
-- (all nodes agree on the prefix of the chain, but may disagree on a few blocks
|
||||||
|
-- towards the end).
|
||||||
|
-- This will work as long as at least two thirds of the nodes follow the protocol.
|
||||||
|
--
|
||||||
|
-- Time is divided in /slots/ of a fixed length, and in each slot, one node is the
|
||||||
|
-- /slot leader/ with the right to create the next block.
|
||||||
|
-- Slots leaders are determined in a round-robin fashion: Node 0
|
||||||
|
-- can create a block in Slot 0, Node 1 in Slot 1, Node @(n-1)@ in Slot @(n-1)@,
|
||||||
|
-- Node 0 in Slot @n@ and so on.
|
||||||
|
-- When a node is slot leader, it adds a block to its current chain and
|
||||||
|
-- broadcasts the new chain to the other nodes.
|
||||||
|
--
|
||||||
|
-- Each node holds on to a current chain (all nodes start with the chain
|
||||||
|
-- just consisting of the /genesis block/). When a node receives a new chain
|
||||||
|
-- from another node, it checks the new chain for /validity/ and
|
||||||
|
-- adopts it as its own chain /if it is longer than its own chain/.
|
||||||
|
--
|
||||||
|
-- A chain is /valid/ if
|
||||||
|
--
|
||||||
|
-- * The timestamps are stricly increasing,
|
||||||
|
-- * All blocks have been created by the slot leader of that slot and
|
||||||
|
-- * The newest block's timestamp is not from the future.
|
||||||
|
--
|
||||||
|
-- In reality, nodes would use digital signatures to sign the blocks
|
||||||
|
-- they create, and each block would contain a /payload/, but we
|
||||||
|
-- want to keep matters as simple as possible.
|
||||||
|
--
|
||||||
|
-- ->>> runIO $ clock : [node 3 nid | nid <- [0,1,2]]
|
||||||
|
-- Time 0
|
||||||
|
-- Time 1
|
||||||
|
-- NewChain (Genesis :> {1 1})
|
||||||
|
-- Time 2
|
||||||
|
-- NewChain (Genesis :> {1 1} :> {2 2})
|
||||||
|
-- Time 3
|
||||||
|
-- NewChain (Genesis :> {1 1} :> {2 2} :> {3 0})
|
||||||
|
-- Time 4
|
||||||
|
-- NewChain (Genesis :> {1 1} :> {2 2} :> {3 0} :> {4 1})
|
||||||
|
-- Time 5
|
||||||
|
-- NewChain (Genesis :> {1 1} :> {2 2} :> {3 0} :> {4 1} :> {5 2})
|
||||||
|
-- Time 6
|
||||||
|
-- NewChain (Genesis :> {1 1} :> {2 2} :> {3 0} :> {4 1} :> {5 2} :> {6 0})
|
||||||
|
-- ...
|
||||||
|
, Slot
|
||||||
|
, NodeId
|
||||||
|
, Block(..)
|
||||||
|
, Chain(..)
|
||||||
|
, chainLength
|
||||||
|
, slotLeader
|
||||||
|
, chainValid
|
||||||
|
, clock
|
||||||
|
, node
|
||||||
|
-- * A /pure/ interpreter for agents.
|
||||||
|
-- |We also want to be able to interpret a list of agents in a
|
||||||
|
-- /pure and deterministic/ fashion.
|
||||||
|
--
|
||||||
|
-- When we try this with our ping-pong example, we will be able to do:
|
||||||
|
--
|
||||||
|
-- >>> take 5 $ runPure [ping, pong]
|
||||||
|
-- [(1,Ping),(2,Pong),(3,Ping),(4,Pong),(5,Ping)]
|
||||||
|
, runPure
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Concurrent
|
||||||
|
import Control.Concurrent.Async
|
||||||
|
import Control.Concurrent.STM
|
||||||
|
import Control.Concurrent.STM.TChan
|
||||||
|
import Control.Monad
|
||||||
|
import GHC.IO.Handle.Lock
|
||||||
|
import Numeric.Natural ( Natural )
|
||||||
|
import Text.Printf ( printf )
|
||||||
|
import System.IO
|
||||||
|
|
||||||
|
import Debug.Trace
|
||||||
|
|
||||||
|
|
||||||
|
-- |An @'Agent' msg a@ is an abstract process that can send and receive broadcast
|
||||||
|
-- messages of type @msg@ and will eventually return a result of type @a@.
|
||||||
|
-- Define the type such that @'Agent' msg@ is a /free monad/ supporting the operations
|
||||||
|
-- @'delay'@, @'broadcast'@ and @'receive'@ below.
|
||||||
|
--
|
||||||
|
-- Agents can be used to model concurrent agents that communicate and coordinate
|
||||||
|
-- via message exchange over a broadcast channel.
|
||||||
|
data Agent msg a = Return a
|
||||||
|
| Delay (Agent msg a)
|
||||||
|
| Send msg (Agent msg a)
|
||||||
|
| Receive (msg -> Agent msg a)
|
||||||
|
|
||||||
|
instance Functor (Agent msg) where
|
||||||
|
fmap f (Return a ) = Return (f a)
|
||||||
|
fmap f (Delay agent ) = Delay (fmap f agent)
|
||||||
|
fmap f (Send msg agent) = Send msg (fmap f agent)
|
||||||
|
fmap f (Receive g ) = Receive (fmap f . g)
|
||||||
|
|
||||||
|
instance Applicative (Agent msg) where
|
||||||
|
pure = Return
|
||||||
|
(Return f ) <*> m = fmap f m
|
||||||
|
(Delay f ) <*> m = Delay (f <*> m)
|
||||||
|
(Send msg f) <*> m = Send msg (f <*> m)
|
||||||
|
(Receive f ) <*> m = Receive ((<*> m) . f)
|
||||||
|
|
||||||
|
-- The resulting monad should be /free/ and support operations
|
||||||
|
-- @'delay'@, @'broadcast'@ and @'receive'@ described below.
|
||||||
|
instance Monad (Agent msg) where
|
||||||
|
return = Return
|
||||||
|
m >>= ma = case m of
|
||||||
|
Return a -> ma a
|
||||||
|
Delay agent -> Delay (agent >>= ma)
|
||||||
|
Send msg agent -> Send msg (agent >>= ma)
|
||||||
|
Receive g -> Receive ((>>= ma) . g)
|
||||||
|
|
||||||
|
-- |Delay for one timestep.
|
||||||
|
delay :: Agent msg ()
|
||||||
|
delay = Delay (Return ())
|
||||||
|
|
||||||
|
-- |Broadcast a message.
|
||||||
|
broadcast :: msg -- ^The message to broadcast.
|
||||||
|
-> Agent msg ()
|
||||||
|
broadcast = flip Send (Return ())
|
||||||
|
|
||||||
|
-- |Wait for a broadcast and return the received message.
|
||||||
|
receive :: Agent msg msg
|
||||||
|
receive = Receive Return
|
||||||
|
|
||||||
|
-- |The message type used by agents @'ping'@ and @'pong'@.
|
||||||
|
data PingPongMessage =
|
||||||
|
Ping -- ^Message used by the @'ping'@ agent, which the @'pong'@ agent waits for.
|
||||||
|
| Pong -- ^Message used by the @'pong'@ agent, which the @'ping'@ agent waits for.
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
-- |Agent @'ping'@ starts by broadcasting a @'Ping'@ message, then
|
||||||
|
-- waits for a @'Pong'@ message and repeats.
|
||||||
|
-- Note how it guards against the possibility of receiving its own broadcasts!
|
||||||
|
ping :: Agent PingPongMessage ()
|
||||||
|
ping = delay >> broadcast Ping >> go
|
||||||
|
where
|
||||||
|
go = do
|
||||||
|
msg <- receive
|
||||||
|
case msg of
|
||||||
|
Ping -> go
|
||||||
|
Pong -> ping
|
||||||
|
|
||||||
|
-- |Agent @'pong'@ waits for a @'Ping'@ message, the broadcasts a @'Pong'@ message
|
||||||
|
-- and repeats.
|
||||||
|
pong :: Agent PingPongMessage ()
|
||||||
|
pong = do
|
||||||
|
msg <- receive
|
||||||
|
case msg of
|
||||||
|
Ping -> delay >> broadcast Pong >> pong
|
||||||
|
Pong -> pong
|
||||||
|
|
||||||
|
-- |Function @'runIO' agents@ runs each agent in the given list
|
||||||
|
-- concurrently in the @'IO'@-monad.
|
||||||
|
-- Broadcast should be realized using a @'TChan' msg@ which is shared amongst
|
||||||
|
-- the threads running each agent,
|
||||||
|
-- and @'runIO'@ should only return if and when every agent has returned.
|
||||||
|
--
|
||||||
|
-- The operations should be interpreted as follows:
|
||||||
|
--
|
||||||
|
-- * @'delay'@ should delay execution for one second.
|
||||||
|
-- * @'broadcast' msg@ should broadcast @msg@ via the shared @'TChan'@
|
||||||
|
-- and additionally log the message to the console. Doing this naively
|
||||||
|
-- could lead to garbled output, so care must be taken to ensure sequential access
|
||||||
|
-- to the console.
|
||||||
|
-- * @'receive'@ should block the thread until a message is received on the
|
||||||
|
-- shared @'TChan'@.
|
||||||
|
--
|
||||||
|
runIO :: forall msg
|
||||||
|
. Show msg
|
||||||
|
=> [Agent msg ()] -- ^ The agents to run concurrently.
|
||||||
|
-> IO ()
|
||||||
|
runIO agents = do
|
||||||
|
-- start broadcast channel
|
||||||
|
bchan <- newBroadcastTChanIO
|
||||||
|
|
||||||
|
-- for blocking stdout actions
|
||||||
|
mvar <- newTMVarIO ()
|
||||||
|
|
||||||
|
-- concurrently start agents
|
||||||
|
forConcurrently_ agents $ \agent -> do
|
||||||
|
-- broadcast channel needs to be duped for each thread
|
||||||
|
tchan <- atomically $ dupTChan bchan
|
||||||
|
runAgent agent tchan mvar
|
||||||
|
where
|
||||||
|
runAgent :: Agent msg () -> TChan msg -> TMVar () -> IO ()
|
||||||
|
runAgent agent tchan mvar =
|
||||||
|
-- unix write(2) is atomic too, but less portable
|
||||||
|
let printAtomic :: msg -> IO ()
|
||||||
|
printAtomic msg = do
|
||||||
|
void $ atomically $ takeTMVar mvar
|
||||||
|
hPutStrLn stdout (show msg)
|
||||||
|
hFlush stdout
|
||||||
|
atomically $ putTMVar mvar ()
|
||||||
|
run :: Agent msg () -> IO ()
|
||||||
|
run a = runAgent a tchan mvar
|
||||||
|
-- the IO interpreter
|
||||||
|
in case agent of
|
||||||
|
Return x -> return x
|
||||||
|
Delay next -> threadDelay 1000000 >> run next
|
||||||
|
Send msg next -> do
|
||||||
|
printAtomic msg
|
||||||
|
(atomically $ writeTChan tchan msg) >> run next
|
||||||
|
Receive f -> do
|
||||||
|
msg <- atomically $ readTChan tchan
|
||||||
|
run (f msg)
|
||||||
|
|
||||||
|
|
||||||
|
-- |Time is divided into @'Slot'@s.
|
||||||
|
type Slot = Natural
|
||||||
|
|
||||||
|
-- |Each node has a @'NodeId'@.
|
||||||
|
-- If there are @n@ nodes, their id's will be 0, 1, 2,...,@(n-1)@.
|
||||||
|
type NodeId = Natural
|
||||||
|
|
||||||
|
-- |The blockchain is built from @'Block'@s.
|
||||||
|
data Block = Block
|
||||||
|
{ slot :: Slot -- ^Timestamp indicating when the block was created.
|
||||||
|
, creator :: NodeId -- ^Identifies the node that created the block.
|
||||||
|
}
|
||||||
|
|
||||||
|
instance Show Block where
|
||||||
|
show b = printf "{%d %d}" (slot b) (creator b)
|
||||||
|
|
||||||
|
infixl 5 :>
|
||||||
|
|
||||||
|
-- |A blockchain can either be empty (just the genesis block) or contain @'Block'@s.
|
||||||
|
data Chain =
|
||||||
|
Genesis
|
||||||
|
| Chain :> Block
|
||||||
|
|
||||||
|
instance Show Chain where
|
||||||
|
showsPrec _ Genesis = showString "Genesis"
|
||||||
|
showsPrec d (c :> b) =
|
||||||
|
showParen (d > 10) $ showsPrec 0 c . showString " :> " . showString (show b)
|
||||||
|
|
||||||
|
-- |Computes the length of a @'Chain'@.
|
||||||
|
--
|
||||||
|
-- >>> chainLength Genesis
|
||||||
|
-- 0
|
||||||
|
-- >>> chainLength $ Genesis :> Block 2 2 :> Block 3 0
|
||||||
|
-- 2
|
||||||
|
chainLength :: Chain -> Int
|
||||||
|
chainLength = go 0
|
||||||
|
where
|
||||||
|
-- strict accumulator
|
||||||
|
go i c = case c of
|
||||||
|
Genesis -> i
|
||||||
|
(chain :> Block{}) -> let !acc = i + 1 in go acc chain
|
||||||
|
|
||||||
|
|
||||||
|
-- |Computes the slot leader.
|
||||||
|
--
|
||||||
|
-- >>> slotLeader 3 0
|
||||||
|
-- 0
|
||||||
|
-- >>> slotLeader 3 1
|
||||||
|
-- 1
|
||||||
|
-- >>> slotLeader 3 3
|
||||||
|
-- 0
|
||||||
|
slotLeader :: Int -- ^Total number of nodes.
|
||||||
|
-> Slot -- ^The @'Slot'@.
|
||||||
|
-> NodeId -- ^Identifies the node that has the right to create a block
|
||||||
|
-- in the given @'Slot'@.
|
||||||
|
slotLeader n s =
|
||||||
|
-- Not safe with n <= 0
|
||||||
|
-- Make sure this is efficient with large slot values.
|
||||||
|
let n' = fromIntegral n in (s + n') `rem` n'
|
||||||
|
|
||||||
|
-- |Determines whether a chain is valid.
|
||||||
|
--
|
||||||
|
-- >>> chainValid 3 4 $ Genesis :> Block 10 1
|
||||||
|
-- False
|
||||||
|
-- >>> chainValid 3 14 $ Genesis :> Block 10 1
|
||||||
|
-- True
|
||||||
|
-- >>> chainValid 3 14 $ Genesis :> Block 10 2
|
||||||
|
-- False
|
||||||
|
-- >>> chainValid 3 14 $ Genesis :> Block 3 1 :> Block 10 1
|
||||||
|
-- False
|
||||||
|
-- >>> chainValid 3 14 $ Genesis :> Block 3 0 :> Block 10 1
|
||||||
|
-- True
|
||||||
|
chainValid :: Int -- ^Total number of nodes.
|
||||||
|
-> Slot -- ^Current slot.
|
||||||
|
-> Chain -- ^Chain to validate.
|
||||||
|
-> Bool
|
||||||
|
chainValid _ _ Genesis = True
|
||||||
|
chainValid n s c@(_ :> bl) = slot bl <= s && val c
|
||||||
|
where
|
||||||
|
-- make sure we can short circuit and traverse only once
|
||||||
|
val :: Chain -> Bool
|
||||||
|
val Genesis = True
|
||||||
|
val (Genesis :> b1) = valSlotLeader b1
|
||||||
|
val (chain :> b1 :> b2) =
|
||||||
|
((slot b1) < (slot b2) && valSlotLeader b2) && val (chain :> b1)
|
||||||
|
|
||||||
|
valSlotLeader :: Block -> Bool
|
||||||
|
valSlotLeader Block {..} = slotLeader n slot == creator
|
||||||
|
|
||||||
|
|
||||||
|
-- |The type of messages used for communication in the BFT-protocol.
|
||||||
|
data BftMessage =
|
||||||
|
Time Slot -- ^Message used by the @'clock'@ to broadcast the current time.
|
||||||
|
| NewChain Chain -- ^Message used by a @'node'@ to announce a new @'Chain'@.
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
-- |The nodes do not keep track of time by themselves, but instead rely on
|
||||||
|
-- the @'clock'@ agent, which broadcasts the beginning of each new @'Slot'@
|
||||||
|
-- using @'Time'@-messages. The agent should start with @'Slot' 0@ and run forever.
|
||||||
|
clock :: Agent BftMessage a
|
||||||
|
clock = broadcast (Time 0) >> go 1
|
||||||
|
where go i = delay >> broadcast (Time i) >> go (i + 1)
|
||||||
|
|
||||||
|
|
||||||
|
-- |A @'node'@ participating in the BFT-protocol. It should start with the @'Genesis'@
|
||||||
|
-- chain at @'Slot' 0@ and run forever.
|
||||||
|
node :: forall a
|
||||||
|
. Int -- ^Total number of nodes.
|
||||||
|
-> NodeId -- ^Identifier of /this/ node.
|
||||||
|
-> Agent BftMessage a
|
||||||
|
node i n = go Genesis 0
|
||||||
|
where
|
||||||
|
go c t = do
|
||||||
|
msg <- receive
|
||||||
|
case msg of
|
||||||
|
Time 0 -> go c t
|
||||||
|
Time x -> newBlock c x
|
||||||
|
NewChain nc@(_ :> Block {..})
|
||||||
|
| chainValid i t nc
|
||||||
|
, chainLength nc > chainLength c
|
||||||
|
-- don't recurse on our own broadcast
|
||||||
|
-- and don't accept fake broadcasts
|
||||||
|
, creator /= n -> go nc t
|
||||||
|
NewChain _ -> go c t
|
||||||
|
|
||||||
|
-- create a new block if we are the slot leader
|
||||||
|
-- or recurse
|
||||||
|
newBlock :: Chain -> Slot -> Agent BftMessage a
|
||||||
|
newBlock c t
|
||||||
|
| slotLeader i t == n
|
||||||
|
= let nc = (c :> Block { slot = t, creator = n })
|
||||||
|
in broadcast (NewChain nc) >> go nc t
|
||||||
|
| otherwise
|
||||||
|
= go c t
|
||||||
|
|
||||||
|
-- |Interprets a list of agents in a /pure/ fashion,
|
||||||
|
-- returning the list of all broadcasts (with their timestamps).
|
||||||
|
--
|
||||||
|
-- __Hint:__ It might be helpful to keep track of
|
||||||
|
--
|
||||||
|
-- * all active agents,
|
||||||
|
-- * all delayed agents and
|
||||||
|
-- * all agents waiting for a broadcast.
|
||||||
|
runPure :: [Agent msg ()] -- ^The agents to run.
|
||||||
|
-> [(Natural, msg)] -- ^A list of all broadcasts, represented by
|
||||||
|
-- pairs containing a timestamp and the message that was sent.
|
||||||
|
runPure agents = zip [1 ..] $ go agents [] [] []
|
||||||
|
where
|
||||||
|
go :: [Agent msg ()] -- ^active agents
|
||||||
|
-> [Agent msg ()] -- ^delayed agents
|
||||||
|
-> [Agent msg ()] -- ^waiting agents
|
||||||
|
-> [msg] -- ^broadcast channel
|
||||||
|
-> [msg] -- ^all broadcasts
|
||||||
|
go [] [] [] _ = []
|
||||||
|
go [] [] _ _ = [] -- deadlock
|
||||||
|
-- if there are no active agents left, make all delayed agents active
|
||||||
|
go [] delayed waiting chan = go delayed [] waiting chan
|
||||||
|
-- there are active agents, run the first agent
|
||||||
|
go (a : as) delayed waiting chan = case a of
|
||||||
|
-- TODO: multiple inefficiencies. Complexity grows with number of agents.
|
||||||
|
Return _ -> go as delayed waiting chan
|
||||||
|
Delay next -> go as (delayed ++ [next]) waiting chan
|
||||||
|
-- we model the broadcast channel by just duplicating the message
|
||||||
|
-- for `length agents` and then making sure the sender does not
|
||||||
|
-- consume all of them by prioritizing waiting agents
|
||||||
|
Send msg next ->
|
||||||
|
msg
|
||||||
|
: (go (as ++ waiting ++ [next]) -- these are run next
|
||||||
|
delayed
|
||||||
|
[]
|
||||||
|
(chan ++ replicate numAgents msg)
|
||||||
|
)
|
||||||
|
ag@(Receive f) -> case chan of
|
||||||
|
[] -> go as delayed (waiting ++ [ag]) chan
|
||||||
|
(c : cs) -> go (as ++ [f c]) delayed waiting cs
|
||||||
|
numAgents = length agents
|
4
test/doctest.hs
Normal file
4
test/doctest.hs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import Test.DocTest
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = doctest ["-ilib", "lib/HDT/Tasks.hs"]
|
Loading…
Reference in New Issue
Block a user