module Data.Blockchain.Types.BlockchainConfig ( BlockchainConfig(..) , defaultConfig , targetReward , targetDifficulty ) where import Control.Monad (when) import qualified Data.Aeson as Aeson import qualified Data.Time.Clock as Time import qualified Data.Word as Word import qualified GHC.Generics as Generic import Data.Blockchain.Types.Block import Data.Blockchain.Types.Difficulty import Data.Blockchain.Types.Hex data BlockchainConfig = BlockchainConfig { initialDifficulty :: Difficulty -- Maximum hash - difficulties will be calculated using this value , difficulty1Target :: Hex256 , targetSecondsPerBlock :: Word.Word , difficultyRecalculationHeight :: Word.Word , initialMiningReward :: Word.Word -- Defines num blocks when reward is halved -- `0` means reward never changes , miningRewardHalvingHeight :: Word.Word } deriving (Generic.Generic, Eq, Show) instance Aeson.FromJSON BlockchainConfig instance Aeson.ToJSON BlockchainConfig -- | A reasonable default config to use for testing. Mines blocks quickly and changes difficulty and rewards frequently. -- Note: reward will go to zero after 1100 blocks, which will take about 180 minutes of mining. -- -- @ -- defaultConfig :: BlockchainConfig -- defaultConfig = BlockchainConfig -- { initialDifficulty = Difficulty 1 -- , difficulty1Target = hex256LeadingZeros 4 -- , targetSecondsPerBlock = 10 -- , difficultyRecalculationHeight = 50 -- , initialMiningReward = 1024 -- , miningRewardHalvingHeight = 100 -- } -- @ -- defaultConfig :: BlockchainConfig defaultConfig = BlockchainConfig { initialDifficulty = Difficulty 1 , difficulty1Target = hex256LeadingZeros 4 , targetSecondsPerBlock = 10 , difficultyRecalculationHeight = 50 , initialMiningReward = 1024 , miningRewardHalvingHeight = 100 } -- | Calculates the target reward for a blockchain. Uses the longest chain. targetReward :: BlockchainConfig -> Word.Word -> Word.Word targetReward config height = either id id $ do let initialReward = initialMiningReward config halveHeight = miningRewardHalvingHeight config when (halveHeight == 0) $ Left initialReward let numHalves = height `div` halveHeight -- 2^64 is greater than maxBound :: Word -- And any word halved 64 times will be zero when (numHalves >= 64) $ Left 0 return $ initialReward `div` (2 ^ numHalves) -- TODO: array of blocks hold no assurances of expected invariants -- for example block1 could be created more recently than blockN -- should create a `SingleChain` wrapper -- TODO: take in entire blockchain -- | Calculates the target difficulty for a blockchain. Uses the longest chain. targetDifficulty :: BlockchainConfig -> [Block] -> Difficulty targetDifficulty config [] = initialDifficulty config targetDifficulty config _ | difficultyRecalculationHeight config == 0 = initialDifficulty config targetDifficulty config _ | difficultyRecalculationHeight config == 1 = initialDifficulty config targetDifficulty config _ | targetSecondsPerBlock config == 0 = initialDifficulty config -- targetDifficulty config blocks | length blocks == 1 = initialDifficulty config -- super messy... targetDifficulty config blocks = case length blocks `mod` fromIntegral recalcHeight of -- Note: this includes the genesis block, is that correct? 0 -> let recentBlocks = take (fromIntegral recalcHeight) (reverse blocks) -- TODO: (drop (length blocks - recalcHeight)) lastBlock = head recentBlocks firstBlock = last recentBlocks -- TODO: get rid of `abs`, move invariant into types diffTime = abs $ Time.diffUTCTime (blockTime lastBlock) (blockTime firstBlock) avgSolveTime = realToFrac diffTime / fromIntegral recalcHeight solveRate = fromIntegral (targetSecondsPerBlock config) / avgSolveTime lastDifficulty = difficulty (blockHeader lastBlock) nextDifficulty = Difficulty $ round $ solveRate * toRational lastDifficulty in nextDifficulty _ -> difficulty $ blockHeader $ last blocks where recalcHeight = difficultyRecalculationHeight config blockTime = time . blockHeader