summaryrefslogtreecommitdiffhomepage
path: root/Cost.hs
blob: a18a0889d1def6a06d88e3dc392ecb032a856054 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
{-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses #-}

{- Copyright 2016 Joey Hess <id@joeyh.name>
 -
 - 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