{-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses #-} {- Copyright 2016 Joey Hess - - Licensed under the GNU AGPL version 3 or higher. -} module Cost ( module Cost, module Types.Cost ) where import Types.Cost import Data.List import Data.Maybe import Text.Read -- | Cost in seconds, with the type of hardware needed. totalCost :: Cost op -> (Seconds, [UsingHardware]) totalCost (CPUCost s _) = (s, [UsingCPU]) raiseCostPower :: Cost c -> Entropy e -> Cost c raiseCostPower c (Entropy e) = mapCost (* 2^e) c mapCost :: (Rational-> Rational) -> Cost op -> Cost op mapCost f (CPUCost (Seconds n) d) = CPUCost (Seconds (f n)) d type NumCores = Integer showCostMinutes :: NumCores -> Cost op -> String showCostMinutes numcores (CPUCost (Seconds n) (Divisibility d)) | n' < 61 = "1 minute" | otherwise = show (n' / 60) ++ " minutes" where n' :: Double n' = fromRational n / fromIntegral (min numcores d) -- If an operation took n seconds on a number of cores, -- multiply to get the CPUCost, which is for a single core. coreCost :: NumCores -> Seconds -> Divisibility -> Cost op coreCost cores (Seconds n) d = CPUCost (Seconds (fromIntegral cores * n)) d castCost :: Cost a -> Cost b castCost (CPUCost s d) = CPUCost s d -- | CostCalc for a brute force linear search through an entropy space -- in which each step entails paying a cost. -- -- On average, the solution will be found half way through. -- This is equivilant to one bit less of entropy. bruteForceLinearSearch :: Cost step -> CostCalc BruteForceOp t bruteForceLinearSearch stepcost e = castCost stepcost `raiseCostPower` reduceEntropy e 1 -- | Estimate of cost of a brute force attack. estimateBruteforceOf :: Bruteforceable t a => t -> Entropy a -> Cost BruteForceOp estimateBruteforceOf t e = getBruteCostCalc t e data DataCenterPrice = DataCenterPrice { instanceCpuCores :: Integer , instanceCpuCoreMultiplier :: Integer -- ^ If the cores are twice as fast as the commodity hardware -- that keysafe's cost estimates are based on, use 2 to indicate -- this, etc. , instanceCostPerHour :: Cents } -- August 2016 spot pricing: 36 CPU core c4.8xlarge at 33c/hour spotAWS :: DataCenterPrice spotAWS = DataCenterPrice { instanceCpuCores = 36 , instanceCpuCoreMultiplier = 2 , instanceCostPerHour = Cents 33 } -- | Estimate of cost of brute force attack using a datacenter. -- -- Note that this assumes that CPU cores and GPU cores are of equal number, -- which is unlikely to be the case; typically there will be many more -- cores than GPUs. So, this underestimates the price to brute force -- operations which run faster on GPUs. estimateAttackCost :: DataCenterPrice -> Cost BruteForceOp -> Dollars estimateAttackCost dc opcost = centsToDollars $ costcents where (Seconds cpuseconds) = fst (totalCost opcost) cpuyears = cpuseconds / (60*60*24*365) costpercpuyear = Cents $ fromIntegral (instanceCostPerHour dc) * 24 * 365 `div` (instanceCpuCores dc * instanceCpuCoreMultiplier dc) costcents = Cents (ceiling cpuyears) * costpercpuyear newtype Cents = Cents Integer deriving (Num, Integral, Enum, Real, Ord, Eq, Show) -- | USD newtype Dollars = Dollars Integer deriving (Num, Integral, Enum, Real, Ord, Eq) instance Show Dollars where show (Dollars n) = go [ (1000000000000, "trillion") , (1000000000, "billion") , (1000000, "million") , (1000, "thousand") ] where go [] = fmt (show n) go ((d, u):us) | n >= d = let n' = n `div` d in fmt (show n' ++ " " ++ u) | otherwise = go us fmt d = "$" ++ d ++ " (USD)" centsToDollars :: Cents -> Dollars centsToDollars (Cents c) = Dollars (c `div` 100) type Year = Integer -- | Apply Moore's law to show how a cost might vary over time. costOverTime :: Dollars -> Year -> [(Dollars, Year)] costOverTime (Dollars currcost) thisyear = (Dollars currcost, thisyear) : map calc otheryears where otheryears = [thisyear+1, thisyear+5, thisyear+10] calc y = let monthdelta = (fromIntegral ((y * 12) - (thisyear * 12))) :: Double cost = floor $ fromIntegral currcost / 2 ** (monthdelta / 18) in (Dollars cost, y) costOverTimeTable :: Dollars -> Year -> [String] costOverTimeTable cost thisyear = go [] thisyear $ costOverTime cost thisyear where go t _ [] = reverse t go t yprev ((c, y):ys) = let s = " in " ++ show y ++ ": " ++ show c in if yprev < y - 1 then go (s:" ...":t) y ys else go (s:t) y ys -- Number of physical cores. This is not the same as -- getNumProcessors, which includes hyper-threading. getNumCores :: IO (Maybe NumCores) getNumCores = getmax . mapMaybe parse . lines <$> readFile "/proc/cpuinfo" where getmax [] = Nothing getmax l = Just $ maximum l + 1 -- add 1 because /proc/cpuinfo counts from 0 parse l | "core id" `isPrefixOf` l = readMaybe $ drop 1 $ dropWhile (/= ':') l | otherwise = Nothing