packages feed

blockchain-0.0.0.1: test/Integration/Mining.hs

module Integration.Mining
    ( mineTestChain
    ) where

import qualified Control.Concurrent.Async          as Async
import qualified Control.Concurrent.MVar           as MVar
import           Control.Monad                     (forM, void)
import qualified Data.Aeson                        as Aeson
import qualified Data.ByteString.Lazy              as Lazy
import qualified Data.List.NonEmpty                as NonEmpty
import           Data.Monoid                       ((<>))
import qualified Data.Time.Clock                   as Time

import qualified Data.Blockchain.Core.Blockchain   as B
import qualified Data.Blockchain.Core.Crypto       as Crypto
import qualified Data.Blockchain.Core.Types        as B
import qualified Data.Blockchain.Mining.Block      as Mining
import qualified Data.Blockchain.Mining.Blockchain as Mining

data MinerSpec = MinerSpec
    { mid               :: Int
    , pubKey            :: Crypto.PublicKey
    , receiveBlock      :: IO B.Block
    , sendBlock         :: B.Block -> IO ()
    , persistBlockchain :: B.Blockchain B.Validated -> IO ()
    }


mineTestChain :: Int -> IO ()
mineTestChain miners = runTestMultiMiner miners config persist
  where
    persist i = writeJSON $ "data/test_blockchain_" <> show i <> ".json"
    config = B.defaultConfig
        { B.initialMiningReward           = 500
        , B.difficultyRecalculationHeight = 10
        , B.miningRewardHalvingHeight     = 20
        }


runTestMultiMiner
    :: Int -> B.BlockchainConfig
    -> (Int -> B.Blockchain B.Validated -> IO ()) -> IO ()
runTestMultiMiner numMiners config persist = do
    putStrLn "Creating blockchain and mining genesis block"
    blockchain <- Mining.createBlockchain config

    let idList = [1..numMiners]

    vars <- mapM (const MVar.newEmptyMVar) idList

    let sends    = MVar.putMVar <$> vars
        receives = MVar.takeMVar <$> (tail vars <> pure (head vars))
        specs    = zip3 sends receives idList

    miners <- forM specs $ \(sendBlock, receiveBlock, mid) -> do
        let persistBlockchain = persist mid
        (Crypto.KeyPair pubKey _privKey) <- Crypto.generate
        return MinerSpec{..}

    asyncs <- mapM (Async.async . runMinerFakeNetwork blockchain) miners
    void (Async.waitAny asyncs)


runMinerFakeNetwork :: B.Blockchain B.Validated -> MinerSpec -> IO ()
runMinerFakeNetwork blockchain spec@(MinerSpec mid _pubKey receive send persist) = do
    miningThread <- Async.async $ do
        block <- runMining spec blockchain
        log' "Found block, broadcasting to other miners"
        send block

        case B.addBlock block blockchain of
            Right blockchain' -> return blockchain'
            Left  e           -> error $ "Mined invalid block: " <> show e

    receiveBlockThread <- Async.async $ do
        block <- receive

        case B.addBlock block blockchain of
            Right blockchain' -> do
                log' "Received valid block. Stopping mining on current block and re-broadcasting."
                send block
                return blockchain'
            Left e -> log' ("Received invalid block - unable to insert block into chain. Reason: " <> show e) >> return blockchain

    blockchain' <- either id id <$> Async.waitEitherCancel miningThread receiveBlockThread

    persist blockchain'
    runMinerFakeNetwork blockchain' spec
  where
    log' s = do
        timestamp <- Time.getCurrentTime
        putStrLn $ pad 30 (show timestamp) <> " [miner_" <> show mid <> "] " <> s

runMining :: MinerSpec -> B.Blockchain B.Validated -> IO B.Block
runMining (MinerSpec mid pubKey _ _ _) chain = do
    let mainChain   = NonEmpty.toList $ B.longestChain chain
        chainLength = length mainChain
        config      = B.blockchainConfig chain

    log' $ "Mining block #" <> show chainLength
    -- TODO: bad interface to find diff. Exported interface should just take blockchain.
    log' $ "Target difficulty: " <> show (B.targetDifficulty config mainChain)

    start <- Time.getCurrentTime
    block <- throwLeft <$> Mining.mineEmptyBlock pubKey chain
    end   <- Time.getCurrentTime

    let h = Crypto.hash (B.blockHeader block)

    log' "Found next block"
    log' $ "  Hash: " <> show h
    log' $ "  Elapsed time: " <> show (Time.diffUTCTime end start)

    return block
  where
    log' s = do
        timestamp <- Time.getCurrentTime
        putStrLn $ pad 30 (show timestamp) <> " [miner_" <> show mid <> "] " <> s

throwLeft :: Show a => Either a b -> b
throwLeft = either (error . show) id

pad :: Int -> String -> String
pad x str = str <> replicate (x - length str) ' '


writeJSON :: Aeson.ToJSON a => FilePath -> a -> IO ()
writeJSON path = Lazy.writeFile path . Aeson.encode