/
Model.hs
359 lines (317 loc) · 11 KB
/
Model.hs
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Copyright: © 2018-2020 IOHK
-- License: Apache-2.0
--
-- An implementation of the production pool database using only pure functions.
--
-- These functions and types model the behaviour of the SQLite database backend,
-- and are used for QuickCheck state machine testing, and the MVar database
-- backend.
module Cardano.Pool.DB.Model
(
-- * Model Types
PoolDatabase (..)
, emptyPoolDatabase
-- * Model Operation Types
, ModelPoolOp
, PoolErr (..)
-- * Model pool database functions
, mCleanPoolProduction
, mPutPoolProduction
, mReadPoolProduction
, mReadTotalProduction
, mPutStakeDistribution
, mReadStakeDistribution
, mReadPoolMetadata
, mPutPoolRegistration
, mReadPoolRegistration
, mPutPoolRetirement
, mReadPoolRetirement
, mUnfetchedPoolMetadataRefs
, mPutFetchAttempt
, mPutPoolMetadata
, mListRegisteredPools
, mReadSystemSeed
, mRollbackTo
, mReadCursor
) where
import Prelude
import Cardano.Pool.DB
( CertificatePublicationTime )
import Cardano.Wallet.Primitive.Types
( BlockHeader (..)
, EpochNo (..)
, PoolId
, PoolOwner (..)
, PoolRegistrationCertificate (..)
, PoolRetirementCertificate (..)
, SlotId (..)
, StakePoolMetadata
, StakePoolMetadataHash
, StakePoolMetadataUrl
)
import Data.Bifunctor
( first )
import Data.Foldable
( fold )
import Data.Map.Strict
( Map )
import Data.Ord
( Down (..) )
import Data.Quantity
( Quantity (..) )
import Data.Word
( Word64 )
import GHC.Generics
( Generic )
import System.Random
( StdGen, newStdGen )
import qualified Data.List as L
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
{-------------------------------------------------------------------------------
Model Database Types
-------------------------------------------------------------------------------}
data PoolDatabase = PoolDatabase
{ pools :: !(Map PoolId [BlockHeader])
-- ^ Information of what blocks were produced by which stake pools
, distributions :: !(Map EpochNo [(PoolId, Quantity "lovelace" Word64)])
-- ^ Store known stake distributions for epochs
, owners :: !(Map PoolId [PoolOwner])
-- ^ Mapping between pool ids and owners
, registrations
:: !(Map (CertificatePublicationTime, PoolId) PoolRegistrationCertificate)
-- ^ On-chain registrations associated with pools
, retirements
:: !(Map (CertificatePublicationTime, PoolId) PoolRetirementCertificate)
-- ^ On-chain retirements associated with pools
, metadata :: !(Map StakePoolMetadataHash StakePoolMetadata)
-- ^ Off-chain metadata cached in database
, fetchAttempts :: !(Map (StakePoolMetadataUrl, StakePoolMetadataHash) Int)
-- ^ Metadata (failed) fetch attempts
, seed :: !SystemSeed
-- ^ Store an arbitrary random generator seed
} deriving (Generic, Show, Eq)
data SystemSeed
= SystemSeed StdGen
| NotSeededYet
deriving (Generic, Show)
-- | Shallow / weak equality on seeds.
instance Eq SystemSeed where
(SystemSeed _) == (SystemSeed _) = True
NotSeededYet == NotSeededYet = True
_ == _ = False
-- | Produces an empty model pool production database.
emptyPoolDatabase :: PoolDatabase
emptyPoolDatabase =
PoolDatabase mempty mempty mempty mempty mempty mempty mempty NotSeededYet
{-------------------------------------------------------------------------------
Model Operation Types
-------------------------------------------------------------------------------}
type ModelPoolOp a = PoolDatabase -> (Either PoolErr a, PoolDatabase)
newtype PoolErr = PointAlreadyExists BlockHeader
deriving (Show, Eq)
{-------------------------------------------------------------------------------
Model Pool Database Functions
-------------------------------------------------------------------------------}
mCleanPoolProduction :: ModelPoolOp ()
mCleanPoolProduction _ = (Right (), emptyPoolDatabase)
mPutPoolProduction :: BlockHeader -> PoolId -> ModelPoolOp ()
mPutPoolProduction point poolId db@PoolDatabase{pools} =
let alter slot = \case
Nothing -> Just [slot]
Just slots -> Just $ sortDesc (slot:slots)
sortDesc = L.sortBy (flip compare)
in if point `elem` concat (Map.elems pools) then
(Left (PointAlreadyExists point), db)
else
( Right ()
, db { pools = Map.alter (alter point) poolId pools }
)
mReadPoolProduction :: EpochNo -> ModelPoolOp (Map PoolId [BlockHeader])
mReadPoolProduction epoch db@PoolDatabase{pools} =
let updateSlots e = Map.map (filter (\x -> epochNumber (slotId x) == e))
updatePools = Map.filter (not . L.null)
in (Right (updatePools $ (updateSlots epoch) pools), db)
mReadTotalProduction :: ModelPoolOp (Map PoolId (Quantity "block" Word64))
mReadTotalProduction db@PoolDatabase{pools} =
( Right (Map.map (Quantity . fromIntegral . length) pools), db )
mPutStakeDistribution
:: EpochNo
-> [(PoolId, Quantity "lovelace" Word64)]
-> ModelPoolOp ()
mPutStakeDistribution epoch distrib db@PoolDatabase{distributions} =
( Right ()
, db { distributions = Map.insert epoch distrib distributions }
)
mReadStakeDistribution
:: EpochNo
-> ModelPoolOp [(PoolId, Quantity "lovelace" Word64)]
mReadStakeDistribution epoch db@PoolDatabase{distributions} =
( Right $ Map.findWithDefault mempty epoch distributions
, db
)
mPutPoolRegistration
:: CertificatePublicationTime
-> PoolRegistrationCertificate
-> ModelPoolOp ()
mPutPoolRegistration sp registration db =
( Right ()
, db { owners = Map.insert poolId poolOwners owners
, registrations = Map.insert (sp, poolId) registration registrations
}
)
where
PoolDatabase {owners, registrations} = db
PoolRegistrationCertificate {poolId, poolOwners} = registration
mReadPoolRegistration
:: PoolId
-> ModelPoolOp
(Maybe (CertificatePublicationTime, PoolRegistrationCertificate))
mReadPoolRegistration poolId db =
( Right
$ fmap (first fst)
$ Map.lookupMax
$ Map.filterWithKey (only poolId) registrations
, db
)
where
PoolDatabase {registrations} = db
only k (_, k') _ = k == k'
mPutPoolRetirement
:: CertificatePublicationTime
-> PoolRetirementCertificate
-> ModelPoolOp ()
mPutPoolRetirement sp retirement db =
( Right ()
, db { retirements = Map.insert (sp, poolId) retirement retirements }
)
where
PoolDatabase {retirements} = db
PoolRetirementCertificate poolId _retiredIn = retirement
mReadPoolRetirement
:: PoolId
-> ModelPoolOp
(Maybe (CertificatePublicationTime, PoolRetirementCertificate))
mReadPoolRetirement poolId db =
( Right
$ fmap (first fst)
$ Map.lookupMax
$ Map.filterWithKey (only poolId) retirements
, db
)
where
PoolDatabase {retirements} = db
only k (_, k') _ = k == k'
mListRegisteredPools :: PoolDatabase -> ([PoolId], PoolDatabase)
mListRegisteredPools db@PoolDatabase{registrations} =
( snd <$> Map.keys registrations, db )
mUnfetchedPoolMetadataRefs
:: Int
-> ModelPoolOp [(StakePoolMetadataUrl, StakePoolMetadataHash)]
mUnfetchedPoolMetadataRefs n db@PoolDatabase{registrations,metadata} =
( Right (toTuple <$> take n (Map.elems unfetched))
, db
)
where
unfetched
:: Map (CertificatePublicationTime, PoolId) PoolRegistrationCertificate
unfetched = flip Map.filter registrations $ \r ->
case poolMetadata r of
Nothing -> False
Just (_, hash) -> hash `notElem` Map.keys metadata
toTuple
:: PoolRegistrationCertificate
-> (StakePoolMetadataUrl, StakePoolMetadataHash)
toTuple PoolRegistrationCertificate{poolMetadata} =
(metadataUrl, metadataHash)
where
Just (metadataUrl, metadataHash) = poolMetadata
mPutFetchAttempt
:: (StakePoolMetadataUrl, StakePoolMetadataHash)
-> ModelPoolOp ()
mPutFetchAttempt key db@PoolDatabase{fetchAttempts} =
( Right ()
, db { fetchAttempts = Map.insertWith (+) key 1 fetchAttempts }
)
mPutPoolMetadata
:: StakePoolMetadataHash
-> StakePoolMetadata
-> ModelPoolOp ()
mPutPoolMetadata hash meta db@PoolDatabase{metadata,fetchAttempts} =
( Right ()
, db { metadata = Map.insert hash meta metadata
, fetchAttempts = Map.filterWithKey (\k _ -> snd k /= hash) fetchAttempts
}
)
mReadPoolMetadata
:: ModelPoolOp (Map StakePoolMetadataHash StakePoolMetadata)
mReadPoolMetadata db@PoolDatabase{metadata} = (Right metadata, db)
mReadSystemSeed
:: PoolDatabase
-> IO (StdGen, PoolDatabase)
mReadSystemSeed db@PoolDatabase{seed} =
case seed of
NotSeededYet -> do
seed' <- newStdGen
return ( seed', db { seed = SystemSeed seed' })
SystemSeed s ->
return ( s, db )
mReadCursor :: Int -> ModelPoolOp [BlockHeader]
mReadCursor k db@PoolDatabase{pools} =
let allHeaders = fold pools
sortDesc = L.sortOn (Down . slotId)
limit = take k
in (Right $ reverse $ limit $ sortDesc allHeaders, db)
mRollbackTo :: SlotId -> ModelPoolOp ()
mRollbackTo point PoolDatabase { pools
, distributions
, owners
, registrations
, retirements
, metadata
, seed
, fetchAttempts
} =
let
registrations' =
Map.mapMaybeWithKey (discardBy id . fst . fst) registrations
retirements' =
Map.mapMaybeWithKey (discardBy id . fst . fst) retirements
owners' = Map.restrictKeys owners
$ Set.fromList
$ snd <$> Map.keys registrations'
in
( Right ()
, PoolDatabase
{ pools = updatePools $ updateSlots pools
, distributions =
Map.mapMaybeWithKey (discardBy epochNumber) distributions
, owners = owners'
, registrations = registrations'
, retirements = retirements'
, metadata
, fetchAttempts
, seed
}
)
where
updateSlots = Map.map (filter ((<= point) . slotId))
updatePools = Map.filter (not . L.null)
discardBy :: Ord point => (SlotId -> point) -> point -> a -> Maybe a
discardBy get point' v
| point' <= get point = Just v
| otherwise = Nothing