diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Revision history for chainweb-mining-client
+
+## 0.2 -- 2020-08-20
+
+* Rename package into chainweb-mining-client
+
+## 0.1 -- 2020-08-20
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+Copyright (c) 2019 Kadena LLC
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+A mining client for Kadena Chainweb. It supports
+
+* multi threaded CPU mining,
+* external mining workers (e.g. a GPU),
+* simulated mining for testing.
+
+Competitive mining on the Kadena Chainweb Mainnet requires special mining
+hardware, which usually comes with its own mining client and mining pool
+support implementations. This generic mining client is intended mostly for
+testing.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,4 @@
+module Main (main) where
+
+import Configuration.Utils.Setup
+
diff --git a/chainweb-mining-client.cabal b/chainweb-mining-client.cabal
new file mode 100644
--- /dev/null
+++ b/chainweb-mining-client.cabal
@@ -0,0 +1,88 @@
+cabal-version: 3.0
+name: chainweb-mining-client
+description:
+    A mining client for Kadena Chainweb. It supports
+
+    * multi threaded CPU mining,
+    * external mining workers (e.g. a GPU),
+    * simulated mining for testing.
+
+    Competitive mining on the Kadena Chainweb Mainnet requires special mining
+    hardware, which usually comes with its own mining client and mining pool
+    support implementations. This generic mining client is intended mostly for
+    testing.
+
+version: 0.2
+synopsis: Mining Client for Kadena Chainweb
+homepage: https://github.com/kadena-io/chainweb-mining-client
+bug-reports: https://github.com/kadena-io/chainweb-mining-client/issues
+license: BSD-3-Clause
+license-file: LICENSE
+author: Lars Kuhtz
+maintainer: lars@kadena.io
+copyright: Copyright (c) 2019 - 2020, Kadena LLC
+category: Data, Mathematics
+build-type: Custom
+tested-with:
+      GHC==8.10.2
+    , GHC==8.8.4
+extra-source-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+    type: git
+    location: https://github.com/kadena-io/chainweb-mining-client.git
+
+custom-setup
+    setup-depends:
+          base >=4.10 && <5
+        , Cabal >=3.0
+        , configuration-tools >=0.5
+
+executable chainweb-mining-client
+    hs-source-dirs: main, src
+    main-is: Main.hs
+    other-modules:
+        PkgInfo
+        Logger
+        Worker
+        Worker.CPU
+        Worker.External
+        Worker.Simulation
+    autogen-modules:
+        PkgInfo
+    default-language: Haskell2010
+    ghc-options:
+        -Wall
+        -threaded
+        -with-rtsopts=-N
+    build-depends:
+          base >=4.10 && <4.15
+        , aeson >=1.5
+        , async >=2.2
+        , bytes >=0.17
+        , bytestring >=0.10
+        , configuration-tools >=0.5
+        , connection >=0.3
+        , containers >=0.5
+        , cryptonite >=0.27
+        , exceptions >=0.10
+        , hashable >=1.3
+        , hostaddress >=0.1
+        , http-client >=0.7
+        , http-client-tls >=0.3
+        , http-types >=0.12
+        , lens >=4.19
+        , loglevel >=0.1
+        , memory >=0.15
+        , mwc-random >=0.14
+        , process >=1.6
+        , retry >=0.8
+        , stm >=2.5
+        , streaming >=0.2
+        , streaming-events >=1.0
+        , text >=1.2
+        , time >=1.9
+        , unordered-containers >=0.2
+        , wai-extra >=3.0
diff --git a/main/Main.hs b/main/Main.hs
new file mode 100644
--- /dev/null
+++ b/main/Main.hs
@@ -0,0 +1,788 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TupleSections #-}
+{-# LANGUAGE TypeApplications #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+-- |
+-- Module: Main
+-- Copyright: Copyright © 2020 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+module Main
+( main
+) where
+
+import Configuration.Utils hiding (Error)
+
+import Control.Concurrent
+import Control.Concurrent.Async
+import Control.Concurrent.STM
+import Control.Exception (IOException, SomeAsyncException)
+import Control.Lens hiding ((.=))
+import Control.Monad
+import Control.Monad.Catch
+import Control.Monad.IO.Class
+import Control.Retry
+
+import Crypto.Hash.Algorithms (Blake2s_256)
+import qualified Crypto.PubKey.Ed25519 as C
+
+import Data.Bifunctor
+import qualified Data.ByteArray.Encoding as BA
+import Data.Bytes.Get
+import Data.Bytes.Put
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Char8 as B8
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.ByteString.Short as BS
+import Data.Hashable
+import qualified Data.HashMap.Strict as HM
+import Data.String
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import Data.Word
+
+import GHC.Generics
+
+import qualified Network.Connection as HTTP
+import Network.HostAddress
+import qualified Network.HTTP.Client as HTTP
+import qualified Network.HTTP.Client.TLS as HTTP
+import qualified Network.HTTP.Types.Status as HTTP
+import Network.Wai.EventSource.EventStream
+import Network.Wai.EventSource.Streaming
+
+import Numeric.Natural
+
+import PkgInfo hiding (tag)
+
+import qualified Streaming.Prelude as SP
+
+import System.LogLevel
+import qualified System.Random.MWC as MWC
+
+import qualified Text.ParserCombinators.ReadP as R
+import qualified Text.ParserCombinators.ReadPrec as P
+import Text.Printf
+import Text.Read (Read(..), readListPrecDefault)
+
+-- internal modules
+
+import Logger
+import Worker
+import Worker.CPU
+import Worker.External
+import Worker.Simulation
+
+-- -------------------------------------------------------------------------- --
+-- Orphans
+
+instance ToJSON HostAddress where
+    toJSON = toJSON . hostAddressToText
+    {-# INLINE toJSON #-}
+
+instance FromJSON HostAddress where
+    parseJSON = withText "HostAddress"
+        $ either (fail . show) return . hostAddressFromText
+    {-# INLINE parseJSON #-}
+
+-- -------------------------------------------------------------------------- --
+--  Utils
+
+textReader :: (T.Text -> Either SomeException a) -> ReadM a
+textReader p = eitherReader $ first show . p . T.pack
+
+sshow :: Show a => IsString b => a -> b
+sshow = fromString . show
+{-# INLINE sshow #-}
+
+-- -------------------------------------------------------------------------- --
+-- Integral Unit Prefixes
+
+-- | TODO: make this type roundtripable and add support for fractional unit
+-- prefixes.
+--
+newtype UnitPrefixed a = UnitPrefixed { _getUnitPrefixed :: a }
+    deriving newtype
+        ( Show
+        , Eq
+        , Ord
+        , Enum
+        , Bounded
+        , Num
+        , Integral
+        , Fractional
+        , Floating
+        , Real
+        , ToJSON
+        , FromJSON
+        )
+
+instance (Num a, Read a) => Read (UnitPrefixed a) where
+    readPrec = UnitPrefixed <$> readWithUnit
+    readListPrec = readListPrecDefault
+    {-# INLINE readPrec #-}
+    {-# INLINE readListPrec #-}
+
+-- | Read number with Unit Prefixes. The implementation supports integral SI
+-- units prefixes. Binary prefixes are supported according to ISO/IEC 80000.
+--
+-- "Kilo" is supported, both in upper and lower case.
+--
+readWithUnit :: forall a . Num a => Read a => P.ReadPrec a
+readWithUnit = do
+    n <- readPrec
+    p <- P.lift $ noPrefix <|> siPrefix <|> binaryPrefix
+    return $! n * p
+  where
+    noPrefix :: R.ReadP a
+    noPrefix = 1 <$ R.eof
+
+    siPrefix :: R.ReadP a
+    siPrefix
+        = 10^(1 :: Int) <$ R.string "da"
+        <|> 10^(2 :: Int) <$ R.char 'h'
+        <|> 10^(3 :: Int) <$ (R.char 'K' <|> R.char 'k')
+        <|> 10^(6 :: Int) <$ R.char 'M'
+        <|> 10^(9 :: Int) <$ R.char 'G'
+        <|> 10^(12 :: Int) <$ R.char 'T'
+        <|> 10^(15 :: Int) <$ R.char 'P'
+        <|> 10^(18 :: Int) <$ R.char 'E'
+        <|> 10^(21 :: Int) <$ R.char 'Z'
+        <|> 10^(24 :: Int) <$ R.char 'Y'
+
+    binaryPrefix :: R.ReadP a
+    binaryPrefix
+        = 1024^(1 :: Int) <$ (R.string "Ki" <|> R.string "ki")
+        <|> 1024^(2 :: Int) <$ R.string "Mi"
+        <|> 1024^(3 :: Int) <$ R.string "Gi"
+        <|> 1024^(4 :: Int) <$ R.string "Ti"
+        <|> 1024^(5 :: Int) <$ R.string "Pi"
+        <|> 1024^(6 :: Int) <$ R.string "Ei"
+        <|> 1024^(7 :: Int) <$ R.string "Zi"
+        <|> 1024^(8 :: Int) <$ R.string "Yi"
+
+-- -------------------------------------------------------------------------- --
+-- Miner
+
+-- | The account name is that same as the public key. Different account names
+-- are not supported.
+--
+-- Only a single base64UrlWithoutPadding encoded key may be used and the keyset
+-- is "keys-all".
+--
+newtype MinerPublicKey = MinerPublicKey T.Text
+    deriving (Show, Eq, Ord, Generic)
+    deriving newtype (Hashable)
+
+instance ToJSON MinerPublicKey where
+    toJSON (MinerPublicKey k) = toJSON k
+    {-# INLINE toJSON #-}
+
+instance FromJSON MinerPublicKey where
+    parseJSON = withText "MinerPublicKey" $ return . MinerPublicKey
+    {-# INLINE parseJSON #-}
+        -- TODO perform well-formedness checks
+
+newtype Miner = Miner MinerPublicKey
+    deriving (Show, Eq, Ord, Generic)
+    deriving newtype (Hashable)
+
+instance ToJSON Miner where
+    toJSON (Miner (MinerPublicKey k)) = object
+        [ "account" .= k
+        , "public-keys" .= [ k ]
+        , "predicate" .= ("keys-all" :: T.Text)
+        ]
+
+-- -------------------------------------------------------------------------- --
+-- Worker Configuration
+
+data WorkerConfig
+    = CpuWorker
+    | ExternalWorker
+    | SimulationWorker
+    deriving (Show, Eq, Ord, Generic)
+    deriving anyclass (Hashable)
+
+instance ToJSON WorkerConfig where
+    toJSON = toJSON . workerConfigToText
+    {-# INLINE toJSON #-}
+
+instance FromJSON WorkerConfig where
+    parseJSON = withText "WorkerConfig" $
+        either (fail . show) return . workerConfigFromText
+    {-# INLINE parseJSON #-}
+
+workerConfigToText :: WorkerConfig -> T.Text
+workerConfigToText CpuWorker = "cpu"
+workerConfigToText ExternalWorker = "external"
+workerConfigToText SimulationWorker = "simulation"
+
+workerConfigFromText :: MonadThrow m => T.Text -> m WorkerConfig
+workerConfigFromText t = case T.toCaseFold t of
+    "cpu" -> return CpuWorker
+    "external" -> return ExternalWorker
+    "simulation" -> return SimulationWorker
+    _ -> error $ "unknown worker configuraton: " <> T.unpack t
+
+-- -------------------------------------------------------------------------- --
+-- Configuration
+
+newtype ChainwebVersion = ChainwebVersion T.Text
+    deriving (Show, Read, Eq, Ord, Generic)
+    deriving newtype (Hashable, ToJSON, FromJSON)
+
+data Config = Config
+    { _configHashRate :: !(UnitPrefixed HashRate)
+    , _configNode :: !HostAddress
+    , _configUseTls :: !Bool
+    , _configInsecure :: !Bool
+    , _configPublicKey :: !MinerPublicKey
+    , _configThreadCount :: !Natural
+    , _configGenerateKey :: !Bool
+    , _configLogLevel :: !LogLevel
+    , _configWorker :: !WorkerConfig
+    , _configExternalWorkerCommand :: !String
+    }
+    deriving (Show, Eq, Ord, Generic)
+
+makeLenses ''Config
+
+defaultConfig :: Config
+defaultConfig = Config
+    { _configHashRate = UnitPrefixed defaultHashRate
+    , _configNode = unsafeHostAddressFromText "localhost:1789"
+    , _configUseTls = True
+    , _configInsecure = True
+    , _configPublicKey = MinerPublicKey ""
+    , _configThreadCount = 10
+    , _configGenerateKey = False
+    , _configLogLevel = Info
+    , _configWorker = CpuWorker
+    , _configExternalWorkerCommand = "echo 'no external worker command configured' && /bin/false"
+    }
+
+instance ToJSON Config where
+    toJSON c = object
+        [ "hashRate" .= _configHashRate c
+        , "node" .= _configNode c
+        , "useTls" .= _configUseTls c
+        , "insecure" .= _configInsecure c
+        , "publicKey" .= _configPublicKey c
+        , "threadCount" .= _configThreadCount c
+        , "generateKey" .= _configGenerateKey c
+        , "logLevel" .= logLevelToText @T.Text (_configLogLevel c)
+        , "worker" .= _configWorker c
+        , "externalWorkerCommand" .= _configExternalWorkerCommand c
+        ]
+
+instance FromJSON (Config -> Config) where
+    parseJSON = withObject "Config" $ \o -> id
+        <$< configHashRate ..: "hashRate" % o
+        <*< configNode ..: "node" % o
+        <*< configUseTls ..: "useTls" % o
+        <*< configInsecure ..: "insecure" % o
+        <*< configPublicKey ..: "publicKey" % o
+        <*< configThreadCount ..: "threadCount" % o
+        <*< configGenerateKey ..: "generateKey" % o
+        <*< setProperty configLogLevel "logLevel" parseLogLevel o
+        <*< configWorker ..: "worker" % o
+        <*< configExternalWorkerCommand ..: "externalWorkerCommand" % o
+      where
+        parseLogLevel = withText "LogLevel" $ return . logLevelFromText
+
+parseConfig :: MParser Config
+parseConfig = id
+    <$< configHashRate .:: option auto
+        % short 'r'
+        <> long "hash-rate"
+        <> help "hashes per second (only relevant for mining simulation, ignored by the cpu worker)"
+    <*< configNode .:: option (textReader hostAddressFromText)
+        % short 'n'
+        <> long "node"
+        <> help "node to which to connect"
+        <> metavar "DOMAIN:PORT"
+    <*< configUseTls .:: boolOption_
+        % short 't'
+        <> long "tls"
+        <> help "use TLS to connect to node"
+    <*< configInsecure .:: boolOption_
+        % short 'x'
+        <> long "insecure"
+        <> help "accept self-signed TLS certificates"
+    <*< configPublicKey .:: fmap MinerPublicKey . strOption
+        % short 'k'
+        <> long "public-key"
+        <> help "the public-key for the mining rewards account"
+    <*< configThreadCount .:: option auto
+        % short 'c'
+        <> long "thread-count"
+        <> help "number of concurrent mining threads"
+    <*< configGenerateKey .:: boolOption_
+        % long "generate-key"
+        <> help "Generate a new key pair and exit"
+    <*< configLogLevel .:: option (textReader $ Right . logLevelFromText)
+        % short 'l'
+        <> long "log-level"
+        <> help "Level at which log messages are written to the console"
+        <> metavar "error|warn|info|debug"
+    <*< configWorker .:: option (textReader workerConfigFromText)
+        % short 'w'
+        <> long "worker"
+        <> help "The type of mining worker that is used"
+        <> metavar "cpu|external|simulation"
+    <*< configExternalWorkerCommand .:: option (textReader $ Right . T.unpack)
+        % long "external-worker-cmd"
+        <> help "command that is used to call an external worker. When the command is called the target value is added as last parameter to the command line."
+
+-- -------------------------------------------------------------------------- --
+-- HTTP Retry Logic
+
+-- | We don't limit retries. The maximum delay between retries is 5 seconds.
+--
+-- TODO: add configuration option for limitRetriesByCumulativeDelay
+--
+retryHttp :: Logger -> IO a -> IO a
+retryHttp logger = recovering policy (httpRetryHandler logger) . const
+  where
+    policy = capDelay 5000000 $ fullJitterBackoff 100
+
+httpRetryHandler :: Logger -> [RetryStatus -> Handler IO Bool]
+httpRetryHandler logger = skipAsyncExceptions <>
+    [ logRetries (return . httpRetries) f
+    , logRetries (\(_ :: SomeException) -> return True) logRetry
+    ]
+  where
+    logRetry True reason s = writeLog logger Warn
+        $ "Http request failed: " <> sshow reason
+        <> ". Retrying attempt " <> sshow (rsIterNumber s)
+    logRetry False reason s = writeLog logger Warn
+        $ "Http request finally failed after " <> sshow (rsIterNumber s)
+        <> " retries: " <> sshow reason
+
+    f True (HTTP.HttpExceptionRequest _req reason) s = logRetry True reason s
+    f False (HTTP.HttpExceptionRequest _req reason) s = logRetry False reason s
+    f _ e _ = throwM e
+
+
+-- | HTTP Exceptions for which a retry may result in subsequent succes.
+--
+-- This retries rather aggressively on any server or network related failure
+-- condition.
+--
+httpRetries :: HTTP.HttpException -> Bool
+httpRetries (HTTP.HttpExceptionRequest _req reason) = case reason of
+    HTTP.StatusCodeException resp _body
+        | HTTP.statusIsServerError (HTTP.responseStatus resp) -> True
+    HTTP.ResponseTimeout -> True
+    HTTP.ConnectionTimeout -> True
+    HTTP.ConnectionFailure _e -> True
+    HTTP.InvalidStatusLine _bs -> True
+    HTTP.InvalidHeader _bs -> True
+    HTTP.InternalException _e -> True
+    HTTP.ProxyConnectException _host _port status
+        | HTTP.statusIsServerError status -> True
+    HTTP.NoResponseDataReceived -> True
+    HTTP.ResponseBodyTooShort _expected _actual -> True
+    HTTP.InvalidChunkHeaders -> True
+    HTTP.IncompleteHeaders -> True
+    HTTP.HttpZlibException _e -> True
+    HTTP.ConnectionClosed -> True
+    _ -> False
+httpRetries (HTTP.InvalidUrlException _url _reason) = False
+
+-- -------------------------------------------------------------------------- --
+-- Chainweb Mining API Types
+
+-- | ChainId
+--
+newtype ChainId = ChainId Word32
+    deriving (Show, Eq, Ord, Generic)
+    deriving anyclass (Hashable)
+
+decodeChainId :: MonadGet m => m ChainId
+decodeChainId = ChainId <$> getWord32le
+
+encodeChainId :: MonadPut m => ChainId -> m ()
+encodeChainId (ChainId w32) = putWord32le w32
+
+-- -------------------------------------------------------------------------- --
+-- Chainweb Mining API Requetss
+
+newtype GetWorkFailure = GetWorkFailure T.Text
+    deriving (Show, Eq, Ord)
+
+instance Exception GetWorkFailure
+
+-- | Make an HTTP request with an JSON response
+--
+getJson :: FromJSON a => HTTP.Manager -> HTTP.Request -> IO a
+getJson mgr req = (eitherDecode . HTTP.responseBody <$> HTTP.httpLbs req mgr) >>= \case
+    Left e -> error $ "Failed to decode json response: " <> show e
+    Right r -> return r
+
+-- | Base request type for chainweb queries
+--
+baseReq :: Config -> ChainwebVersion -> B.ByteString -> HTTP.Request
+baseReq conf (ChainwebVersion v) pathSuffix = HTTP.defaultRequest
+        { HTTP.host = T.encodeUtf8 $ hostnameToText $ _hostAddressHost node
+        , HTTP.path = "chainweb/0.0/" <> T.encodeUtf8 v <> "/" <> pathSuffix
+        , HTTP.port = fromIntegral $ _hostAddressPort node
+        , HTTP.secure = _configUseTls conf
+        , HTTP.method = "GET"
+        , HTTP.checkResponse = HTTP.throwErrorStatusCodes
+        }
+  where
+    node = _configNode conf
+
+-- | Query node info
+--
+getInfo :: Config -> HTTP.Manager -> IO (HM.HashMap T.Text Value)
+getInfo conf mgr = getJson mgr req
+  where
+    req = HTTP.defaultRequest
+        { HTTP.host = T.encodeUtf8 $ hostnameToText $ _hostAddressHost node
+        , HTTP.path = "info"
+        , HTTP.port = fromIntegral $ _hostAddressPort node
+        , HTTP.secure = _configUseTls conf
+        , HTTP.method = "GET"
+        , HTTP.checkResponse = HTTP.throwErrorStatusCodes
+        }
+    node = _configNode conf
+
+-- | Obtain chainweb version of the chainweb node
+--
+-- No retry here. This is use at startup and we want to fail fast if the node
+-- isn't available.
+--
+getNodeVersion :: Config -> HTTP.Manager -> IO ChainwebVersion
+getNodeVersion conf mgr = do
+    i <- getInfo conf mgr
+    case HM.lookup "nodeVersion" i of
+        Just (String x) -> return $ ChainwebVersion x
+        _ -> error "failed to parse chainweb version from node info"
+
+-- | Get new work from the chainweb node (for some available chain)
+--
+-- We don't retry here. If this fails, we loop around.
+--
+getJob :: Config -> ChainwebVersion -> HTTP.Manager -> IO (ChainId, Target, Work)
+getJob conf ver mgr = do
+    bytes <- HTTP.httpLbs req mgr
+    case runGetS decodeJob (BL.toStrict $ HTTP.responseBody $ bytes) of
+        Left e -> error $ "failed to decode work: " <> sshow e
+        Right (a,b,c) -> return (a, b, c)
+  where
+    req = (baseReq conf ver "mining/work")
+        { HTTP.requestBody = HTTP.RequestBodyLBS $ encode $ Miner $ _configPublicKey conf
+        , HTTP.requestHeaders = [("content-type", "application/json")]
+        }
+
+    decodeJob :: MonadGet m => m (ChainId, Target, Work)
+    decodeJob = (,,)
+        <$> decodeChainId
+        <*> decodeTarget
+        <*> decodeWork
+
+-- | Post solved work to the chainweb node
+--
+-- No timeout is used and in case of failure we retry aggressively. If the
+-- solvedwork becomes stale, the thread will be preempted and cancled.
+--
+postSolved :: Config -> ChainwebVersion -> Logger -> HTTP.Manager -> Work -> IO ()
+postSolved conf ver logger mgr (Work bytes) = retryHttp logger $ do
+    logg Info "post solved worked"
+    (void $ HTTP.httpLbs req mgr)
+        `catch` \(e@(HTTP.HttpExceptionRequest _ _)) -> do
+            logg Error $ "failed to submit solved work: " <> sshow e
+            return ()
+  where
+    logg = writeLog logger
+    req = (baseReq conf ver "mining/solved")
+        { HTTP.requestBody = HTTP.RequestBodyBS $ BS.fromShort $ bytes
+        , HTTP.method = "POST"
+        }
+
+-- | Automatically restarts the stream when the response status is 2** and throws
+-- and exception otherwise.
+--
+-- No
+-- No retry is used. Retrying is handled by the outer logic.
+--
+updateStream
+    :: Config
+    -> ChainwebVersion
+    -> Logger
+    -> HTTP.Manager
+    -> ChainId
+    -> TVar Int
+    -> IO ()
+updateStream conf v logger mgr cid var =
+    liftIO $ withEvents req mgr $ \updates -> updates
+        & SP.filter realEvent
+        & SP.chain (\_ -> logg Info $ "got update on chain " <> sshow cid)
+        & SP.mapM_ (\_ -> atomically $ modifyTVar' var (+ 1))
+  where
+    logg = writeLog logger
+
+    realEvent ServerEvent{} = True
+    realEvent _ = False
+
+    req = (baseReq conf v "mining/updates")
+        { HTTP.requestBody = HTTP.RequestBodyBS $ runPutS $ encodeChainId cid
+        , HTTP.responseTimeout = HTTP.responseTimeoutNone
+        }
+
+-- -------------------------------------------------------------------------- --
+-- Trigger Type
+
+data Reason = Timeout | Update | StreamClosed | StreamFailed SomeException
+    deriving (Show)
+
+-- | A trigger is used to preempt a worker thread.
+--
+newtype Trigger = Trigger (STM Reason)
+
+awaitTrigger :: Trigger -> IO Reason
+awaitTrigger (Trigger t) = atomically t
+
+-- -------------------------------------------------------------------------- --
+-- Update Map
+
+newtype UpdateFailure = UpdateFailure T.Text
+    deriving (Show, Eq, Ord)
+
+instance Exception UpdateFailure
+
+-- | This keeps track of the current work for the respective chain. It is shared
+-- among all worker threads. If an update for some chain is received the worker
+-- threads are preempted and query new work.
+--
+-- It also keeps track of the long-polling update streams.
+--
+newtype UpdateMap = UpdateMap
+    { _updateMap :: MVar (HM.HashMap ChainId (TVar Int, Async ()))
+    }
+
+-- | Creates a map that maintains one upstream for each chain
+--
+newUpdateMap :: IO UpdateMap
+newUpdateMap = UpdateMap <$> newMVar mempty
+
+-- | Obtain a trigger that is used to preempt a worker threads. It notifies the
+-- thread if an update is available.
+--
+getTrigger
+    :: Config
+    -> ChainwebVersion
+    -> Logger
+    -> HTTP.Manager
+    -> UpdateMap
+    -> ChainId
+    -> IO Trigger
+getTrigger conf ver logger mgr (UpdateMap v) k = modifyMVar v $ \m -> case HM.lookup k m of
+
+    -- If there exists already an update stream, check that it's live, and
+    -- restart if necessary.
+    --
+    Just s -> do
+        logg Debug "use existing update stream"
+        n@(!var, !a) <- checkStream s
+        !t <- newTrigger var a
+        let !x = HM.insert k n m
+        return (x, t)
+
+    -- If there isn't an update stream in the map, create a new one.
+    --
+    Nothing -> do
+        logg Debug "create new update stream"
+        n@(!var, !a) <- newTVarIO 0 >>= newUpdateStream
+        !t <- newTrigger var a
+        let !x = HM.insert k n m
+        return (x, t)
+  where
+    logg = writeLog logger
+
+    checkStream :: (TVar Int, Async ()) -> IO (TVar Int, Async ())
+    checkStream (!var, !a) = poll a >>= \case
+        Nothing -> return (var, a)
+        Just (Left _) -> newUpdateStream var -- TODO logging, throttling
+        Just (Right _) -> newUpdateStream var
+
+    newUpdateStream :: TVar Int -> IO (TVar Int, Async ())
+    newUpdateStream var = (var,)
+        <$> async (updateStream conf ver logger mgr k var)
+
+    -- There are three possible outcomes
+    --
+    newTrigger :: TVar Int -> Async () -> IO Trigger
+    newTrigger var a = do
+        cur <- readTVarIO var
+        timeoutVar <- registerDelay (5 * 30_000_000)
+            -- 5 times the block time ~ 0.7% of all blocks. This for detecting if
+            -- a stream gets stale without failing.
+
+        return $ Trigger $ pollSTM a >>= \case
+            Just (Right ()) -> return StreamClosed
+            Just (Left e) -> return $ StreamFailed e
+            Nothing -> do
+                isTimeout <- readTVar timeoutVar
+                isUpdate <- (/= cur) <$> readTVar var
+                unless (isTimeout || isUpdate) retry
+                return Update
+
+-- | Run an operation that is preempted if an update event occurs.
+--
+-- Streams are restarted automatically, when they got closed by the server. We
+-- don't restart streams automatically in case of a failure, but instead throw
+-- an exception. Failures are supposed to be handled in the outer mining
+-- functions.
+--
+-- There is risk that a stream stalls without explicitely failing. We solve this
+-- by preempting the loop if we haven't seen an update after 5 times the block
+-- time (which will affect about 0.7% of all blocks).
+--
+withPreemption
+    :: Config
+    -> ChainwebVersion
+    -> Logger
+    -> HTTP.Manager
+    -> UpdateMap
+    -> ChainId
+    -> IO a
+    -> IO (Either () a)
+withPreemption conf ver logger mgr m k = race awaitChange
+  where
+    awaitChange = do
+        trigger <- getTrigger conf ver logger mgr m k
+        awaitTrigger trigger >>= \case
+            StreamClosed -> awaitChange
+            StreamFailed e -> throwM $ UpdateFailure $ "update stream failed: " <> errMsg e
+            Timeout -> throwM $ UpdateFailure "timeout of update stream"
+            Update -> return ()
+
+    errMsg e = case fromException e of
+        Just (HTTP.HttpExceptionRequest _ ex) -> sshow ex
+        _ -> sshow e
+
+-- -------------------------------------------------------------------------- --
+-- Mining Loop
+
+data Recovery = Irrecoverable | Recoverable
+
+-- | The outer mining loop.
+--
+miningLoop
+    :: Config
+    -> ChainwebVersion
+    -> Logger
+    -> HTTP.Manager
+    -> UpdateMap
+    -> Worker
+    -> IO ()
+miningLoop conf ver logger mgr umap worker = go
+  where
+    nonce = Nonce 0
+    logg = writeLog logger
+    go = (forever loopBody `catches` handlers) >>= \case
+        Irrecoverable -> return ()
+        Recoverable -> threadDelay 500_000 >> go
+
+    handlers =
+        [ Handler $ \(e :: IOException) -> do
+            logg Error $ T.pack $ displayException e
+            return Irrecoverable
+        , Handler $ \(e :: GetWorkFailure) -> do
+            logg Error $ T.pack $ displayException e
+            return Irrecoverable -- FIXME we want proper retry logic for all of this!
+        , Handler $ \(e :: UpdateFailure) -> do
+            logg Error $ T.pack $ displayException e
+            return Recoverable
+        , Handler $ \(e :: SomeAsyncException) -> do
+            logg Warn $ "Mining Loop terminated: " <> sshow e
+            throwM e
+        , Handler $ \(e :: SomeException) -> do
+            logg Warn "Some general error in mining loop. Trying again..."
+            logg Info $ "Exception: " <> T.pack (displayException e)
+            return Recoverable
+        ]
+
+    loopBody = do
+        (cid, target, work) <- getJob conf ver mgr
+        logg Info $ "got new work for chain " <> sshow cid
+        withPreemption conf ver logger mgr umap cid (worker nonce target work) >>= \case
+            Right solved -> do
+                -- TODO: we should do this asynchronously, however, preemption
+                -- should still apply. So, ideally, we would kick of a new
+                -- asynchronous loop interation while continuing this loop
+                -- iteration here.
+                postSolved conf ver logger mgr solved
+                logg Debug "submitted work"
+            Left () ->
+                logg Info "Mining loop was preempted. Getting updated work ..."
+
+-- -------------------------------------------------------------------------- --
+-- Key generation
+
+genKeys :: IO ()
+genKeys = do
+    sk <- C.generateSecretKey
+    let !pk = C.toPublic sk
+    printf "public:  %s\n" (B8.unpack $ BA.convertToBase BA.Base16 pk)
+    printf "private: %s\n" (B8.unpack $ BA.convertToBase BA.Base16 sk)
+
+-- -------------------------------------------------------------------------- --
+-- Main
+
+mainInfo :: ProgramInfo Config
+mainInfo = programInfo "Kadena Chainweb Mining Client" parseConfig defaultConfig
+
+-- | TODO: validate the configuration:
+--
+-- * MinerPublicKey must be present
+-- * node must be present
+--
+main :: IO ()
+main = runWithPkgInfoConfiguration mainInfo pkgInfo $ \conf ->
+    if _configGenerateKey conf
+      then genKeys
+      else withLogger (_configLogLevel conf) $ run conf
+
+run :: Config -> Logger -> IO ()
+run conf logger = do
+    mgr <- HTTP.newManager (HTTP.mkManagerSettings tlsSettings Nothing)
+        { HTTP.managerResponseTimeout = HTTP.responseTimeoutMicro 1000000 }
+            -- We don't want to wait too long, because latencies matter in
+            -- mining. NOTE, however, that for large blocks it can take a while
+            -- to get new work. This can be an issue with public mining mode.
+            -- For private mining that is done asynchronously. Public mining is
+            -- considered deprecated.
+    ver <- getNodeVersion conf mgr
+    rng <- MWC.createSystemRandom
+    updateMap <- newUpdateMap
+    forConcurrently_ [0 .. (_configThreadCount conf) - 1] $ \i ->
+        withLogTag logger ("Thread " <> sshow i) $ \taggedLogger ->
+            miningLoop conf ver taggedLogger mgr updateMap $
+                case _configWorker conf of
+                    SimulationWorker -> simulationWorker taggedLogger rng workerRate
+                    ExternalWorker -> externalWorker taggedLogger (_configExternalWorkerCommand conf)
+                    CpuWorker -> cpuWorker @Blake2s_256 taggedLogger
+  where
+    tlsSettings = HTTP.TLSSettingsSimple (_configInsecure conf) False False
+
+    workerRate = _getUnitPrefixed (_configHashRate conf) / fromIntegral (_configThreadCount conf)
+
diff --git a/src/Logger.hs b/src/Logger.hs
new file mode 100644
--- /dev/null
+++ b/src/Logger.hs
@@ -0,0 +1,227 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- |
+-- Module: Logger
+-- Copyright: Copyright © 2020 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+-- A Lightweight Logging System
+--
+module Logger
+( maxLoggerQueueSize
+, Logger
+, withLogger
+, withLogTag
+, writeLog
+) where
+
+import Control.Concurrent
+import Control.Concurrent.Async
+import Control.Exception
+import Control.Monad
+
+import Data.IORef
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import Data.Time.Clock.System
+import Data.Time.Format.ISO8601
+
+import System.IO
+import System.IO.Unsafe
+import System.LogLevel
+
+-- -------------------------------------------------------------------------- --
+-- Constants
+
+maxLoggerQueueSize :: Int
+maxLoggerQueueSize = 10000
+
+-- -------------------------------------------------------------------------- --
+-- Utils
+
+incrementCounter :: IORef Int -> IO ()
+incrementCounter ref = atomicModifyIORef' ref $ \x -> (x + 1, ())
+
+decrementCounter :: IORef Int -> IO ()
+decrementCounter ref = atomicModifyIORef' ref $ \x -> (x - 1, ())
+
+resetCounter :: IORef Int -> IO Int
+resetCounter ref = atomicModifyIORef' ref $ \x -> (0, x)
+
+-- -------------------------------------------------------------------------- --
+-- Terminal Colors
+
+useColor :: Bool
+useColor = unsafePerformIO $ hIsTerminalDevice stdout
+
+data Color = Black | Red  | Green | Yellow | Blue | Magenta | Cyan | White
+
+colorCode :: Color -> Int
+colorCode Black = 0
+colorCode Red = 1
+colorCode Green = 2
+colorCode Yellow = 3
+colorCode Blue = 4
+colorCode Magenta = 5
+colorCode Cyan = 6
+colorCode White = 7
+
+asDull, asVivid  :: Color -> T.Text -> T.Text
+asDull c t = setCode (30 + colorCode c) <> t <> setCode 0
+asVivid c t = setCode (90 + colorCode c) <> t <> setCode 0
+
+setCode :: Int -> T.Text
+setCode c
+    | useColor = "\ESC[" <> (T.pack . show) c <> "m"
+    | otherwise = ""
+
+-- -------------------------------------------------------------------------- --
+--  Log Messages
+
+-- | Log Message that is emitted by the code and enqueued in the logging queue.
+--
+-- The goal is to produce messages with very low latency in order to not delay
+-- production logic. The most expensive part obtaining the system time, which is
+-- stored raw without formatting.
+--
+-- When the LogMessage is written to the queue it must be fully evaluted to
+-- normal form.
+--
+data LogMessage = LogMessage
+    { _logMessageText :: !T.Text
+    , _logMessageLevel :: !LogLevel
+    , _logMessageTime :: !SystemTime
+    , _logMessageTags :: ![T.Text]
+    }
+
+-- | Format Log message.
+--
+-- This is done in the logging backend asynchronously.
+--
+-- TODO: implement Chunk formatting.
+--
+formatLogMessage :: LogMessage -> T.Text
+formatLogMessage !msg =
+    asDull Cyan (padTime . T.pack . iso8601Show . systemToUTCTime $ _logMessageTime msg)
+    <> " "
+    <> formatLogLevel (_logMessageLevel msg)
+    <> " "
+    <> bracketed (formatTags (_logMessageTags msg))
+    <> " "
+    <> _logMessageText msg
+  where
+    formatTags tags = T.intercalate "|" $ asDull Green <$> reverse tags
+    bracketed t = "[" <> t <> "]"
+
+    padTime t
+        | T.length t == 28 = t
+        | otherwise = T.take 27 (T.drop 1 t <> "000000") <> "Z"
+
+    formatLogLevel Quiet = "Quiet"
+    formatLogLevel Debug = asVivid Blue "Debug"
+    formatLogLevel Info = asVivid Yellow "Info "
+    formatLogLevel Warn = asVivid Magenta "Warn "
+    formatLogLevel Error = asVivid Red "Error"
+    formatLogLevel (Other x) = asVivid Blue x
+
+-- -------------------------------------------------------------------------- --
+-- Logger Context
+
+data Logger = Logger
+    { _loggerTags :: [T.Text]
+    , _loggerLevel :: !LogLevel
+    , _loggerQueue :: !(Chan LogMessage)
+    , _loggerApproxQueueSize :: !(IORef Int)
+    , _loggerSkipped :: !(IORef Int)
+    , _loggerMaxQueueSize :: !Int
+    }
+
+-- | Locally push a tag to the stack of log message tags.
+--
+withLogTag :: Logger -> T.Text -> (Logger -> IO a) -> IO a
+withLogTag !logger !tag inner = inner $! logger
+    { _loggerTags = tag : _loggerTags logger
+    }
+
+-- | Write a log message.
+--
+-- A common pattern is to define a local helper function:
+--
+-- @
+-- where
+--   logg = writeLog logger
+-- @
+--
+writeLog :: Logger -> LogLevel -> T.Text -> IO ()
+writeLog !logger !level !msg
+    | level <= _loggerLevel logger = mask_ $ do
+        -- Nothing in here is expected to block or throw. So the mask should be
+        -- sufficient. (TODO what about the getSystemTime?)
+        c <- readIORef (_loggerApproxQueueSize logger)
+        if (c > _loggerMaxQueueSize logger)
+          then incrementCounter (_loggerSkipped logger)
+          else do
+            skipped <- resetCounter (_loggerSkipped logger)
+            now <- getSystemTime
+            when (skipped > 0) $ do
+                writeChan (_loggerQueue logger) $ skippedMessage now skipped
+                incrementCounter (_loggerApproxQueueSize logger)
+            writeChan (_loggerQueue logger) $ LogMessage
+                { _logMessageText = msg
+                , _logMessageLevel = level
+                , _logMessageTime = now
+                , _logMessageTags = _loggerTags logger
+                }
+            incrementCounter (_loggerApproxQueueSize logger)
+
+    | otherwise = return ()
+  where
+    skippedMessage now skipped = LogMessage
+        { _logMessageText = "Skipped " <> T.pack (show skipped) <> " log messages."
+        , _logMessageLevel = Error
+        , _logMessageTime = now
+        , _logMessageTags = _loggerTags logger
+        }
+
+-- -------------------------------------------------------------------------- --
+-- Logging Backend Worker
+
+-- | Initialize a logger context and start a background worker for writing
+-- log messages to stdout.
+--
+withLogger :: LogLevel -> (Logger -> IO a) -> IO a
+withLogger !level inner = do
+    queue <- newChan
+    sizeRef <- newIORef 0
+    skippedRef <- newIORef 0
+    r <- race (backend queue sizeRef) $ inner $ Logger
+        { _loggerQueue = queue
+        , _loggerLevel = level
+        , _loggerTags = []
+        , _loggerApproxQueueSize = sizeRef
+        , _loggerSkipped = skippedRef
+        , _loggerMaxQueueSize = maxLoggerQueueSize
+        }
+    case r of
+        Left () -> error "logger existed unexpectedly"
+        Right a -> return a
+  where
+    -- TODO: implement batch processing
+    --
+    backend queue sizeRef = forever $ do
+        -- No need for masking here. If anything throws in here we exit the
+        -- application anyways.
+        --
+        -- TODO: there is no way to check if Chan is empty. Use a different
+        -- queue that allows to check that sizeRef and the Queue are
+        -- approximately in sync.
+        --
+        msg <- readChan queue
+        decrementCounter sizeRef
+        T.putStrLn $ formatLogMessage msg
+
diff --git a/src/Worker.hs b/src/Worker.hs
new file mode 100644
--- /dev/null
+++ b/src/Worker.hs
@@ -0,0 +1,106 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- |
+-- Module: Worker
+-- Copyright: Copyright © 2020 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+-- The type of a mining worker
+--
+module Worker
+(
+-- * Hash Target
+  Target(..)
+, encodeTarget
+, decodeTarget
+, targetToText16
+
+-- * Mining Work
+, Work(..)
+, encodeWork
+, decodeWork
+
+-- * Nonce
+, Nonce(..)
+
+-- * Mining Worker
+, Worker
+) where
+
+import qualified Data.ByteArray.Encoding as BA
+import Data.Bytes.Get
+import Data.Bytes.Put
+import qualified Data.ByteString.Short as BS
+import Data.Hashable
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import Data.Word
+
+import GHC.Generics
+
+-- -------------------------------------------------------------------------- --
+-- Hash Target
+
+-- | Hash target. A little endian encoded 256 bit (unsigned) word.
+--
+-- Cf. https://github.com/kadena-io/chainweb-node/wiki/Block-Header-Binary-Encoding#work-header-binary-format
+--
+newtype Target = Target { _targetBytes :: BS.ShortByteString }
+    deriving (Show, Eq, Ord, Generic)
+    deriving newtype (Hashable)
+
+decodeTarget :: MonadGet m => m Target
+decodeTarget = Target . BS.toShort <$> getBytes 32
+{-# INLINE decodeTarget #-}
+
+encodeTarget :: MonadPut m => Target -> m ()
+encodeTarget (Target b) = putByteString $ BS.fromShort b
+{-# INLINE encodeTarget #-}
+
+-- | Represent target bytes in hexadecimal base
+--
+targetToText16 :: Target -> T.Text
+targetToText16 = T.decodeUtf8 . BA.convertToBase BA.Base16 . BS.fromShort . _targetBytes
+{-# INLINE targetToText16 #-}
+
+-- -------------------------------------------------------------------------- --
+-- Work
+
+-- | Work bytes. The last 8 bytes are the nonce that is updated by the miner
+-- while solving the work.
+--
+-- Cf. https://github.com/kadena-io/chainweb-node/wiki/Block-Header-Binary-Encoding#work-header-binary-format
+--
+newtype Work = Work BS.ShortByteString
+    deriving (Show, Eq, Ord, Generic)
+    deriving newtype (Hashable)
+
+decodeWork :: MonadGet m => m Work
+decodeWork = Work . BS.toShort <$> getBytes 286
+{-# INLINE decodeWork #-}
+
+encodeWork :: MonadPut m => Work -> m ()
+encodeWork (Work b) = putByteString $ BS.fromShort b
+{-# INLINE encodeWork #-}
+
+-- -------------------------------------------------------------------------- --
+-- Nonce
+
+-- | POW Nonce
+--
+newtype Nonce = Nonce Word64
+    deriving (Show, Eq, Ord, Generic)
+    deriving newtype (Hashable)
+
+-- -------------------------------------------------------------------------- --
+-- Mining Worker
+
+type Worker = Nonce -> Target -> Work -> IO Work
+
diff --git a/src/Worker/CPU.hs b/src/Worker/CPU.hs
new file mode 100644
--- /dev/null
+++ b/src/Worker/CPU.hs
@@ -0,0 +1,181 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- |
+-- Module: Worker.CPU
+-- Copyright: Copyright © 2020 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+-- TODO
+--
+module Worker.CPU
+( cpuWorker
+) where
+
+import Crypto.Hash.IO
+
+import qualified Data.ByteArray as BA
+import Data.Bytes.Signed
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Short as BS
+import Data.Int
+import Data.IORef
+import qualified Data.Memory.Endian as BA
+import qualified Data.Text as T
+import Data.Time.Clock.System
+import Data.Word
+
+import Foreign.Marshal.Alloc (allocaBytes)
+import Foreign.Ptr (Ptr, castPtr)
+import Foreign.Storable (peekElemOff, pokeByteOff)
+
+import System.LogLevel
+
+-- internal modules
+
+import Logger
+import Worker
+
+-- -------------------------------------------------------------------------- --
+-- Utils
+
+int :: Integral a => Num b => a -> b
+int = fromIntegral
+
+getCurrentTimeMicros :: IO Int64
+getCurrentTimeMicros = do
+    MkSystemTime secs nanos <- getSystemTime
+    return $! secs * 1000000 + (int nanos `div` 1000)
+
+-- | `injectNonce` makes low-level assumptions about the byte layout of a
+-- hashed `BlockHeader`. If that layout changes, this functions need to be
+-- updated. The assumption allows us to iterate on new nonces quickly.
+--
+-- Recall: `Nonce` contains a `Word64`, and is thus 8 bytes long.
+--
+-- See also: https://github.com/kadena-io/chainweb-node/wiki/Block-Header-Binary-Encoding
+--
+injectNonce :: Nonce -> Ptr Word8 -> IO ()
+injectNonce (Nonce n) buf = pokeByteOff buf 278 n
+{-# INLINE injectNonce #-}
+
+injectTime :: Int64 -> Ptr Word8 -> IO ()
+injectTime t buf = pokeByteOff buf 8 $ encodeTimeToWord64 t
+{-# INLINE injectTime #-}
+
+encodeTimeToWord64 :: Int64 -> Word64
+encodeTimeToWord64 t = BA.unLE . BA.toLE $ unsigned t
+{-# INLINE encodeTimeToWord64 #-}
+
+-- | `PowHashNat` interprets POW hashes as unsigned 256 bit integral numbers in
+-- little endian encoding, hence we compare against the target from the end of
+-- the bytes first, then move toward the front 8 bytes at a time.
+--
+fastCheckTarget :: Ptr Word64 -> Ptr Word64 -> IO Bool
+fastCheckTarget !trgPtr !powPtr =
+    fastCheckTargetN 3 trgPtr powPtr >>= \case
+        LT -> return False
+        GT -> return True
+        EQ -> fastCheckTargetN 2 trgPtr powPtr >>= \case
+            LT -> return False
+            GT -> return True
+            EQ -> fastCheckTargetN 1 trgPtr powPtr >>= \case
+                LT -> return False
+                GT -> return True
+                EQ -> fastCheckTargetN 0 trgPtr powPtr >>= \case
+                    LT -> return False
+                    GT -> return True
+                    EQ -> return True
+{-# INLINE fastCheckTarget #-}
+
+-- | Recall that `peekElemOff` acts like `drop` for the size of the type in
+-- question. Here, this is `Word64`. Since our hash is treated as a `Word256`,
+-- each @n@ knocks off a `Word64`'s worth of bytes, and there would be 4 such
+-- sections (64 * 4 = 256).
+--
+-- This must never be called for @n >= 4@.
+--
+fastCheckTargetN :: Int -> Ptr Word64 -> Ptr Word64 -> IO Ordering
+fastCheckTargetN n trgPtr powPtr = compare
+    <$> peekElemOff trgPtr n
+    <*> peekElemOff powPtr n
+{-# INLINE fastCheckTargetN #-}
+
+-- -------------------------------------------------------------------------- --
+-- Worker
+
+-- | Single threaded CPU mining worker for Chainweb.
+--
+-- TODO: Check the chainweb version to make sure this function can handle the
+-- respective version.
+--
+cpuWorker
+  :: forall a
+  . HashAlgorithm a
+  => Logger
+  -> Worker
+cpuWorker logger orig@(Nonce o) target work = do
+    nonces <- newIORef 0
+    BA.withByteArray tbytes $ \trgPtr -> do
+        !ctx <- hashMutableInit @a
+        new <- BA.copy hbytes $ \buf ->
+            allocaBytes (powSize :: Int) $ \pow -> do
+
+                -- inner mining loop
+                --
+                let go1 0 n = return (Just n)
+                    go1 !i !n@(Nonce nv) = do
+                        -- Compute POW hash for the nonce
+                        injectNonce n buf
+                        hash ctx buf pow
+
+                        -- check whether the nonce meets the target
+                        fastCheckTarget trgPtr (castPtr pow) >>= \case
+                            True -> Nothing <$ writeIORef nonces (nv - o)
+                            False -> go1 (i - 1) (Nonce $! nv + 1)
+
+                -- outer loop
+                -- Estimates how many iterations of the inner loop run in one second. It runs the inner loop
+                -- that many times and injects an updated creation time in each cycle.
+                let go0 :: Int -> Int64 {- microseconds -} -> Nonce -> IO ()
+                    go0 x t !n = do
+                        injectTime t buf
+                        go1 x n >>= \case
+                            Nothing -> return ()
+                            Just n' -> do
+                                t' <- getCurrentTimeMicros
+                                let td = t' - t
+                                    x' = round @Double (int x * 1000000 / int td) -- target 1 second
+                                go0 x' t' n'
+
+                -- Start outer mining loop
+                t <- getCurrentTimeMicros
+                go0 100000 t orig
+        attempts <- readIORef nonces
+        writeLog logger Info $ "Solved header with " <> T.pack (show attempts) <> " attempts."
+        return (Work $ BS.toShort new)
+  where
+    tbytes = let (Target b) = target in BS.fromShort b
+    hbytes = let (Work b) = work in BS.fromShort b
+
+    bufSize :: Int
+    !bufSize = B.length hbytes
+
+    powSize :: Int
+    !powSize = hashDigestSize @a undefined
+
+    --  Compute POW hash
+    hash :: MutableContext a -> Ptr Word8 -> Ptr Word8 -> IO ()
+    hash ctx buf pow = do
+        hashMutableReset ctx
+        BA.withByteArray ctx $ \ctxPtr -> do
+            hashInternalUpdate @a ctxPtr buf $ fromIntegral bufSize
+            hashInternalFinalize ctxPtr $ castPtr pow
+    {-# INLINE hash #-}
+
diff --git a/src/Worker/External.hs b/src/Worker/External.hs
new file mode 100644
--- /dev/null
+++ b/src/Worker/External.hs
@@ -0,0 +1,131 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- |
+-- Module: Worker.External
+-- Copyright: Copyright © 2020 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+-- TODO
+--
+module Worker.External
+( externalWorker
+) where
+
+import Control.Concurrent.Async
+import Control.Monad.Catch
+
+import qualified Data.ByteArray.Encoding as BA
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Char8 as B8
+import qualified Data.ByteString.Short as BS
+import Data.Char
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import qualified Data.Text.IO as T
+
+import GHC.Generics
+
+import System.Exit
+import System.IO
+import System.LogLevel
+import qualified System.Process as P
+
+-- internal modules
+
+import Logger
+import Worker
+
+-- -------------------------------------------------------------------------- --
+--  Exceptions
+
+newtype ExternalWorkerException = ExternalWorkerException T.Text
+    deriving (Show, Eq, Ord, Generic)
+
+instance Exception ExternalWorkerException
+
+-- -------------------------------------------------------------------------- --
+--
+
+-- | Run an external worker:
+--
+-- External workers is an external operating system process that is provided by
+-- it's file system path.
+--
+-- It is invoked with
+--
+-- * the target as first parameter (hex bytes in little endian encoding),
+--   followed by
+-- * what ever extra arguments are configured by the user.
+--
+-- The work bytes are provided to stdin as raw bytes.
+--
+-- On finding a solution it is expected to print the nonce (encoded in hex in
+-- big endian byte order) and to exit with an exit code of 0.
+--
+-- In case of an exit code other than zero any outputs are logged and discarded.
+--
+externalWorker
+    :: Logger
+    -> String
+        -- ^ worker command
+    -> Worker
+externalWorker logger cmd _nonce target (Work work) =
+    withLogTag logger "Worker" $ \workerLogger ->
+        P.withCreateProcess workerProc (go workerLogger)
+  where
+    targetArg = T.unpack $ targetToText16 target
+
+    workerProc = (P.shell $ cmd <> " " <> targetArg)
+        { P.std_in = P.CreatePipe
+        , P.std_out = P.CreatePipe
+        , P.std_err = P.CreatePipe
+        }
+
+    go workerLogger (Just hstdin) (Just hstdout) (Just hstderr) ph = do
+        B.hPut hstdin $ BS.fromShort work
+        hClose hstdin
+        writeLog logger Info "send command to external worker"
+        writeLog logger Debug $ "external worker command: " <> T.pack (cmd <> " " <> targetArg)
+        withAsync (B.hGetContents hstdout) $ \stdoutThread ->
+            withAsync (errThread workerLogger hstderr) $ \stderrThread -> do
+                code <- P.waitForProcess ph
+                (outbytes, _) <- (,) <$> wait stdoutThread <*> wait stderrThread
+                writeLog logger Info "received nonce for solved work from external worker"
+                if (code /= ExitSuccess)
+                  then
+                    throwM $ ExternalWorkerException $
+                        "External worker failed with code: " <> (T.pack . show) code
+                            -- FIXME: handle non-printable characters
+                  else do
+                    nonceB16 <- case B8.splitWith isSpace outbytes of
+                        [] -> throwM $ ExternalWorkerException $
+                            "expected nonce from miner, got: " <> T.decodeUtf8 outbytes
+                            -- FIXME: handle non-printable characters
+                        (a:_) -> return a
+
+                    -- reverse -- we want little-endian
+                    case BA.convertFromBase BA.Base16 nonceB16 of
+                        Left e -> throwM $ ExternalWorkerException $
+                            "failed to decode nonce bytes: " <> (T.pack . show) e
+                        Right bs
+                            | B.length bs /= 8 ->
+                                throwM $ ExternalWorkerException "process returned short nonce"
+                            | otherwise -> return $ Work $!
+                                BS.toShort $ B.take 278 (BS.fromShort work) <> B.reverse bs
+
+    go _ _ _ _ _ = throwM $ ExternalWorkerException $
+        "impossible: process is opened with CreatePipe in/out/err"
+
+    errThread l hstderr = next
+      where
+        next = hIsEOF hstderr >>= \case
+            True -> return ()
+            False -> do
+                T.hGetLine hstderr >>= writeLog l Debug
+                next
+
diff --git a/src/Worker/Simulation.hs b/src/Worker/Simulation.hs
new file mode 100644
--- /dev/null
+++ b/src/Worker/Simulation.hs
@@ -0,0 +1,85 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- |
+-- Module: Worker.Simulation
+-- Copyright: Copyright © 2020 Kadena LLC.
+-- License: MIT
+-- Maintainer: Lars Kuhtz <lars@kadena.io>
+-- Stability: experimental
+--
+-- Simulation Mining Worker
+--
+-- A fake mining worker that is not actually doing any work. It calculates the
+-- solve time base on the assumed hash power of the worker thread and returns
+-- the work bytes unchanged after that time has passed.
+--
+module Worker.Simulation
+( HashRate(..)
+, defaultHashRate
+, simulationWorker
+) where
+
+import Control.Concurrent (threadDelay)
+
+import Data.Aeson
+import qualified Data.ByteString.Short as BS
+import Data.Hashable
+import qualified Data.Text as T
+
+import GHC.Generics
+
+import System.LogLevel
+import qualified System.Random.MWC as MWC
+import qualified System.Random.MWC.Distributions as MWC
+
+-- internal modules
+
+import Logger
+import Worker
+
+-- -------------------------------------------------------------------------- --
+-- HashRate
+
+newtype HashRate = HashRate Double
+    deriving (Show, Eq, Ord, Generic)
+    deriving newtype (Read, Num, Fractional, Floating, Real, Enum, Hashable, ToJSON, FromJSON)
+
+-- | Default is 1MH
+defaultHashRate :: HashRate
+defaultHashRate = 1_000_000
+
+-- -------------------------------------------------------------------------- --
+-- Simulation Mining Worker
+
+-- | A fake mining worker that is not actually doing any work. It calculates the
+-- solve time base on the assumed hash power of the worker thread and returns
+-- the work bytes unchanged after that time has passed.
+--
+simulationWorker :: Logger -> MWC.GenIO -> HashRate -> Worker
+simulationWorker logger rng rate _nonce (Target targetBytes) work = do
+    delay <- round <$> MWC.exponential scale rng
+    logg Info $ "solve time (microseconds): " <> T.pack (show delay)
+    threadDelay delay
+    return work
+  where
+    logg = writeLog logger
+
+    -- expectedMicros = 1_000_000 * difficulty / rate
+
+    -- MWC.exponential is parameterized by the rate, i.e. 1 / expected_time
+    scale = realToFrac $ realToFrac rate / (difficulty * 1_000_000)
+
+    -- the expected number of attempts for solving a target is the difficulty.
+    difficulty :: Rational
+    difficulty = 2^(256 :: Integer) / targetNum
+
+    -- Target is an little endian encoded (unsigned) 256 bit word.
+    targetNum :: Rational
+    targetNum = foldr (\b a -> fromIntegral b + 256 * a) 0 $ BS.unpack $ targetBytes
+
