/
Shelley.hs
354 lines (302 loc) · 13.2 KB
/
Shelley.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
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Cardano.Node.Protocol.Shelley
( mkSomeConsensusProtocolShelley
-- * Errors
, ShelleyProtocolInstantiationError(..)
, GenesisReadError(..)
, GenesisValidationError(..)
, PraosLeaderCredentialsError(..)
-- * Reusable parts
, readGenesis
, readGenesisAny
, readLeaderCredentials
, genesisHashToPraosNonce
, validateGenesis
) where
import Cardano.Prelude (ConvertText (..))
import Control.Exception (IOException)
import Control.Monad.Except (ExceptT, MonadError (..))
import qualified Cardano.Api as Api
import qualified Data.Aeson as Aeson
import qualified Data.ByteString as BS
import qualified Data.Text as T
import Control.Monad.Trans.Except.Extra (firstExceptT, handleIOExceptT, hoistEither, left,
newExceptT)
import qualified Cardano.Crypto.Hash.Class as Crypto
import Cardano.Ledger.Crypto (StandardCrypto)
import Cardano.Ledger.Keys (coerceKeyRole)
import qualified Ouroboros.Consensus.Cardano as Consensus
import qualified Ouroboros.Consensus.Mempool.Capacity as TxLimits
import Ouroboros.Consensus.Protocol.Praos.Common (PraosCanBeLeader (..))
import Ouroboros.Consensus.Shelley.Eras (StandardShelley)
import Ouroboros.Consensus.Shelley.Node (Nonce (..), ProtocolParamsShelley (..),
ProtocolParamsShelleyBased (..), ShelleyLeaderCredentials (..))
import Cardano.Ledger.BaseTypes (ProtVer (..))
import qualified Cardano.Ledger.Shelley.Genesis as Shelley
import Cardano.Api.DeserialiseAnyOf (readFileTextEnvelope')
import Cardano.Api.Orphans ()
import Cardano.Api.Shelley hiding (FileError)
import Cardano.Node.Types
import Cardano.Tracing.OrphanInstances.HardFork ()
import Cardano.Tracing.OrphanInstances.Shelley ()
import Cardano.Node.Tracing.Era.HardFork ()
import Cardano.Node.Tracing.Era.Shelley ()
import Cardano.Node.Tracing.Formatting ()
import Cardano.Node.Tracing.Tracers.ChainDB ()
import Cardano.Node.Protocol.Types
------------------------------------------------------------------------------
-- Shelley protocol
--
-- | Make 'SomeConsensusProtocol' using the Shelley instance.
--
-- This lets us handle multiple protocols in a generic way.
--
-- This also serves a purpose as a sanity check that we have all the necessary
-- type class instances available.
mkSomeConsensusProtocolShelley
:: NodeShelleyProtocolConfiguration
-> Maybe ProtocolFilepaths
-> ExceptT ShelleyProtocolInstantiationError IO SomeConsensusProtocol
mkSomeConsensusProtocolShelley NodeShelleyProtocolConfiguration {
npcShelleyGenesisFile,
npcShelleyGenesisFileHash
}
files = do
(genesis, genesisHash) <- firstExceptT GenesisReadError $
readGenesis npcShelleyGenesisFile
npcShelleyGenesisFileHash
firstExceptT GenesisValidationError $ validateGenesis genesis
leaderCredentials <- firstExceptT PraosLeaderCredentialsError $
readLeaderCredentials files
return $ SomeConsensusProtocol Api.ShelleyBlockType $ Api.ProtocolInfoArgsShelley
Consensus.ProtocolParamsShelleyBased {
shelleyBasedGenesis = genesis,
shelleyBasedInitialNonce = genesisHashToPraosNonce genesisHash,
shelleyBasedLeaderCredentials =
leaderCredentials
}
Consensus.ProtocolParamsShelley {
shelleyProtVer =
ProtVer 2 0,
shelleyMaxTxCapacityOverrides =
TxLimits.mkOverrides TxLimits.noOverridesMeasure
}
genesisHashToPraosNonce :: GenesisHash -> Nonce
genesisHashToPraosNonce (GenesisHash h) = Nonce (Crypto.castHash h)
readGenesis :: GenesisFile
-> Maybe GenesisHash
-> ExceptT GenesisReadError IO
(ShelleyGenesis StandardShelley, GenesisHash)
readGenesis = readGenesisAny
readGenesisAny :: FromJSON genesis
=> GenesisFile
-> Maybe GenesisHash
-> ExceptT GenesisReadError IO (genesis, GenesisHash)
readGenesisAny (GenesisFile file) mbExpectedGenesisHash = do
content <- handleIOExceptT (GenesisReadFileError file) $
BS.readFile file
let genesisHash = GenesisHash (Crypto.hashWith id content)
checkExpectedGenesisHash genesisHash
genesis <- firstExceptT (GenesisDecodeError file) $ hoistEither $
Aeson.eitherDecodeStrict' content
return (genesis, genesisHash)
where
checkExpectedGenesisHash :: GenesisHash
-> ExceptT GenesisReadError IO ()
checkExpectedGenesisHash actual =
case mbExpectedGenesisHash of
Just expected | actual /= expected
-> throwError (GenesisHashMismatch actual expected)
_ -> return ()
validateGenesis :: ShelleyGenesis StandardShelley
-> ExceptT GenesisValidationError IO ()
validateGenesis genesis =
firstExceptT GenesisValidationErrors . hoistEither $
Shelley.validateGenesis genesis
readLeaderCredentials
:: Maybe ProtocolFilepaths
-> ExceptT PraosLeaderCredentialsError IO [ShelleyLeaderCredentials StandardCrypto]
readLeaderCredentials Nothing = return []
readLeaderCredentials (Just pfp) =
-- The set of credentials is a sum total of what comes from the CLI,
-- as well as what's in the bulk credentials file.
(<>) <$> readLeaderCredentialsSingleton pfp
<*> readLeaderCredentialsBulk pfp
readLeaderCredentialsSingleton ::
ProtocolFilepaths ->
ExceptT PraosLeaderCredentialsError IO
[ShelleyLeaderCredentials StandardCrypto]
-- It's OK to supply none of the files on the CLI
readLeaderCredentialsSingleton
ProtocolFilepaths
{ shelleyCertFile = Nothing,
shelleyVRFFile = Nothing,
shelleyKESFile = Nothing
} = pure []
-- Or to supply all of the files
readLeaderCredentialsSingleton
ProtocolFilepaths { shelleyCertFile = Just opCertFile,
shelleyVRFFile = Just vrfFile,
shelleyKESFile = Just kesFile
} = do
vrfSKey <-
firstExceptT FileError (newExceptT $ readFileTextEnvelope' (AsSigningKey AsVrfKey) vrfFile)
(opCert, kesSKey) <- opCertKesKeyCheck kesFile opCertFile
return [mkPraosLeaderCredentials opCert vrfSKey kesSKey]
-- But not OK to supply some of the files without the others.
readLeaderCredentialsSingleton ProtocolFilepaths {shelleyCertFile = Nothing} =
left OCertNotSpecified
readLeaderCredentialsSingleton ProtocolFilepaths {shelleyVRFFile = Nothing} =
left VRFKeyNotSpecified
readLeaderCredentialsSingleton ProtocolFilepaths {shelleyKESFile = Nothing} =
left KESKeyNotSpecified
opCertKesKeyCheck
:: FilePath
-- ^ KES key
-> FilePath
-- ^ Operational certificate
-> ExceptT PraosLeaderCredentialsError IO (OperationalCertificate, SigningKey KesKey)
opCertKesKeyCheck kesFile certFile = do
opCert <-
firstExceptT FileError (newExceptT $ readFileTextEnvelope' AsOperationalCertificate certFile)
kesSKey <-
firstExceptT FileError (newExceptT $ readFileTextEnvelope' (AsSigningKey AsKesKey) kesFile)
let opCertSpecifiedKesKeyhash = verificationKeyHash $ getHotKey opCert
suppliedKesKeyHash = verificationKeyHash $ getVerificationKey kesSKey
-- Specified KES key in operational certificate should match the one
-- supplied to the node.
if suppliedKesKeyHash /= opCertSpecifiedKesKeyhash
then left $ MismatchedKesKey kesFile certFile
else return (opCert, kesSKey)
data ShelleyCredentials
= ShelleyCredentials
{ scCert :: (TextEnvelope, FilePath)
, scVrf :: (TextEnvelope, FilePath)
, scKes :: (TextEnvelope, FilePath)
}
readLeaderCredentialsBulk
:: ProtocolFilepaths
-> ExceptT PraosLeaderCredentialsError IO [ShelleyLeaderCredentials StandardCrypto]
readLeaderCredentialsBulk ProtocolFilepaths { shelleyBulkCredsFile = mfp } =
mapM parseShelleyCredentials =<< readBulkFile mfp
where
parseShelleyCredentials
:: ShelleyCredentials
-> ExceptT PraosLeaderCredentialsError IO (ShelleyLeaderCredentials StandardCrypto)
parseShelleyCredentials ShelleyCredentials { scCert, scVrf, scKes } = do
mkPraosLeaderCredentials
<$> parseEnvelope AsOperationalCertificate scCert
<*> parseEnvelope (AsSigningKey AsVrfKey) scVrf
<*> parseEnvelope (AsSigningKey AsKesKey) scKes
readBulkFile
:: Maybe FilePath
-> ExceptT PraosLeaderCredentialsError IO [ShelleyCredentials]
readBulkFile Nothing = pure []
readBulkFile (Just fp) = do
content <- handleIOExceptT (CredentialsReadError fp) $
BS.readFile fp
envelopes <- firstExceptT (EnvelopeParseError fp) $ hoistEither $
Aeson.eitherDecodeStrict' content
pure $ uncurry mkCredentials <$> zip [0..] envelopes
where
mkCredentials :: Int -> (TextEnvelope, TextEnvelope, TextEnvelope)
-> ShelleyCredentials
mkCredentials ix (teCert, teVrf, teKes) =
let loc ty = fp <> "." <> show ix <> ty
in ShelleyCredentials (teCert, loc "cert")
(teVrf, loc "vrf")
(teKes, loc "kes")
mkPraosLeaderCredentials ::
OperationalCertificate
-> SigningKey VrfKey
-> SigningKey KesKey
-> ShelleyLeaderCredentials StandardCrypto
mkPraosLeaderCredentials
(OperationalCertificate opcert (StakePoolVerificationKey vkey))
(VrfSigningKey vrfKey)
(KesSigningKey kesKey) =
ShelleyLeaderCredentials
{ shelleyLeaderCredentialsCanBeLeader =
PraosCanBeLeader {
praosCanBeLeaderOpCert = opcert,
praosCanBeLeaderColdVerKey = coerceKeyRole vkey,
praosCanBeLeaderSignKeyVRF = vrfKey
},
shelleyLeaderCredentialsInitSignKey = kesKey,
shelleyLeaderCredentialsLabel = "Shelley"
}
parseEnvelope ::
HasTextEnvelope a
=> AsType a
-> (TextEnvelope, String)
-> ExceptT PraosLeaderCredentialsError IO a
parseEnvelope as (te, loc) =
firstExceptT (FileError . Api.FileError loc . InputTextEnvelopeError) . hoistEither $
deserialiseFromTextEnvelope as te
------------------------------------------------------------------------------
-- Errors
--
data ShelleyProtocolInstantiationError =
GenesisReadError GenesisReadError
| GenesisValidationError GenesisValidationError
| PraosLeaderCredentialsError PraosLeaderCredentialsError
deriving Show
instance Error ShelleyProtocolInstantiationError where
displayError (GenesisReadError err) = displayError err
displayError (GenesisValidationError err) = displayError err
displayError (PraosLeaderCredentialsError err) = displayError err
data GenesisReadError =
GenesisReadFileError !FilePath !IOException
| GenesisHashMismatch !GenesisHash !GenesisHash -- actual, expected
| GenesisDecodeError !FilePath !String
deriving Show
instance Error GenesisReadError where
displayError (GenesisReadFileError fp err) =
"There was an error reading the genesis file: "
<> toS fp <> " Error: " <> show err
displayError (GenesisHashMismatch actual expected) =
"Wrong genesis file: the actual hash is " <> show actual
<> ", but the expected genesis hash given in the node "
<> "configuration file is " <> show expected
displayError (GenesisDecodeError fp err) =
"There was an error parsing the genesis file: "
<> toS fp <> " Error: " <> show err
newtype GenesisValidationError = GenesisValidationErrors [Shelley.ValidationErr]
deriving Show
instance Error GenesisValidationError where
displayError (GenesisValidationErrors vErrs) =
T.unpack (T.unlines (map Shelley.describeValidationErr vErrs))
data PraosLeaderCredentialsError =
CredentialsReadError !FilePath !IOException
| EnvelopeParseError !FilePath !String
| FileError !(Api.FileError InputDecodeError)
--TODO: pick a less generic constructor than FileError
| OCertNotSpecified
| VRFKeyNotSpecified
| KESKeyNotSpecified
| MismatchedKesKey
FilePath
-- KES signing key
FilePath
-- Operational certificate
deriving Show
instance Error PraosLeaderCredentialsError where
displayError (CredentialsReadError fp err) =
"There was an error reading a credentials file: "
<> toS fp <> " Error: " <> show err
displayError (EnvelopeParseError fp err) =
"There was an error parsing a credentials envelope: "
<> toS fp <> " Error: " <> show err
displayError (FileError fileErr) = displayError fileErr
displayError (MismatchedKesKey kesFp certFp) =
"The KES key provided at: " <> show kesFp
<> " does not match the KES key specified in the operational certificate at: " <> show certFp
displayError OCertNotSpecified = missingFlagMessage "shelley-operational-certificate"
displayError VRFKeyNotSpecified = missingFlagMessage "shelley-vrf-key"
displayError KESKeyNotSpecified = missingFlagMessage "shelley-kes-key"
missingFlagMessage :: String -> String
missingFlagMessage flag =
"To create blocks, the --" <> flag <> " must also be specified"