diff --git a/ouroboros-consensus-cardano/app/GenHeader/Parsers.hs b/ouroboros-consensus-cardano/app/GenHeader/Parsers.hs new file mode 100644 index 0000000000..e415ff96de --- /dev/null +++ b/ouroboros-consensus-cardano/app/GenHeader/Parsers.hs @@ -0,0 +1,46 @@ +module GenHeader.Parsers where + +import Cardano.Tools.Headers (Options (..)) +import Data.Version (showVersion) +import Options.Applicative (Parser, ParserInfo, auto, command, + execParser, help, helper, hsubparser, info, long, metavar, + option, progDesc, short, (<**>)) +import Paths_ouroboros_consensus_cardano (version) + +parseOptions :: IO Options +parseOptions = execParser argsParser + +argsParser :: ParserInfo Options +argsParser = + info + (optionsParser <**> helper) + ( progDesc $ + unlines + [ "gen-header - A utility to generate valid and invalid Praos headers for testing purpose" + , "version: " <> showVersion version + ] + ) + +optionsParser :: Parser Options +optionsParser = + hsubparser + ( command "generate" (info generateOptionsParser (progDesc "Generate Praos headers context and valid/invalid headers. Writes JSON formatted context to stdout and headers to stdout.")) + <> command "validate" (info validateOptionsParser (progDesc "Validate a sample of Praos headers within a context. Reads JSON formatted sample from stdin.")) + ) + +validateOptionsParser :: Parser Options +validateOptionsParser = pure Validate + +generateOptionsParser :: Parser Options +generateOptionsParser = + Generate <$> countParser + +countParser :: Parser Int +countParser = + option + auto + ( long "count" + <> short 'c' + <> metavar "INT" + <> help "Number of headers to generate" + ) diff --git a/ouroboros-consensus-cardano/app/gen-header.hs b/ouroboros-consensus-cardano/app/gen-header.hs new file mode 100644 index 0000000000..2a2ea76cae --- /dev/null +++ b/ouroboros-consensus-cardano/app/gen-header.hs @@ -0,0 +1,12 @@ +-- | This tool generates valid and invalid Cardano headers. +module Main (main) where + +import Cardano.Crypto.Init (cryptoInit) +import Cardano.Tools.Headers (run) +import GenHeader.Parsers (parseOptions) +import Main.Utf8 (withUtf8) + +main :: IO () +main = withUtf8 $ do + cryptoInit + parseOptions >>= run diff --git a/ouroboros-consensus-cardano/ouroboros-consensus-cardano.cabal b/ouroboros-consensus-cardano/ouroboros-consensus-cardano.cabal index bec7e5a1dd..a27171958b 100644 --- a/ouroboros-consensus-cardano/ouroboros-consensus-cardano.cabal +++ b/ouroboros-consensus-cardano/ouroboros-consensus-cardano.cabal @@ -356,7 +356,7 @@ test-suite shelley-test ouroboros-consensus:{ouroboros-consensus, unstable-consensus-testlib}, ouroboros-consensus-cardano, ouroboros-consensus-diffusion:unstable-diffusion-testlib, - ouroboros-consensus-protocol, + ouroboros-consensus-protocol:ouroboros-consensus-protocol, sop-core, strict-sop-core, tasty, @@ -497,6 +497,7 @@ library unstable-cardano-tools Cardano.Tools.DBTruncater.Run Cardano.Tools.DBTruncater.Types Cardano.Tools.GitRev + Cardano.Tools.Headers Cardano.Tools.ImmDBServer.Diffusion Cardano.Tools.ImmDBServer.MiniProtocols @@ -553,6 +554,7 @@ library unstable-cardano-tools ouroboros-consensus-cardano, ouroboros-consensus-diffusion ^>=0.18, ouroboros-consensus-protocol ^>=0.9, + ouroboros-consensus-protocol:unstable-protocol-testlib, ouroboros-network, ouroboros-network-api, ouroboros-network-framework ^>=0.14, @@ -662,9 +664,30 @@ test-suite tools-test hs-source-dirs: test/tools-test main-is: Main.hs build-depends: + aeson, base, ouroboros-consensus:{ouroboros-consensus, unstable-consensus-testlib}, - ouroboros-consensus-cardano, + ouroboros-consensus-cardano:{ouroboros-consensus-cardano,unstable-cardano-tools}, + ouroboros-consensus-protocol:unstable-protocol-testlib, + QuickCheck, tasty, tasty-hunit, + tasty-quickcheck, + text, unstable-cardano-tools, + other-modules: + Test.Cardano.Tools.Headers + +executable gen-header + import: common-exe + hs-source-dirs: app + main-is: gen-header.hs + build-depends: + base, + cardano-crypto-class, + optparse-applicative, + ouroboros-consensus-cardano:unstable-cardano-tools, + with-utf8, + other-modules: + GenHeader.Parsers + Paths_ouroboros_consensus_cardano diff --git a/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Tools/Headers.hs b/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Tools/Headers.hs new file mode 100644 index 0000000000..888f70e88f --- /dev/null +++ b/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Tools/Headers.hs @@ -0,0 +1,86 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-missing-export-lists #-} + +-- | Tooling to generate and validate (Praos) headers. +module Cardano.Tools.Headers where + +import Cardano.Crypto.DSIGN (deriveVerKeyDSIGN) +import Cardano.Crypto.Hash (Blake2b_256, hashToBytes) +import Cardano.Crypto.VRF + (VRFAlgorithm (deriveVerKeyVRF, hashVerKeyVRF)) +import Cardano.Ledger.Api (ConwayEra, StandardCrypto, VRF) +import Cardano.Ledger.BaseTypes (BoundedRational (boundRational), + PositiveUnitInterval, mkActiveSlotCoeff) +import Cardano.Ledger.Coin (Coin (..)) +import Cardano.Ledger.Compactible (toCompact) +import Cardano.Ledger.Keys (VKey (..), hashKey) +import Cardano.Ledger.PoolDistr (IndividualPoolStake (..)) +import Cardano.Prelude (ExitCode (..), exitWith, forM_, hPutStrLn, + stderr) +import Control.Monad.Except (runExcept) +import qualified Data.Aeson as Json +import qualified Data.ByteString.Base16 as Hex +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Map as Map +import Data.Maybe (fromJust) +import Data.Text (unpack) +import Data.Text.Encoding (decodeUtf8) +import Debug.Trace (trace) +import Ouroboros.Consensus.Block (validateView) +import Ouroboros.Consensus.Protocol.Praos (Praos, + doValidateKESSignature, doValidateVRFSignature) +import qualified Ouroboros.Consensus.Protocol.Praos.Views as Views +import Ouroboros.Consensus.Shelley.HFEras () +import Ouroboros.Consensus.Shelley.Ledger (ShelleyBlock, + mkShelleyHeader) +import Ouroboros.Consensus.Shelley.Protocol.Praos () +import Test.Ouroboros.Consensus.Protocol.Praos.Header + (GeneratorContext (..), MutatedHeader (..), Mutation (..), + Sample (..), expectedError, generateSamples, header, + mutation) + +type ConwayBlock = ShelleyBlock (Praos StandardCrypto) (ConwayEra StandardCrypto) + +-- * Running Generator +data Options + = Generate Int + | Validate + +run :: Options -> IO () +run = \case + Generate n -> do + sample <- generateSamples n + LBS.putStr $ Json.encode sample <> "\n" + Validate -> + Json.eitherDecode <$> LBS.getContents >>= \case + Left err -> hPutStrLn stderr err >> exitWith (ExitFailure 1) + Right Sample{sample} -> + forM_ sample $ \(context, mutatedHeader) -> do + print $ validate context mutatedHeader + +data ValidationResult = Valid !Mutation | Invalid !Mutation !String + deriving (Eq, Show) + +validate :: GeneratorContext -> MutatedHeader -> ValidationResult +validate context MutatedHeader{header, mutation} = + case (runExcept $ validateKES >> validateVRF, mutation) of + (Left err, mut) | expectedError mut err -> Valid mut + (Left err, mut) -> Invalid mut (show err) + (Right _, NoMutation) -> Valid NoMutation + (Right _, mut) -> Invalid mut $ "Expected error from mutation " <> show mut <> ", but validation succeeded" + where + GeneratorContext{praosSlotsPerKESPeriod, nonce, coldSignKey, vrfSignKey, ocertCounters, activeSlotCoeff} = context + -- TODO: get these from the context + maxKESEvo = 62 + coin = fromJust . toCompact . Coin + ownsAllStake vrfKey = IndividualPoolStake 1 (coin 1) vrfKey + poolDistr = Map.fromList [(poolId, ownsAllStake hashVRFKey)] + poolId = hashKey $ VKey $ deriveVerKeyDSIGN coldSignKey + hashVRFKey = hashVerKeyVRF $ deriveVerKeyVRF vrfSignKey + + headerView = validateView @ConwayBlock undefined (mkShelleyHeader header) + validateKES = doValidateKESSignature maxKESEvo praosSlotsPerKESPeriod poolDistr ocertCounters headerView + validateVRF = doValidateVRFSignature nonce poolDistr activeSlotCoeff headerView diff --git a/ouroboros-consensus-cardano/test/shelley-test/Main.hs b/ouroboros-consensus-cardano/test/shelley-test/Main.hs index f50c0c264d..30f2422978 100644 --- a/ouroboros-consensus-cardano/test/shelley-test/Main.hs +++ b/ouroboros-consensus-cardano/test/shelley-test/Main.hs @@ -14,10 +14,11 @@ main = defaultMainWithTestEnv defaultTestEnvConfig tests tests :: TestTree tests = - testGroup "shelley" - [ Test.Consensus.Shelley.Coherence.tests - , Test.Consensus.Shelley.Golden.tests - , Test.Consensus.Shelley.Serialisation.tests - , Test.Consensus.Shelley.SupportedNetworkProtocolVersion.tests - , Test.ThreadNet.Shelley.tests - ] + testGroup + "shelley" + [ Test.Consensus.Shelley.Coherence.tests + , Test.Consensus.Shelley.Golden.tests + , Test.Consensus.Shelley.Serialisation.tests + , Test.Consensus.Shelley.SupportedNetworkProtocolVersion.tests + , Test.ThreadNet.Shelley.tests + ] diff --git a/ouroboros-consensus-cardano/test/tools-test/Main.hs b/ouroboros-consensus-cardano/test/tools-test/Main.hs index 66e59c3d5d..4c4a2ed38c 100644 --- a/ouroboros-consensus-cardano/test/tools-test/Main.hs +++ b/ouroboros-consensus-cardano/test/tools-test/Main.hs @@ -8,92 +8,94 @@ import qualified Cardano.Tools.DBSynthesizer.Run as DBSynthesizer import Cardano.Tools.DBSynthesizer.Types import Ouroboros.Consensus.Block import Ouroboros.Consensus.Cardano.Block +import qualified Test.Cardano.Tools.Headers import Test.Tasty import Test.Tasty.HUnit import Test.Util.TestEnv - nodeConfig, chainDB :: FilePath -nodeConfig = "test/tools-test/disk/config/config.json" -chainDB = "test/tools-test/disk/chaindb" - +nodeConfig = "test/tools-test/disk/config/config.json" +chainDB = "test/tools-test/disk/chaindb" testSynthOptionsCreate :: DBSynthesizerOptions testSynthOptionsCreate = - DBSynthesizerOptions { - synthLimit = ForgeLimitEpoch 1 - , synthOpenMode = OpenCreateForce - } + DBSynthesizerOptions + { synthLimit = ForgeLimitEpoch 1 + , synthOpenMode = OpenCreateForce + } testSynthOptionsAppend :: DBSynthesizerOptions testSynthOptionsAppend = - DBSynthesizerOptions { - synthLimit = ForgeLimitSlot 8192 - , synthOpenMode = OpenAppend - } + DBSynthesizerOptions + { synthLimit = ForgeLimitSlot 8192 + , synthOpenMode = OpenAppend + } testNodeFilePaths :: NodeFilePaths testNodeFilePaths = - NodeFilePaths { - nfpConfig = nodeConfig - , nfpChainDB = chainDB - } + NodeFilePaths + { nfpConfig = nodeConfig + , nfpChainDB = chainDB + } testNodeCredentials :: NodeCredentials testNodeCredentials = - NodeCredentials { - credCertFile = Nothing - , credVRFFile = Nothing - , credKESFile = Nothing - , credBulkFile = Just "test/tools-test/disk/config/bulk-creds-k2.json" - } + NodeCredentials + { credCertFile = Nothing + , credVRFFile = Nothing + , credKESFile = Nothing + , credBulkFile = Just "test/tools-test/disk/config/bulk-creds-k2.json" + } testImmutaliserConfig :: DBImmutaliser.Opts testImmutaliserConfig = - DBImmutaliser.Opts { - DBImmutaliser.dbDirs = DBImmutaliser.DBDirs { - DBImmutaliser.immDBDir = chainDB <> "/immutable" - , DBImmutaliser.volDBDir = chainDB <> "/volatile" + DBImmutaliser.Opts + { DBImmutaliser.dbDirs = + DBImmutaliser.DBDirs + { DBImmutaliser.immDBDir = chainDB <> "/immutable" + , DBImmutaliser.volDBDir = chainDB <> "/volatile" + } + , DBImmutaliser.configFile = nodeConfig } - , DBImmutaliser.configFile = nodeConfig - } testAnalyserConfig :: DBAnalyserConfig testAnalyserConfig = - DBAnalyserConfig { - dbDir = chainDB - , verbose = False - , selectDB = SelectImmutableDB Origin - , validation = Just ValidateAllBlocks - , analysis = CountBlocks - , confLimit = Unlimited - } + DBAnalyserConfig + { dbDir = chainDB + , verbose = False + , selectDB = SelectImmutableDB Origin + , validation = Just ValidateAllBlocks + , analysis = CountBlocks + , confLimit = Unlimited + } testBlockArgs :: Cardano.Args (CardanoBlock StandardCrypto) testBlockArgs = Cardano.CardanoBlockArgs nodeConfig Nothing --- | A multi-step test including synthesis and analaysis 'SomeConsensusProtocol' using the Cardano instance. --- --- 1. step: synthesize a ChainDB from scratch and count the amount of blocks forged. --- 2. step: append to the previous ChainDB and coutn the amount of blocks forged. --- 3. step: copy the VolatileDB into the ImmutableDB. --- 3. step: analyze the ImmutableDB resulting from previous steps and confirm the total block count. +{- | A multi-step test including synthesis and analaysis 'SomeConsensusProtocol' using the Cardano instance. + +1. step: synthesize a ChainDB from scratch and count the amount of blocks forged. +2. step: append to the previous ChainDB and coutn the amount of blocks forged. +3. step: copy the VolatileDB into the ImmutableDB. +3. step: analyze the ImmutableDB resulting from previous steps and confirm the total block count. +-} -- blockCountTest :: (String -> IO ()) -> Assertion blockCountTest logStep = do logStep "running synthesis - create" - (options, protocol) <- either assertFailure pure - =<< DBSynthesizer.initialize - testNodeFilePaths - testNodeCredentials - testSynthOptionsCreate + (options, protocol) <- + either assertFailure pure + =<< DBSynthesizer.initialize + testNodeFilePaths + testNodeCredentials + testSynthOptionsCreate resultCreate <- DBSynthesizer.synthesize genTxs options protocol let blockCountCreate = resultForged resultCreate blockCountCreate > 0 @? "no blocks have been forged during create step" logStep "running synthesis - append" - resultAppend <- DBSynthesizer.synthesize genTxs options {confOptions = testSynthOptionsAppend} protocol + resultAppend <- DBSynthesizer.synthesize genTxs options{confOptions = testSynthOptionsAppend} protocol let blockCountAppend = resultForged resultAppend blockCountAppend > 0 @? "no blocks have been forged during append step" @@ -104,17 +106,23 @@ blockCountTest logStep = do resultAnalysis <- DBAnalyser.analyse testAnalyserConfig testBlockArgs let blockCount = blockCountCreate + blockCountAppend - resultAnalysis == Just (ResultCountBlock blockCount) @? - "wrong number of blocks encountered during analysis \ - \ (counted: " ++ show resultAnalysis ++ "; expected: " ++ show blockCount ++ ")" + resultAnalysis == Just (ResultCountBlock blockCount) + @? "wrong number of blocks encountered during analysis \ + \ (counted: " + ++ show resultAnalysis + ++ "; expected: " + ++ show blockCount + ++ ")" where genTxs _ _ _ = pure [] tests :: TestTree tests = - testGroup "cardano-tools" - [ testCaseSteps "synthesize and analyse: blockCount\n" blockCountTest - ] + testGroup + "cardano-tools" + [ testCaseSteps "synthesize and analyse: blockCount\n" blockCountTest + , Test.Cardano.Tools.Headers.tests + ] main :: IO () main = defaultMainWithTestEnv defaultTestEnvConfig tests diff --git a/ouroboros-consensus-cardano/test/tools-test/Test/Cardano/Tools/Headers.hs b/ouroboros-consensus-cardano/test/tools-test/Test/Cardano/Tools/Headers.hs new file mode 100644 index 0000000000..fc2b10e505 --- /dev/null +++ b/ouroboros-consensus-cardano/test/tools-test/Test/Cardano/Tools/Headers.hs @@ -0,0 +1,50 @@ +module Test.Cardano.Tools.Headers (tests) where + +import Cardano.Tools.Headers (ValidationResult (..), validate) +import qualified Data.Aeson as Json +import Data.Function ((&)) +import qualified Data.Text.Lazy as LT +import Data.Text.Lazy.Encoding (decodeUtf8) +import Test.Ouroboros.Consensus.Protocol.Praos.Header (genContext, + genMutatedHeader, genSample) +import Test.QuickCheck (Property, counterexample, forAll, forAllBlind, + label, property, (===)) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.QuickCheck (testProperty) + +tests :: TestTree +tests = + testGroup + "HeaderValidation" + [ testProperty "roundtrip To/FromJSON samples" prop_roundtrip_json_samples + , testProperty "validate legit header" prop_validate_legit_header + ] + +prop_roundtrip_json_samples :: Property +prop_roundtrip_json_samples = + forAll genSample $ \sample -> + let encoded = Json.encode sample + decoded = Json.eitherDecode encoded + in decoded === Right sample + +prop_validate_legit_header :: Property +prop_validate_legit_header = + forAllBlind genContext $ \context -> + forAllBlind (genMutatedHeader context) $ \(context', header) -> + annotate context header $ + case validate context' header of + Valid mut -> property True & label (show mut) + Invalid mut err -> property False & counterexample ("Expected: " <> show mut <> "\nError: " <> err) + where + annotate context header = + counterexample + ( unlines $ + [ "context:" + , asJson context + , "header:" + , show header + ] + ) + + asJson :: (Json.ToJSON a) => a -> String + asJson = LT.unpack . decodeUtf8 . Json.encode diff --git a/ouroboros-consensus-protocol/ouroboros-consensus-protocol.cabal b/ouroboros-consensus-protocol/ouroboros-consensus-protocol.cabal index 45fc293847..51431234cc 100644 --- a/ouroboros-consensus-protocol/ouroboros-consensus-protocol.cabal +++ b/ouroboros-consensus-protocol/ouroboros-consensus-protocol.cabal @@ -84,16 +84,26 @@ library unstable-protocol-testlib import: common-lib visibility: public hs-source-dirs: src/unstable-protocol-testlib - exposed-modules: Test.Consensus.Protocol.Serialisation.Generators + exposed-modules: + Test.Consensus.Protocol.Serialisation.Generators + Test.Ouroboros.Consensus.Protocol.Praos.Header build-depends: QuickCheck, + aeson, base, + base16-bytestring, + bytestring, cardano-crypto-class, + cardano-crypto-praos, cardano-crypto-tests, + cardano-ledger-binary, + cardano-ledger-core, cardano-ledger-shelley-test, cardano-protocol-tpraos, cardano-slotting, + containers, ouroboros-consensus-protocol, + text test-suite protocol-test import: common-test diff --git a/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs b/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs index c9c54ed4cd..a231f25921 100644 --- a/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs +++ b/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs @@ -29,6 +29,9 @@ module Ouroboros.Consensus.Protocol.Praos ( , Ticked (..) , forgePraosFields , praosCheckCanForge + -- * For testing purposes + , doValidateKESSignature + , doValidateVRFSignature ) where import Cardano.Binary (FromCBOR (..), ToCBOR (..), enforceSize) @@ -97,13 +100,13 @@ import Ouroboros.Consensus.Util.Versioned (VersionDecoder (Decode), data Praos c class - ( Crypto c, - DSIGN.Signable (DSIGN c) (OCertSignable c), - DSIGN.Signable (DSIGN c) (SL.Hash c EraIndependentTxBody), - KES.Signable (KES c) (HeaderBody c), - VRF.Signable (VRF c) InputVRF - ) => - PraosCrypto c + ( Crypto c + , DSIGN.Signable (DSIGN c) (OCertSignable c) + , DSIGN.Signable (DSIGN c) (SL.Hash c EraIndependentTxBody) + , KES.Signable (KES c) (HeaderBody c) + , VRF.Signable (VRF c) InputVRF + ) => + PraosCrypto c instance PraosCrypto StandardCrypto @@ -112,73 +115,74 @@ instance PraosCrypto StandardCrypto -------------------------------------------------------------------------------} data PraosFields c toSign = PraosFields - { praosSignature :: SL.SignedKES c toSign, - praosToSign :: toSign - } - deriving (Generic) + { praosSignature :: SL.SignedKES c toSign + , praosToSign :: toSign + } + deriving (Generic) deriving instance - (NoThunks toSign, PraosCrypto c) => - NoThunks (PraosFields c toSign) + (NoThunks toSign, PraosCrypto c) => + NoThunks (PraosFields c toSign) deriving instance - (Show toSign, PraosCrypto c) => - Show (PraosFields c toSign) + (Show toSign, PraosCrypto c) => + Show (PraosFields c toSign) --- | Fields arising from praos execution which must be included in --- the block signature. +{- | Fields arising from praos execution which must be included in +the block signature. +-} data PraosToSign c = PraosToSign - { -- | Verification key for the issuer of this block. - praosToSignIssuerVK :: SL.VKey 'SL.BlockIssuer c, - praosToSignVrfVK :: SL.VerKeyVRF c, - -- | Verifiable random value. This is used both to prove the issuer is + { praosToSignIssuerVK :: SL.VKey 'SL.BlockIssuer c + -- ^ Verification key for the issuer of this block. + , praosToSignVrfVK :: SL.VerKeyVRF c + , praosToSignVrfRes :: SL.CertifiedVRF c InputVRF + -- ^ Verifiable random value. This is used both to prove the issuer is -- eligible to issue a block, and to contribute to the evolving nonce. - praosToSignVrfRes :: SL.CertifiedVRF c InputVRF, - -- | Lightweight delegation certificate mapping the cold (DSIGN) key to + , praosToSignOCert :: OCert.OCert c + -- ^ Lightweight delegation certificate mapping the cold (DSIGN) key to -- the online KES key. - praosToSignOCert :: OCert.OCert c - } - deriving (Generic) + } + deriving (Generic) -instance PraosCrypto c => NoThunks (PraosToSign c) +instance (PraosCrypto c) => NoThunks (PraosToSign c) -deriving instance PraosCrypto c => Show (PraosToSign c) +deriving instance (PraosCrypto c) => Show (PraosToSign c) forgePraosFields :: - ( PraosCrypto c, - SL.KESignable c toSign, - Monad m - ) => - HotKey c m -> - CanBeLeader (Praos c) -> - IsLeader (Praos c) -> - (PraosToSign c -> toSign) -> - m (PraosFields c toSign) + ( PraosCrypto c + , SL.KESignable c toSign + , Monad m + ) => + HotKey c m -> + CanBeLeader (Praos c) -> + IsLeader (Praos c) -> + (PraosToSign c -> toSign) -> + m (PraosFields c toSign) forgePraosFields - hotKey - PraosCanBeLeader - { praosCanBeLeaderColdVerKey, - praosCanBeLeaderSignKeyVRF, - praosCanBeLeaderOpCert - } - PraosIsLeader {praosIsLeaderVrfRes} - mkToSign = do - signature <- HotKey.sign hotKey toSign - return - PraosFields - { praosSignature = signature, - praosToSign = toSign + hotKey + PraosCanBeLeader + { praosCanBeLeaderColdVerKey + , praosCanBeLeaderSignKeyVRF + , praosCanBeLeaderOpCert } - where - toSign = mkToSign signedFields - - signedFields = - PraosToSign - { praosToSignIssuerVK = praosCanBeLeaderColdVerKey, - praosToSignVrfVK = VRF.deriveVerKeyVRF praosCanBeLeaderSignKeyVRF, - praosToSignVrfRes = praosIsLeaderVrfRes, - praosToSignOCert = praosCanBeLeaderOpCert - } + PraosIsLeader{praosIsLeaderVrfRes} + mkToSign = do + signature <- HotKey.sign hotKey toSign + return + PraosFields + { praosSignature = signature + , praosToSign = toSign + } + where + toSign = mkToSign signedFields + + signedFields = + PraosToSign + { praosToSignIssuerVK = praosCanBeLeaderColdVerKey + , praosToSignVrfVK = VRF.deriveVerKeyVRF praosCanBeLeaderSignKeyVRF + , praosToSignVrfRes = praosIsLeaderVrfRes + , praosToSignOCert = praosCanBeLeaderOpCert + } {------------------------------------------------------------------------------- Protocol proper @@ -186,49 +190,50 @@ forgePraosFields -- | Praos parameters that are node independent data PraosParams = PraosParams - { -- | See 'Globals.slotsPerKESPeriod'. - praosSlotsPerKESPeriod :: !Word64, - -- | Active slots coefficient. This parameter represents the proportion + { praosSlotsPerKESPeriod :: !Word64 + -- ^ See 'Globals.slotsPerKESPeriod'. + , praosLeaderF :: !SL.ActiveSlotCoeff + -- ^ Active slots coefficient. This parameter represents the proportion -- of slots in which blocks should be issued. This can be interpreted as -- the probability that a party holding all the stake will be elected as -- leader for a given slot. - praosLeaderF :: !SL.ActiveSlotCoeff, -- | See 'Globals.securityParameter'. - praosSecurityParam :: !SecurityParam, - -- | Maximum number of KES iterations, see 'Globals.maxKESEvo'. - praosMaxKESEvo :: !Word64, - -- | All blocks invalid after this protocol version, see + , praosSecurityParam :: !SecurityParam + , praosMaxKESEvo :: !Word64 + -- ^ Maximum number of KES iterations, see 'Globals.maxKESEvo'. + , praosMaxMajorPV :: !MaxMajorProtVer + -- ^ All blocks invalid after this protocol version, see -- 'Globals.maxMajorPV'. - praosMaxMajorPV :: !MaxMajorProtVer, - -- | The number of slots before the start of an epoch where the + } + -- \| The number of slots before the start of an epoch where the -- corresponding epoch nonce is snapshotted. This has to be at least one -- stability window such that the nonce is stable at the beginning of the -- epoch. Ouroboros Genesis requires this to be even larger, see -- 'SL.computeRandomnessStabilisationWindow'. - praosRandomnessStabilisationWindow :: !Word64 - } - deriving (Generic, NoThunks) --- | Assembled proof that the issuer has the right to issue a block in the --- selected slot. + deriving (Generic, NoThunks) + +{- | Assembled proof that the issuer has the right to issue a block in the +selected slot. +-} newtype PraosIsLeader c = PraosIsLeader - { praosIsLeaderVrfRes :: SL.CertifiedVRF c InputVRF - } - deriving (Generic) + { praosIsLeaderVrfRes :: SL.CertifiedVRF c InputVRF + } + deriving (Generic) -instance PraosCrypto c => NoThunks (PraosIsLeader c) +instance (PraosCrypto c) => NoThunks (PraosIsLeader c) -- | Static configuration data instance ConsensusConfig (Praos c) = PraosConfig - { praosParams :: !PraosParams, - praosEpochInfo :: !(EpochInfo (Except History.PastHorizonException)) + { praosParams :: !PraosParams + , praosEpochInfo :: !(EpochInfo (Except History.PastHorizonException)) -- it's useful for this record to be EpochInfo and one other thing, -- because the one other thing can then be used as the -- PartialConsensConfig in the HFC instance. - } - deriving (Generic) + } + deriving (Generic) -instance PraosCrypto c => NoThunks (ConsensusConfig (Praos c)) +instance (PraosCrypto c) => NoThunks (ConsensusConfig (Praos c)) type PraosValidateView c = Views.HeaderView c @@ -236,311 +241,326 @@ type PraosValidateView c = Views.HeaderView c ConsensusProtocol -------------------------------------------------------------------------------} --- | Praos consensus state. --- --- We track the last slot and the counters for operational certificates, as well --- as a series of nonces which get updated in different ways over the course of --- an epoch. +{- | Praos consensus state. + +We track the last slot and the counters for operational certificates, as well +as a series of nonces which get updated in different ways over the course of +an epoch. +-} data PraosState c = PraosState - { praosStateLastSlot :: !(WithOrigin SlotNo), - -- | Operation Certificate counters - praosStateOCertCounters :: !(Map (KeyHash 'BlockIssuer c) Word64), - -- | Evolving nonce - praosStateEvolvingNonce :: !Nonce, - -- | Candidate nonce - praosStateCandidateNonce :: !Nonce, - -- | Epoch nonce - praosStateEpochNonce :: !Nonce, - -- | Nonce constructed from the hash of the previous block - praosStateLabNonce :: !Nonce, - -- | Nonce corresponding to the LAB nonce of the last block of the previous + { praosStateLastSlot :: !(WithOrigin SlotNo) + , praosStateOCertCounters :: !(Map (KeyHash 'BlockIssuer c) Word64) + -- ^ Operation Certificate counters + , praosStateEvolvingNonce :: !Nonce + -- ^ Evolving nonce + , praosStateCandidateNonce :: !Nonce + -- ^ Candidate nonce + , praosStateEpochNonce :: !Nonce + -- ^ Epoch nonce + , praosStateLabNonce :: !Nonce + -- ^ Nonce constructed from the hash of the previous block + , praosStateLastEpochBlockNonce :: !Nonce + -- ^ Nonce corresponding to the LAB nonce of the last block of the previous -- epoch - praosStateLastEpochBlockNonce :: !Nonce - } - deriving (Generic, Show, Eq) - -instance PraosCrypto c => NoThunks (PraosState c) - -instance PraosCrypto c => ToCBOR (PraosState c) where - toCBOR = encode - -instance PraosCrypto c => FromCBOR (PraosState c) where - fromCBOR = decode - -instance PraosCrypto c => Serialise (PraosState c) where - encode - PraosState - { praosStateLastSlot, - praosStateOCertCounters, - praosStateEvolvingNonce, - praosStateCandidateNonce, - praosStateEpochNonce, - praosStateLabNonce, - praosStateLastEpochBlockNonce - } = - encodeVersion 0 $ - mconcat - [ CBOR.encodeListLen 7, - toCBOR praosStateLastSlot, - toCBOR praosStateOCertCounters, - toCBOR praosStateEvolvingNonce, - toCBOR praosStateCandidateNonce, - toCBOR praosStateEpochNonce, - toCBOR praosStateLabNonce, - toCBOR praosStateLastEpochBlockNonce - ] - - decode = - decodeVersion - [(0, Decode decodePraosState)] - where - decodePraosState = do - enforceSize "PraosState" 7 + } + deriving (Generic, Show, Eq) + +instance (PraosCrypto c) => NoThunks (PraosState c) + +instance (PraosCrypto c) => ToCBOR (PraosState c) where + toCBOR = encode + +instance (PraosCrypto c) => FromCBOR (PraosState c) where + fromCBOR = decode + +instance (PraosCrypto c) => Serialise (PraosState c) where + encode PraosState - <$> fromCBOR - <*> fromCBOR - <*> fromCBOR - <*> fromCBOR - <*> fromCBOR - <*> fromCBOR - <*> fromCBOR + { praosStateLastSlot + , praosStateOCertCounters + , praosStateEvolvingNonce + , praosStateCandidateNonce + , praosStateEpochNonce + , praosStateLabNonce + , praosStateLastEpochBlockNonce + } = + encodeVersion 0 $ + mconcat + [ CBOR.encodeListLen 7 + , toCBOR praosStateLastSlot + , toCBOR praosStateOCertCounters + , toCBOR praosStateEvolvingNonce + , toCBOR praosStateCandidateNonce + , toCBOR praosStateEpochNonce + , toCBOR praosStateLabNonce + , toCBOR praosStateLastEpochBlockNonce + ] + + decode = + decodeVersion + [(0, Decode decodePraosState)] + where + decodePraosState = do + enforceSize "PraosState" 7 + PraosState + <$> fromCBOR + <*> fromCBOR + <*> fromCBOR + <*> fromCBOR + <*> fromCBOR + <*> fromCBOR + <*> fromCBOR data instance Ticked (PraosState c) = TickedPraosState - { tickedPraosStateChainDepState :: PraosState c, - tickedPraosStateLedgerView :: Views.LedgerView c - } + { tickedPraosStateChainDepState :: PraosState c + , tickedPraosStateLedgerView :: Views.LedgerView c + } -- | Errors which we might encounter data PraosValidationErr c - = VRFKeyUnknown - !(KeyHash SL.StakePool c) -- unknown VRF keyhash (not registered) - | VRFKeyWrongVRFKey - !(KeyHash SL.StakePool c) -- KeyHash of block issuer - !(SL.Hash c (SL.VerKeyVRF c)) -- VRF KeyHash registered with stake pool - !(SL.Hash c (SL.VerKeyVRF c)) -- VRF KeyHash from Header - | VRFKeyBadProof - !SlotNo -- Slot used for VRF calculation - !Nonce -- Epoch nonce used for VRF calculation - !(VRF.CertifiedVRF (VRF c) InputVRF) -- VRF calculated nonce value - | VRFLeaderValueTooBig Natural Rational ActiveSlotCoeff - | KESBeforeStartOCERT - !KESPeriod -- OCert Start KES Period - !KESPeriod -- Current KES Period - | KESAfterEndOCERT - !KESPeriod -- Current KES Period - !KESPeriod -- OCert Start KES Period - !Word64 -- Max KES Key Evolutions - | CounterTooSmallOCERT - !Word64 -- last KES counter used - !Word64 -- current KES counter - | -- | The KES counter has been incremented by more than 1 - CounterOverIncrementedOCERT - !Word64 -- last KES counter used - !Word64 -- current KES counter - | InvalidSignatureOCERT - !Word64 -- OCert counter - !KESPeriod -- OCert KES period - !String -- DSIGN error message - | InvalidKesSignatureOCERT - !Word -- current KES Period - !Word -- KES start period - !Word -- expected KES evolutions - !String -- error message given by Consensus Layer - | NoCounterForKeyHashOCERT - !(KeyHash 'BlockIssuer c) -- stake pool key hash - deriving (Generic) - -deriving instance PraosCrypto c => Eq (PraosValidationErr c) - -deriving instance PraosCrypto c => NoThunks (PraosValidationErr c) - -deriving instance PraosCrypto c => Show (PraosValidationErr c) - -instance PraosCrypto c => ConsensusProtocol (Praos c) where - type ChainDepState (Praos c) = PraosState c - type IsLeader (Praos c) = PraosIsLeader c - type CanBeLeader (Praos c) = PraosCanBeLeader c - type SelectView (Praos c) = PraosChainSelectView c - type LedgerView (Praos c) = Views.LedgerView c - type ValidationErr (Praos c) = PraosValidationErr c - type ValidateView (Praos c) = PraosValidateView c - - protocolSecurityParam = praosSecurityParam . praosParams - - checkIsLeader - cfg - PraosCanBeLeader - { praosCanBeLeaderSignKeyVRF, - praosCanBeLeaderColdVerKey - } - slot - cs = - if meetsLeaderThreshold cfg lv (SL.coerceKeyRole vkhCold) rho - then - Just - PraosIsLeader - { praosIsLeaderVrfRes = coerce rho - } - else Nothing - where - chainState = tickedPraosStateChainDepState cs - lv = tickedPraosStateLedgerView cs - eta0 = praosStateEpochNonce chainState - vkhCold = SL.hashKey praosCanBeLeaderColdVerKey - rho' = mkInputVRF slot eta0 - - rho = VRF.evalCertified () rho' praosCanBeLeaderSignKeyVRF - - -- Updating the chain dependent state for Praos. - -- - -- If we are not in a new epoch, then nothing happens. If we are in a new - -- epoch, we do two things: - -- - Update the epoch nonce to the combination of the candidate nonce and the - -- nonce derived from the last block of the previous epoch. - -- - Update the "last block of previous epoch" nonce to the nonce derived from - -- the last applied block. - tickChainDepState - PraosConfig {praosEpochInfo} - lv - slot - st = - TickedPraosState - { tickedPraosStateChainDepState = st', - tickedPraosStateLedgerView = lv - } - where - newEpoch = - isNewEpoch - (History.toPureEpochInfo praosEpochInfo) - (praosStateLastSlot st) - slot - st' = - if newEpoch - then - st - { praosStateEpochNonce = - praosStateCandidateNonce st - ⭒ praosStateLastEpochBlockNonce st, - praosStateLastEpochBlockNonce = praosStateLabNonce st + = VRFKeyUnknown + !(KeyHash SL.StakePool c) -- unknown VRF keyhash (not registered) + | VRFKeyWrongVRFKey + !(KeyHash SL.StakePool c) -- KeyHash of block issuer + !(SL.Hash c (SL.VerKeyVRF c)) -- VRF KeyHash registered with stake pool + !(SL.Hash c (SL.VerKeyVRF c)) -- VRF KeyHash from Header + | VRFKeyBadProof + !SlotNo -- Slot used for VRF calculation + !Nonce -- Epoch nonce used for VRF calculation + !(VRF.CertifiedVRF (VRF c) InputVRF) -- VRF calculated nonce value + | VRFLeaderValueTooBig Natural Rational ActiveSlotCoeff + | KESBeforeStartOCERT + !KESPeriod -- OCert Start KES Period + !KESPeriod -- Current KES Period + | KESAfterEndOCERT + !KESPeriod -- Current KES Period + !KESPeriod -- OCert Start KES Period + !Word64 -- Max KES Key Evolutions + | CounterTooSmallOCERT + !Word64 -- last KES counter used + !Word64 -- current KES counter + | -- | The KES counter has been incremented by more than 1 + CounterOverIncrementedOCERT + !Word64 -- last KES counter used + !Word64 -- current KES counter + | InvalidSignatureOCERT + !Word64 -- OCert counter + !KESPeriod -- OCert KES period + !String -- DSIGN error message + | InvalidKesSignatureOCERT + !Word -- current KES Period + !Word -- KES start period + !Word -- expected KES evolutions + !String -- error message given by Consensus Layer + | NoCounterForKeyHashOCERT + !(KeyHash 'BlockIssuer c) -- stake pool key hash + deriving (Generic) + +deriving instance (PraosCrypto c) => Eq (PraosValidationErr c) + +deriving instance (PraosCrypto c) => NoThunks (PraosValidationErr c) + +deriving instance (PraosCrypto c) => Show (PraosValidationErr c) + +instance (PraosCrypto c) => ConsensusProtocol (Praos c) where + type ChainDepState (Praos c) = PraosState c + type IsLeader (Praos c) = PraosIsLeader c + type CanBeLeader (Praos c) = PraosCanBeLeader c + type SelectView (Praos c) = PraosChainSelectView c + type LedgerView (Praos c) = Views.LedgerView c + type ValidationErr (Praos c) = PraosValidationErr c + type ValidateView (Praos c) = PraosValidateView c + + protocolSecurityParam = praosSecurityParam . praosParams + + checkIsLeader + cfg + PraosCanBeLeader + { praosCanBeLeaderSignKeyVRF + , praosCanBeLeaderColdVerKey + } + slot + cs = + if meetsLeaderThreshold cfg lv (SL.coerceKeyRole vkhCold) rho + then + Just + PraosIsLeader + { praosIsLeaderVrfRes = coerce rho + } + else Nothing + where + chainState = tickedPraosStateChainDepState cs + lv = tickedPraosStateLedgerView cs + eta0 = praosStateEpochNonce chainState + vkhCold = SL.hashKey praosCanBeLeaderColdVerKey + rho' = mkInputVRF slot eta0 + + rho = VRF.evalCertified () rho' praosCanBeLeaderSignKeyVRF + + -- Updating the chain dependent state for Praos. + -- + -- If we are not in a new epoch, then nothing happens. If we are in a new + -- epoch, we do two things: + -- - Update the epoch nonce to the combination of the candidate nonce and the + -- nonce derived from the last block of the previous epoch. + -- - Update the "last block of previous epoch" nonce to the nonce derived from + -- the last applied block. + tickChainDepState + PraosConfig{praosEpochInfo} + lv + slot + st = + TickedPraosState + { tickedPraosStateChainDepState = st' + , tickedPraosStateLedgerView = lv } - else st - - -- Validate and update the chain dependent state as a result of processing a - -- new header. - -- - -- This consists of: - -- - Validate the VRF checks - -- - Validate the KES checks - -- - Call 'reupdateChainDepState' - -- - updateChainDepState - cfg@( PraosConfig - PraosParams {praosLeaderF} - _ - ) - b - slot - tcs = do - -- First, we check the KES signature, which validates that the issuer is - -- in fact who they say they are. - validateKESSignature cfg lv (praosStateOCertCounters cs) b - -- Then we examing the VRF proof, which confirms that they have the - -- right to issue in this slot. - validateVRFSignature (praosStateEpochNonce cs) lv praosLeaderF b - -- Finally, we apply the changes from this header to the chain state. - pure $ reupdateChainDepState cfg b slot tcs - where - lv = tickedPraosStateLedgerView tcs - cs = tickedPraosStateChainDepState tcs - - -- Re-update the chain dependent state as a result of processing a header. - -- - -- This consists of: - -- - Update the last applied block hash. - -- - Update the evolving and (potentially) candidate nonces based on the - -- position in the epoch. - -- - Update the operational certificate counter. - reupdateChainDepState - _cfg@( PraosConfig - PraosParams {praosRandomnessStabilisationWindow} - ei - ) - b - slot - tcs = - cs - { praosStateLastSlot = NotOrigin slot, - praosStateLabNonce = prevHashToNonce (Views.hvPrevHash b), - praosStateEvolvingNonce = newEvolvingNonce, - praosStateCandidateNonce = - if slot +* Duration praosRandomnessStabilisationWindow < firstSlotNextEpoch - then newEvolvingNonce - else praosStateCandidateNonce cs, - praosStateOCertCounters = - Map.insert hk n $ praosStateOCertCounters cs - } - where - epochInfoWithErr = - hoistEpochInfo - (either throw pure . runExcept) - ei - firstSlotNextEpoch = runIdentity $ do - EpochNo currentEpochNo <- epochInfoEpoch epochInfoWithErr slot - let nextEpoch = EpochNo $ currentEpochNo + 1 - epochInfoFirst epochInfoWithErr nextEpoch - cs = tickedPraosStateChainDepState tcs - eta = vrfNonceValue (Proxy @c) $ Views.hvVrfRes b - newEvolvingNonce = praosStateEvolvingNonce cs ⭒ eta - OCert _ n _ _ = Views.hvOCert b - hk = hashKey $ Views.hvVK b + where + newEpoch = + isNewEpoch + (History.toPureEpochInfo praosEpochInfo) + (praosStateLastSlot st) + slot + st' = + if newEpoch + then + st + { praosStateEpochNonce = + praosStateCandidateNonce st + ⭒ praosStateLastEpochBlockNonce st + , praosStateLastEpochBlockNonce = praosStateLabNonce st + } + else st + + -- Validate and update the chain dependent state as a result of processing a + -- new header. + -- + -- This consists of: + -- - Validate the VRF checks + -- - Validate the KES checks + -- - Call 'reupdateChainDepState' + -- + updateChainDepState + cfg@( PraosConfig + PraosParams{praosLeaderF} + _ + ) + b + slot + tcs = do + -- First, we check the KES signature, which validates that the issuer is + -- in fact who they say they are. + validateKESSignature cfg lv (praosStateOCertCounters cs) b + -- Then we examing the VRF proof, which confirms that they have the + -- right to issue in this slot. + validateVRFSignature (praosStateEpochNonce cs) lv praosLeaderF b + -- Finally, we apply the changes from this header to the chain state. + pure $ reupdateChainDepState cfg b slot tcs + where + lv = tickedPraosStateLedgerView tcs + cs = tickedPraosStateChainDepState tcs + + -- Re-update the chain dependent state as a result of processing a header. + -- + -- This consists of: + -- - Update the last applied block hash. + -- - Update the evolving and (potentially) candidate nonces based on the + -- position in the epoch. + -- - Update the operational certificate counter. + reupdateChainDepState + _cfg@( PraosConfig + PraosParams{praosRandomnessStabilisationWindow} + ei + ) + b + slot + tcs = + cs + { praosStateLastSlot = NotOrigin slot + , praosStateLabNonce = prevHashToNonce (Views.hvPrevHash b) + , praosStateEvolvingNonce = newEvolvingNonce + , praosStateCandidateNonce = + if slot +* Duration praosRandomnessStabilisationWindow < firstSlotNextEpoch + then newEvolvingNonce + else praosStateCandidateNonce cs + , praosStateOCertCounters = + Map.insert hk n $ praosStateOCertCounters cs + } + where + epochInfoWithErr = + hoistEpochInfo + (either throw pure . runExcept) + ei + firstSlotNextEpoch = runIdentity $ do + EpochNo currentEpochNo <- epochInfoEpoch epochInfoWithErr slot + let nextEpoch = EpochNo $ currentEpochNo + 1 + epochInfoFirst epochInfoWithErr nextEpoch + cs = tickedPraosStateChainDepState tcs + eta = vrfNonceValue (Proxy @c) $ Views.hvVrfRes b + newEvolvingNonce = praosStateEvolvingNonce cs ⭒ eta + OCert _ n _ _ = Views.hvOCert b + hk = hashKey $ Views.hvVK b -- | Check whether this node meets the leader threshold to issue a block. meetsLeaderThreshold :: - forall c. - PraosCrypto c => - ConsensusConfig (Praos c) -> - LedgerView (Praos c) -> - SL.KeyHash 'SL.StakePool c -> - VRF.CertifiedVRF (VRF c) InputVRF -> - Bool + forall c. + (PraosCrypto c) => + ConsensusConfig (Praos c) -> + LedgerView (Praos c) -> + SL.KeyHash 'SL.StakePool c -> + VRF.CertifiedVRF (VRF c) InputVRF -> + Bool meetsLeaderThreshold - PraosConfig {praosParams} - Views.LedgerView {Views.lvPoolDistr} - keyHash - rho = - checkLeaderNatValue - (vrfLeaderValue (Proxy @c) rho) - r - (praosLeaderF praosParams) - where - SL.PoolDistr poolDistr _totalActiveStake = lvPoolDistr - r = - maybe 0 SL.individualPoolStake $ - Map.lookup keyHash poolDistr + PraosConfig{praosParams} + Views.LedgerView{Views.lvPoolDistr} + keyHash + rho = + checkLeaderNatValue + (vrfLeaderValue (Proxy @c) rho) + r + (praosLeaderF praosParams) + where + SL.PoolDistr poolDistr _totalActiveStake = lvPoolDistr + r = + maybe 0 SL.individualPoolStake $ + Map.lookup keyHash poolDistr validateVRFSignature :: - forall c. - ( PraosCrypto c - ) => - Nonce -> - Views.LedgerView c -> - ActiveSlotCoeff -> - Views.HeaderView c -> - Except (PraosValidationErr c) () -validateVRFSignature eta0 (Views.lvPoolDistr -> SL.PoolDistr pd _) f b = do - case Map.lookup hk pd of - Nothing -> throwError $ VRFKeyUnknown hk - Just (IndividualPoolStake sigma _totalPoolStake vrfHK) -> do - vrfHK == hashVerKeyVRF vrfK - ?! VRFKeyWrongVRFKey hk vrfHK (hashVerKeyVRF vrfK) - VRF.verifyCertified - () - vrfK - (mkInputVRF slot eta0) - vrfCert - ?! VRFKeyBadProof slot eta0 vrfCert - checkLeaderNatValue vrfLeaderVal sigma f - ?! VRFLeaderValueTooBig (bvValue vrfLeaderVal) sigma f + forall c. + ( PraosCrypto c + ) => + Nonce -> + Views.LedgerView c -> + ActiveSlotCoeff -> + Views.HeaderView c -> + Except (PraosValidationErr c) () +validateVRFSignature eta0 (Views.lvPoolDistr -> SL.PoolDistr pd _) f = + doValidateVRFSignature eta0 pd f + +-- NOTE: this function is much easier to test than 'validateVRFSignature' because we don't need +-- to construct a 'PraosConfig' nor 'LedgerView' to test it. +doValidateVRFSignature :: + forall c. + (PraosCrypto c) => + Nonce -> + Map (KeyHash SL.StakePool c) (IndividualPoolStake c) -> + ActiveSlotCoeff -> + Views.HeaderView c -> + Except (PraosValidationErr c) () +doValidateVRFSignature eta0 pd f b = do + case Map.lookup hk pd of + Nothing -> throwError $ VRFKeyUnknown hk + Just (IndividualPoolStake sigma _totalPoolStake vrfHK) -> do + vrfHK + == hashVerKeyVRF vrfK + ?! VRFKeyWrongVRFKey hk vrfHK (hashVerKeyVRF vrfK) + VRF.verifyCertified + () + vrfK + (mkInputVRF slot eta0) + vrfCert + ?! VRFKeyBadProof slot eta0 vrfCert + checkLeaderNatValue vrfLeaderVal sigma f + ?! VRFLeaderValueTooBig (bvValue vrfLeaderVal) sigma f where hk = coerceKeyRole . hashKey . Views.hvVK $ b vrfK = Views.hvVrfVK b @@ -549,184 +569,199 @@ validateVRFSignature eta0 (Views.lvPoolDistr -> SL.PoolDistr pd _) f b = do slot = Views.hvSlotNo b validateKESSignature :: - PraosCrypto c => - ConsensusConfig (Praos c) -> - LedgerView (Praos c) -> - Map (KeyHash 'BlockIssuer c) Word64 -> - Views.HeaderView c -> - Except (PraosValidationErr c) () + (PraosCrypto c) => + ConsensusConfig (Praos c) -> + LedgerView (Praos c) -> + Map (KeyHash 'BlockIssuer c) Word64 -> + Views.HeaderView c -> + Except (PraosValidationErr c) () validateKESSignature - _cfg@( PraosConfig - PraosParams {praosMaxKESEvo, praosSlotsPerKESPeriod} - _ei - ) - Views.LedgerView {Views.lvPoolDistr} - ocertCounters - b = do - c0 <= kp ?! KESBeforeStartOCERT c0 kp - kp_ < c0_ + fromIntegral praosMaxKESEvo ?! KESAfterEndOCERT kp c0 praosMaxKESEvo - - let t = if kp_ >= c0_ then kp_ - c0_ else 0 - -- this is required to prevent an arithmetic underflow, in the case of kp_ < - -- c0_ we get the above `KESBeforeStartOCERT` failure in the transition. - - DSIGN.verifySignedDSIGN () vkcold (OCert.ocertToSignable oc) tau ?!: - InvalidSignatureOCERT n c0 - KES.verifySignedKES () vk_hot t (Views.hvSigned b) (Views.hvSignature b) ?!: - InvalidKesSignatureOCERT kp_ c0_ t - - case currentIssueNo of - Nothing -> do - throwError $ NoCounterForKeyHashOCERT hk - Just m -> do - m <= n ?! CounterTooSmallOCERT m n - n <= m + 1 ?! CounterOverIncrementedOCERT m n - where - oc@(OCert vk_hot n c0@(KESPeriod c0_) tau) = Views.hvOCert b - (VKey vkcold) = Views.hvVK b - SlotNo s = Views.hvSlotNo b - hk = hashKey $ Views.hvVK b - kp@(KESPeriod kp_) = + _cfg@( PraosConfig + PraosParams{praosMaxKESEvo, praosSlotsPerKESPeriod} + _ei + ) + Views.LedgerView{Views.lvPoolDistr = SL.PoolDistr lvPoolDistr _totalActiveStake} + ocertCounters = + doValidateKESSignature praosMaxKESEvo praosSlotsPerKESPeriod lvPoolDistr ocertCounters + +-- NOTE: This function is much easier to test than 'validateKESSignature' because we don't need to +-- construct a 'PraosConfig' nor 'LedgerView' to test it. +doValidateKESSignature :: + (PraosCrypto c) => + Word64 -> + Word64 -> + Map (KeyHash SL.StakePool c) (IndividualPoolStake c) -> + Map (KeyHash BlockIssuer c) Word64 -> + Views.HeaderView c -> + Except (PraosValidationErr c) () +doValidateKESSignature praosMaxKESEvo praosSlotsPerKESPeriod stakeDistribution ocertCounters b = + do + c0 <= kp ?! KESBeforeStartOCERT c0 kp + kp_ < c0_ + fromIntegral praosMaxKESEvo ?! KESAfterEndOCERT kp c0 praosMaxKESEvo + + let t = if kp_ >= c0_ then kp_ - c0_ else 0 + -- this is required to prevent an arithmetic underflow, in the case of kp_ < + -- c0_ we get the above `KESBeforeStartOCERT` failure in the transition. + + DSIGN.verifySignedDSIGN () vkcold (OCert.ocertToSignable oc) tau + ?!: InvalidSignatureOCERT n c0 + KES.verifySignedKES () vk_hot t (Views.hvSigned b) (Views.hvSignature b) + ?!: InvalidKesSignatureOCERT kp_ c0_ t + + case currentIssueNo of + Nothing -> do + throwError $ NoCounterForKeyHashOCERT hk + Just m -> do + m <= n ?! CounterTooSmallOCERT m n + n <= m + 1 ?! CounterOverIncrementedOCERT m n + where + oc@(OCert vk_hot n c0@(KESPeriod c0_) tau) = Views.hvOCert b + (VKey vkcold) = Views.hvVK b + SlotNo s = Views.hvSlotNo b + hk = hashKey $ Views.hvVK b + kp@(KESPeriod kp_) = if praosSlotsPerKESPeriod == 0 - then error "kesPeriod: slots per KES period was set to zero" - else KESPeriod . fromIntegral $ s `div` praosSlotsPerKESPeriod + then error "kesPeriod: slots per KES period was set to zero" + else KESPeriod . fromIntegral $ s `div` praosSlotsPerKESPeriod - currentIssueNo :: Maybe Word64 - currentIssueNo + currentIssueNo :: Maybe Word64 + currentIssueNo | Map.member hk ocertCounters = Map.lookup hk ocertCounters - | Set.member (coerceKeyRole hk) (Map.keysSet $ SL.unPoolDistr lvPoolDistr) = - Just 0 + | Set.member (coerceKeyRole hk) (Map.keysSet stakeDistribution) = + Just 0 | otherwise = Nothing {------------------------------------------------------------------------------- CannotForge -------------------------------------------------------------------------------} --- | Expresses that, whilst we believe ourselves to be a leader for this slot, --- we are nonetheless unable to forge a block. +{- | Expresses that, whilst we believe ourselves to be a leader for this slot, +we are nonetheless unable to forge a block. +-} data PraosCannotForge c - = -- | The KES key in our operational certificate can't be used because the - -- current (wall clock) period is before the start period of the key. - -- current KES period. - -- - -- Note: the opposite case, i.e., the wall clock period being after the - -- end period of the key, is caught when trying to update the key in - -- 'updateForgeState'. - PraosCannotForgeKeyNotUsableYet - !OCert.KESPeriod - -- ^ Current KES period according to the wallclock slot, i.e., the KES - -- period in which we want to use the key. - !OCert.KESPeriod - -- ^ Start KES period of the KES key. - deriving (Generic) - -deriving instance PraosCrypto c => Show (PraosCannotForge c) + = -- | The KES key in our operational certificate can't be used because the + -- current (wall clock) period is before the start period of the key. + -- current KES period. + -- + -- Note: the opposite case, i.e., the wall clock period being after the + -- end period of the key, is caught when trying to update the key in + -- 'updateForgeState'. + PraosCannotForgeKeyNotUsableYet + -- | Current KES period according to the wallclock slot, i.e., the KES + -- period in which we want to use the key. + !OCert.KESPeriod + -- | Start KES period of the KES key. + !OCert.KESPeriod + deriving (Generic) + +deriving instance (PraosCrypto c) => Show (PraosCannotForge c) praosCheckCanForge :: - ConsensusConfig (Praos c) -> - SlotNo -> - HotKey.KESInfo -> - Either (PraosCannotForge c) () + ConsensusConfig (Praos c) -> + SlotNo -> + HotKey.KESInfo -> + Either (PraosCannotForge c) () praosCheckCanForge - PraosConfig {praosParams} - curSlot - kesInfo - | let startPeriod = HotKey.kesStartPeriod kesInfo, - startPeriod > wallclockPeriod = - throwError $ PraosCannotForgeKeyNotUsableYet wallclockPeriod startPeriod - | otherwise = - return () - where - -- The current wallclock KES period - wallclockPeriod :: OCert.KESPeriod - wallclockPeriod = - OCert.KESPeriod $ - fromIntegral $ - unSlotNo curSlot `div` praosSlotsPerKESPeriod praosParams - + PraosConfig{praosParams} + curSlot + kesInfo + | let startPeriod = HotKey.kesStartPeriod kesInfo + , startPeriod > wallclockPeriod = + throwError $ PraosCannotForgeKeyNotUsableYet wallclockPeriod startPeriod + | otherwise = + return () + where + -- The current wallclock KES period + wallclockPeriod :: OCert.KESPeriod + wallclockPeriod = + OCert.KESPeriod $ + fromIntegral $ + unSlotNo curSlot `div` praosSlotsPerKESPeriod praosParams {------------------------------------------------------------------------------- PraosProtocolSupportsNode -------------------------------------------------------------------------------} -instance PraosCrypto c => PraosProtocolSupportsNode (Praos c) where - type PraosProtocolSupportsNodeCrypto (Praos c) = c - - getPraosNonces _prx cdst = - PraosNonces { - candidateNonce = praosStateCandidateNonce - , epochNonce = praosStateEpochNonce - , evolvingNonce = praosStateEvolvingNonce - , labNonce = praosStateLabNonce - , previousLabNonce = praosStateLastEpochBlockNonce - } - where - PraosState { - praosStateCandidateNonce - , praosStateEpochNonce - , praosStateEvolvingNonce - , praosStateLabNonce - , praosStateLastEpochBlockNonce - } = cdst - - getOpCertCounters _prx cdst = - praosStateOCertCounters - where - PraosState { - praosStateOCertCounters - } = cdst +instance (PraosCrypto c) => PraosProtocolSupportsNode (Praos c) where + type PraosProtocolSupportsNodeCrypto (Praos c) = c + + getPraosNonces _prx cdst = + PraosNonces + { candidateNonce = praosStateCandidateNonce + , epochNonce = praosStateEpochNonce + , evolvingNonce = praosStateEvolvingNonce + , labNonce = praosStateLabNonce + , previousLabNonce = praosStateLastEpochBlockNonce + } + where + PraosState + { praosStateCandidateNonce + , praosStateEpochNonce + , praosStateEvolvingNonce + , praosStateLabNonce + , praosStateLastEpochBlockNonce + } = cdst + + getOpCertCounters _prx cdst = + praosStateOCertCounters + where + PraosState + { praosStateOCertCounters + } = cdst {------------------------------------------------------------------------------- Translation from transitional Praos -------------------------------------------------------------------------------} --- | We can translate between TPraos and Praos, provided: --- --- - They share the same HASH algorithm --- - They share the same ADDRHASH algorithm --- - They share the same DSIGN verification keys --- - They share the same VRF verification keys +{- | We can translate between TPraos and Praos, provided: + +- They share the same HASH algorithm +- They share the same ADDRHASH algorithm +- They share the same DSIGN verification keys +- They share the same VRF verification keys +-} instance - ( c1 ~ c2 ) => - TranslateProto (TPraos c1) (Praos c2) - where - translateLedgerView _ SL.LedgerView {SL.lvPoolDistr, SL.lvChainChecks} = - Views.LedgerView - { Views.lvPoolDistr = coercePoolDistr lvPoolDistr, - Views.lvMaxHeaderSize = SL.ccMaxBHSize lvChainChecks, - Views.lvMaxBodySize = SL.ccMaxBBSize lvChainChecks, - Views.lvProtocolVersion = SL.ccProtocolVersion lvChainChecks - } + (c1 ~ c2) => + TranslateProto (TPraos c1) (Praos c2) + where + translateLedgerView _ SL.LedgerView{SL.lvPoolDistr, SL.lvChainChecks} = + Views.LedgerView + { Views.lvPoolDistr = coercePoolDistr lvPoolDistr + , Views.lvMaxHeaderSize = SL.ccMaxBHSize lvChainChecks + , Views.lvMaxBodySize = SL.ccMaxBBSize lvChainChecks + , Views.lvProtocolVersion = SL.ccProtocolVersion lvChainChecks + } where coercePoolDistr :: SL.PoolDistr c1 -> SL.PoolDistr c2 coercePoolDistr (SL.PoolDistr m totalActiveStake) = - SL.PoolDistr - (Map.mapKeysMonotonic coerce (Map.map coerceIndividualPoolStake m)) - totalActiveStake + SL.PoolDistr + (Map.mapKeysMonotonic coerce (Map.map coerceIndividualPoolStake m)) + totalActiveStake coerceIndividualPoolStake :: SL.IndividualPoolStake c1 -> SL.IndividualPoolStake c2 coerceIndividualPoolStake (SL.IndividualPoolStake stake totalStake vrf) = - SL.IndividualPoolStake stake totalStake (coerce vrf) - - translateChainDepState _ tpState = - PraosState - { praosStateLastSlot = tpraosStateLastSlot tpState, - praosStateOCertCounters = Map.mapKeysMonotonic coerce certCounters, - praosStateEvolvingNonce = evolvingNonce, - praosStateCandidateNonce = candidateNonce, - praosStateEpochNonce = SL.ticknStateEpochNonce csTickn, - praosStateLabNonce = csLabNonce, - praosStateLastEpochBlockNonce = SL.ticknStatePrevHashNonce csTickn - } - where - SL.ChainDepState {SL.csProtocol, SL.csTickn, SL.csLabNonce} = - tpraosStateChainDepState tpState - SL.PrtclState certCounters evolvingNonce candidateNonce = - csProtocol + SL.IndividualPoolStake stake totalStake (coerce vrf) + + translateChainDepState _ tpState = + PraosState + { praosStateLastSlot = tpraosStateLastSlot tpState + , praosStateOCertCounters = Map.mapKeysMonotonic coerce certCounters + , praosStateEvolvingNonce = evolvingNonce + , praosStateCandidateNonce = candidateNonce + , praosStateEpochNonce = SL.ticknStateEpochNonce csTickn + , praosStateLabNonce = csLabNonce + , praosStateLastEpochBlockNonce = SL.ticknStatePrevHashNonce csTickn + } + where + SL.ChainDepState{SL.csProtocol, SL.csTickn, SL.csLabNonce} = + tpraosStateChainDepState tpState + SL.PrtclState certCounters evolvingNonce candidateNonce = + csProtocol {------------------------------------------------------------------------------- Util -------------------------------------------------------------------------------} +-- | Check value and raise error if it is false. (?!) :: Bool -> e -> Except e () a ?! b = unless a $ throwError b diff --git a/ouroboros-consensus-protocol/src/unstable-protocol-testlib/Test/Ouroboros/Consensus/Protocol/Praos/Header.hs b/ouroboros-consensus-protocol/src/unstable-protocol-testlib/Test/Ouroboros/Consensus/Protocol/Praos/Header.hs new file mode 100644 index 0000000000..cbf1e2b21c --- /dev/null +++ b/ouroboros-consensus-protocol/src/unstable-protocol-testlib/Test/Ouroboros/Consensus/Protocol/Praos/Header.hs @@ -0,0 +1,456 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE ImpredicativeTypes #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-missing-export-lists #-} + +module Test.Ouroboros.Consensus.Protocol.Praos.Header where + +import Cardano.Crypto.DSIGN + (DSIGNAlgorithm (SignKeyDSIGN, genKeyDSIGN, rawSerialiseSignKeyDSIGN), + Ed25519DSIGN, deriveVerKeyDSIGN, + rawDeserialiseSignKeyDSIGN) +import Cardano.Crypto.Hash (Blake2b_256, Hash, hash, hashFromBytes, + hashToBytes) +import qualified Cardano.Crypto.KES as KES +import Cardano.Crypto.KES.Class (genKeyKES, rawDeserialiseSignKeyKES, + rawSerialiseSignKeyKES) +import Cardano.Crypto.Seed (mkSeedFromBytes) +import Cardano.Crypto.VRF (deriveVerKeyVRF, hashVerKeyVRF, + rawDeserialiseSignKeyVRF, rawSerialiseSignKeyVRF) +import qualified Cardano.Crypto.VRF as VRF +import Cardano.Crypto.VRF.Praos (skToBatchCompat) +import qualified Cardano.Crypto.VRF.Praos as VRF +import Cardano.Ledger.BaseTypes (ActiveSlotCoeff, + Globals (activeSlotCoeff), Nonce (..), + PositiveUnitInterval, ProtVer (..), Version, activeSlotVal, + boundRational, mkActiveSlotCoeff, natVersion) +import Cardano.Ledger.Binary (MaxVersion, decCBOR, decodeFull', + decodeFullAnnotator, serialize') +import Cardano.Ledger.Keys (KeyHash, KeyRole (BlockIssuer), VKey (..), + hashKey, signedDSIGN) +import Cardano.Protocol.TPraos.BHeader (HashHeader (..), + PrevHash (..), checkLeaderNatValue) +import Cardano.Protocol.TPraos.OCert (KESPeriod (..), OCert (..), + OCertSignable (..)) +import Cardano.Slotting.Block (BlockNo (..)) +import Cardano.Slotting.Slot (SlotNo (..)) +import Data.Aeson ((.:), (.=)) +import qualified Data.Aeson as Json +import Data.Bifunctor (second) +import Data.ByteString (ByteString) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Base16 as Base16 +import qualified Data.ByteString.Lazy as LBS +import Data.Coerce (coerce) +import Data.Foldable (toList) +import qualified Data.Map as Map +import Data.Maybe (fromJust, fromMaybe) +import Data.Proxy (Proxy (..)) +import Data.Ratio ((%)) +import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import Data.Word (Word64) +import Ouroboros.Consensus.Protocol.Praos (PraosValidationErr (..)) +import Ouroboros.Consensus.Protocol.Praos.Header (Header, + HeaderBody (..), pattern Header) +import Ouroboros.Consensus.Protocol.Praos.VRF (InputVRF, mkInputVRF, + vrfLeaderValue) +import Ouroboros.Consensus.Protocol.TPraos (StandardCrypto) +import Test.QuickCheck (Gen, arbitrary, choose, frequency, generate, + getPositive, resize, shrinkList, sized, suchThat, vectorOf) + +-- * Test Vectors + +generateSamples :: Int -> IO Sample +generateSamples n = generate (resize n genSample) + +-- FIXME: Should be defined according to some Era +testVersion :: Version +testVersion = natVersion @MaxVersion + +data Sample = Sample {sample :: ![(GeneratorContext, MutatedHeader)]} + deriving (Show, Eq) + +instance Json.ToJSON Sample where + toJSON Sample{sample} = Json.toJSON sample + +instance Json.FromJSON Sample where + parseJSON = Json.withArray "Sample" $ \arr -> do + Sample . toList <$> traverse Json.parseJSON arr + +genSample :: Gen Sample +genSample = do + context <- genContext + sample <- sized $ \n -> vectorOf n $ genMutatedHeader context + pure $ Sample{sample} + +genMutatedHeader :: GeneratorContext -> Gen (GeneratorContext, MutatedHeader) +genMutatedHeader context = do + mutation <- genMutation + header <- genHeader context + mutate context header mutation + +shrinkSample :: Sample -> [Sample] +shrinkSample Sample{sample} = Sample <$> shrinkList (const []) sample + +mutate :: GeneratorContext -> Header StandardCrypto -> Mutation -> Gen (GeneratorContext, MutatedHeader) +mutate context header mutation = + second (\h -> MutatedHeader{header = h, mutation}) <$> mutated + where + mutated = + case mutation of + NoMutation -> pure (context, header) + MutateKESKey -> do + let Header body _ = header + newKESSignKey <- newKESSigningKey <$> gen32Bytes + KESPeriod kesPeriod <- genValidKESPeriod (hbSlotNo body) praosSlotsPerKESPeriod + let sig' = KES.signKES () kesPeriod body newKESSignKey + pure (context, Header body (KES.SignedKES sig')) + MutateColdKey -> do + let Header body _ = header + newColdSignKey <- genKeyDSIGN . mkSeedFromBytes <$> gen32Bytes + (hbOCert, KESPeriod kesPeriod) <- genCert (hbSlotNo body) context{coldSignKey = newColdSignKey} + let newBody = body{hbOCert} + let sig' = KES.signKES () kesPeriod newBody kesSignKey + pure (context, Header newBody (KES.SignedKES sig')) + MutateKESPeriod -> do + let Header body _ = header + KESPeriod kesPeriod' <- genKESPeriodAfterLimit (hbSlotNo body) praosSlotsPerKESPeriod + let newKESPeriod = KESPeriod kesPeriod' + let oldOCert@OCert{ocertVkHot, ocertN} = hbOCert body + let newBody = + body + { hbOCert = + oldOCert + { ocertKESPeriod = newKESPeriod + , ocertSigma = signedDSIGN @StandardCrypto coldSignKey (OCertSignable ocertVkHot ocertN newKESPeriod) + } + } + let sig' = KES.signKES () kesPeriod' newBody kesSignKey + pure (context, Header newBody (KES.SignedKES sig')) + MutateKESPeriodBefore -> do + let Header body _ = header + let OCert{ocertKESPeriod = KESPeriod kesPeriod} = hbOCert body + newSlotNo <- genSlotAfterKESPeriod (fromIntegral kesPeriod) praosMaxKESEvo praosSlotsPerKESPeriod + let rho' = mkInputVRF newSlotNo nonce + hbVrfRes = VRF.evalCertified () rho' vrfSignKey + newBody = body{hbSlotNo = newSlotNo, hbVrfRes} + sig' = KES.signKES () kesPeriod newBody kesSignKey + pure (context, Header newBody (KES.SignedKES sig')) + MutateCounterOver1 -> do + let poolId = coerce $ hashKey $ VKey $ deriveVerKeyDSIGN coldSignKey + oldCounter = fromMaybe 0 $ Map.lookup poolId (ocertCounters context) + -- FIXME: assumes oldCounter is greater than 1, which is the case in the base generator + -- but is not guaranteed. If oldCounter == 0 then the mutation will fail + newCounter <- choose (0, oldCounter) + let context' = context{ocertCounters = Map.insert poolId newCounter (ocertCounters context)} + pure (context', header) + MutateCounterUnder -> do + let poolId = coerce $ hashKey $ VKey $ deriveVerKeyDSIGN coldSignKey + oldCounter = fromMaybe 0 $ Map.lookup poolId (ocertCounters context) + newCounter <- arbitrary `suchThat` (> oldCounter) + let context' = context{ocertCounters = Map.insert poolId newCounter (ocertCounters context)} + pure (context', header) + GeneratorContext{praosSlotsPerKESPeriod, praosMaxKESEvo, kesSignKey, vrfSignKey, coldSignKey, nonce} = context + +data Mutation + = -- | No mutation + NoMutation + | -- | Mutate the KES key, ie. sign the header with a different KES key. + MutateKESKey + | -- | Mutate the cold key, ie. sign the operational certificate with a different cold key. + MutateColdKey + | -- | Mutate the KES period in the operational certificate to be + -- after the start of the KES period. + MutateKESPeriod + | -- | Mutate KES period to be before the current KES period + MutateKESPeriodBefore + | -- | Mutate certificate counter to be greater than expected + MutateCounterOver1 + | -- | Mutate certificate counter to be lower than expected + MutateCounterUnder + deriving (Eq, Show) + +instance Json.ToJSON Mutation where + toJSON = \case + NoMutation -> "NoMutation" + MutateKESKey -> "MutateKESKey" + MutateColdKey -> "MutateColdKey" + MutateKESPeriod -> "MutateKESPeriod" + MutateKESPeriodBefore -> "MutateKESPeriodBefore" + MutateCounterOver1 -> "MutateCounterOver1" + MutateCounterUnder -> "MutateCounterUnder" + +instance Json.FromJSON Mutation where + parseJSON = \case + "NoMutation" -> pure NoMutation + "MutateKESKey" -> pure MutateKESKey + "MutateColdKey" -> pure MutateColdKey + "MutateKESPeriod" -> pure MutateKESPeriod + "MutateKESPeriodBefore" -> pure MutateKESPeriodBefore + "MutateCounterOver1" -> pure MutateCounterOver1 + "MutateCounterUnder" -> pure MutateCounterUnder + _ -> fail "Invalid mutation" + +expectedError :: Mutation -> (PraosValidationErr StandardCrypto -> Bool) +expectedError = \case + NoMutation -> const False + MutateKESKey -> \case + InvalidKesSignatureOCERT{} -> True + _ -> False + MutateColdKey -> \case + InvalidSignatureOCERT{} -> True + _ -> False + MutateKESPeriod -> \case + KESBeforeStartOCERT{} -> True + _ -> False + MutateKESPeriodBefore -> \case + KESAfterEndOCERT{} -> True + _ -> False + MutateCounterOver1 -> \case + CounterOverIncrementedOCERT{} -> True + _ -> False + MutateCounterUnder -> \case + CounterTooSmallOCERT{} -> True + _ -> False + +genMutation :: Gen Mutation +genMutation = + frequency + [ (4, pure NoMutation) + , (1, pure MutateKESKey) + , (1, pure MutateColdKey) + , (1, pure MutateKESPeriod) + , (1, pure MutateKESPeriodBefore) + , (1, pure MutateCounterOver1) + , (1, pure MutateCounterUnder) + ] + +data MutatedHeader = MutatedHeader + { header :: !(Header StandardCrypto) + , mutation :: !Mutation + } + deriving (Show, Eq) + +instance Json.ToJSON MutatedHeader where + toJSON MutatedHeader{header, mutation} = + Json.object + [ "header" .= cborHeader + , "mutation" .= mutation + ] + where + cborHeader = decodeUtf8 . Base16.encode $ serialize' testVersion header + +instance Json.FromJSON MutatedHeader where + parseJSON = Json.withObject "MutatedHeader" $ \obj -> do + cborHeader <- obj .: "header" + mutation <- obj .: "mutation" + header <- parseHeader cborHeader + pure MutatedHeader{header, mutation} + where + parseHeader cborHeader = do + let headerBytes = Base16.decodeLenient (encodeUtf8 cborHeader) + either (fail . show) pure $ decodeFullAnnotator @(Header StandardCrypto) testVersion "Header" decCBOR $ LBS.fromStrict headerBytes + +-- * Generators +type KESKey = KES.SignKeyKES (KES.Sum6KES Ed25519DSIGN Blake2b_256) + +newVRFSigningKey :: ByteString -> (VRF.SignKeyVRF VRF.PraosVRF, VRF.VerKeyVRF VRF.PraosVRF) +newVRFSigningKey = VRF.genKeyPairVRF . mkSeedFromBytes + +newKESSigningKey :: ByteString -> KESKey +newKESSigningKey = genKeyKES . mkSeedFromBytes + +data GeneratorContext = GeneratorContext + { praosSlotsPerKESPeriod :: !Word64 + , praosMaxKESEvo :: !Word64 + , kesSignKey :: !KESKey + , coldSignKey :: !(SignKeyDSIGN Ed25519DSIGN) + , vrfSignKey :: !(VRF.SignKeyVRF VRF.PraosVRF) + , nonce :: !Nonce + , ocertCounters :: !(Map.Map (KeyHash BlockIssuer StandardCrypto) Word64) + , activeSlotCoeff :: !ActiveSlotCoeff + } + deriving (Show) + +instance Eq GeneratorContext where + a == b = + praosSlotsPerKESPeriod a == praosSlotsPerKESPeriod b + && praosMaxKESEvo a == praosMaxKESEvo b + && serialize' testVersion (kesSignKey a) == serialize' testVersion (kesSignKey b) + && coldSignKey a == coldSignKey b + && vrfSignKey a == vrfSignKey b + && nonce a == nonce b + +instance Json.ToJSON GeneratorContext where + toJSON GeneratorContext{..} = + Json.object + [ "praosSlotsPerKESPeriod" .= praosSlotsPerKESPeriod + , "praosMaxKESEvo" .= praosMaxKESEvo + , "kesSignKey" .= rawKesSignKey + , "coldSignKey" .= rawColdSignKey + , "vrfSignKey" .= rawVrfSignKey + , "vrfVKeyHash" .= rawVrVKeyHash + , "nonce" .= rawNonce + , "ocertCounters" .= ocertCounters + , "activeSlotCoeff" .= activeSlotVal activeSlotCoeff + ] + where + rawKesSignKey = decodeUtf8 . Base16.encode $ rawSerialiseSignKeyKES kesSignKey + rawColdSignKey = decodeUtf8 . Base16.encode $ rawSerialiseSignKeyDSIGN coldSignKey + rawVrfSignKey = decodeUtf8 . Base16.encode $ rawSerialiseSignKeyVRF $ skToBatchCompat vrfSignKey + rawVrVKeyHash = decodeUtf8 . Base16.encode $ hashToBytes $ hashVerKeyVRF @_ @Blake2b_256 $ deriveVerKeyVRF vrfSignKey + rawNonce = case nonce of + NeutralNonce -> decodeUtf8 . Base16.encode $ BS.replicate 32 0 + Nonce hashNonce -> decodeUtf8 . Base16.encode $ hashToBytes hashNonce + +instance Json.FromJSON GeneratorContext where + parseJSON = Json.withObject "GeneratorContext" $ \obj -> do + praosSlotsPerKESPeriod <- obj .: "praosSlotsPerKESPeriod" + praosMaxKESEvo <- obj .: "praosMaxKESEvo" + rawKesSignKey <- obj .: "kesSignKey" + rawColdSignKey <- obj .: "coldSignKey" + rawVrfSignKey <- obj .: "vrfSignKey" + cborNonce <- obj .: "nonce" + ocertCounters <- obj .: "ocertCounters" + kesSignKey <- parseKesSignKey rawKesSignKey + coldSignKey <- parseColdSignKey rawColdSignKey + vrfSignKey <- parseVrfSignKey rawVrfSignKey + nonce <- parseNonce cborNonce + activeSlotCoeff <- mkActiveSlotCoeff <$> obj .: "activeSlotCoeff" + pure GeneratorContext{..} + where + parseNonce rawNonce = + case Base16.decode (encodeUtf8 rawNonce) of + Left _ -> pure NeutralNonce + Right nonceBytes -> Nonce <$> maybe (fail "invalid bytes for hash") pure (hashFromBytes nonceBytes) + parseColdSignKey rawKey = do + case Base16.decode (encodeUtf8 rawKey) of + Left err -> fail err + Right keyBytes -> + case rawDeserialiseSignKeyDSIGN keyBytes of + Nothing -> fail $ "Invalid cold key bytes: " <> show rawKey + Just key -> pure key + parseKesSignKey rawKey = do + case Base16.decode (encodeUtf8 rawKey) of + Left err -> fail err + Right keyBytes -> + case rawDeserialiseSignKeyKES keyBytes of + Nothing -> fail $ "Invalid KES key bytes: " <> show rawKey + Just key -> pure key + parseVrfSignKey rawKey = do + case Base16.decode (encodeUtf8 rawKey) of + Left err -> fail err + Right keyBytes -> + case rawDeserialiseSignKeyVRF keyBytes of + Nothing -> fail $ "Invalid VRF key bytes: " <> show rawKey + Just key -> pure key + +genContext :: Gen GeneratorContext +genContext = do + praosSlotsPerKESPeriod <- choose (100, 10000) + praosMaxKESEvo <- choose (10, 1000) + ocertCounter <- choose (10, 100) + kesSignKey <- newKESSigningKey <$> gen32Bytes + coldSignKey <- genKeyDSIGN . mkSeedFromBytes <$> gen32Bytes + vrfSignKey <- fst <$> newVRFSigningKey <$> gen32Bytes + nonce <- Nonce <$> genHash + let poolId = coerce $ hashKey $ VKey $ deriveVerKeyDSIGN coldSignKey + ocertCounters = Map.fromList [(poolId, ocertCounter)] + activeSlotCoeff <- genActiveSlotCoeff + pure $ GeneratorContext{..} + +genActiveSlotCoeff :: Gen ActiveSlotCoeff +genActiveSlotCoeff = do + choose (1, 100) >>= \n -> pure $ activeSlotCoeff (n % 100) + where + activeSlotCoeff = mkActiveSlotCoeff . fromJust . boundRational @PositiveUnitInterval + +{- | Generate a well-formed header + +The header is signed with the KES key, and all the signing keys +generated for the purpose of producing the header are returned. +-} +genHeader :: GeneratorContext -> Gen (Header StandardCrypto) +genHeader context = do + (body, KESPeriod kesPeriod) <- genHeaderBody context + let sign = KES.SignedKES $ KES.signKES () kesPeriod body kesSignKey + pure $ (Header body sign) + where + GeneratorContext{kesSignKey} = context + +genHeaderBody :: GeneratorContext -> Gen (HeaderBody StandardCrypto, KESPeriod) +genHeaderBody context = do + hbBlockNo <- BlockNo <$> arbitrary + (hbSlotNo, hbVrfRes, hbVrfVk) <- genLeadingSlot context + hbPrev <- BlockHash . HashHeader <$> genHash + let hbVk = VKey $ deriveVerKeyDSIGN coldSignKey + hbBodySize <- choose (1000, 90000) + hbBodyHash <- genHash + (hbOCert, kesPeriod) <- genCert hbSlotNo context + let hbProtVer = protocolVersionZero + headerBody = HeaderBody{..} + pure $ (headerBody, kesPeriod) + where + GeneratorContext{coldSignKey} = context + +genLeadingSlot :: GeneratorContext -> Gen (SlotNo, VRF.CertifiedVRF VRF.PraosVRF InputVRF, VRF.VerKeyVRF VRF.PraosVRF) +genLeadingSlot context = do + slotNo <- SlotNo . getPositive <$> arbitrary `suchThat` isLeader + let rho' = mkInputVRF slotNo nonce + hbVrfRes = VRF.evalCertified () rho' vrfSignKey + hbVrfVk = deriveVerKeyVRF vrfSignKey + pure (slotNo, hbVrfRes, hbVrfVk) + where + isLeader n = + let slotNo = SlotNo . getPositive $ n + rho' = mkInputVRF slotNo nonce + certified = VRF.evalCertified () rho' vrfSignKey + in checkLeaderNatValue (vrfLeaderValue (Proxy @StandardCrypto) certified) sigma activeSlotCoeff + sigma = 1 + GeneratorContext{vrfSignKey, nonce, activeSlotCoeff} = context + +protocolVersionZero :: ProtVer +protocolVersionZero = ProtVer versionZero 0 + where + versionZero :: Version + versionZero = natVersion @0 + +genCert :: SlotNo -> GeneratorContext -> Gen (OCert StandardCrypto, KESPeriod) +genCert slotNo context = do + let ocertVkHot = KES.deriveVerKeyKES kesSignKey + poolId = coerce $ hashKey $ VKey $ deriveVerKeyDSIGN coldSignKey + ocertN = fromMaybe 0 $ Map.lookup poolId ocertCounters + ocertKESPeriod <- genValidKESPeriod slotNo praosSlotsPerKESPeriod + let ocertSigma = signedDSIGN @StandardCrypto coldSignKey (OCertSignable ocertVkHot ocertN ocertKESPeriod) + pure (OCert{..}, ocertKESPeriod) + where + GeneratorContext{kesSignKey, praosSlotsPerKESPeriod, coldSignKey, ocertCounters} = context + +genValidKESPeriod :: SlotNo -> Word64 -> Gen KESPeriod +genValidKESPeriod slotNo praosSlotsPerKESPeriod = + pure $ KESPeriod $ fromIntegral $ unSlotNo slotNo `div` praosSlotsPerKESPeriod + +genKESPeriodAfterLimit :: SlotNo -> Word64 -> Gen KESPeriod +genKESPeriodAfterLimit slotNo praosSlotsPerKESPeriod = + KESPeriod . fromIntegral <$> arbitrary `suchThat` (> currentKESPeriod) + where + currentKESPeriod = unSlotNo slotNo `div` praosSlotsPerKESPeriod + +genSlotAfterKESPeriod :: Word64 -> Word64 -> Word64 -> Gen SlotNo +genSlotAfterKESPeriod ocertKESPeriod praosMaxKESEvo praosSlotsPerKESPeriod = + -- kp_ < c0_ + praosMaxKESEvo + -- ! => + -- kp >= c0_ + praosMaxKESEvo + -- c0 <= kp - praosMaxKESEvo + SlotNo <$> arbitrary `suchThat` (> (ocertKESPeriod + praosMaxKESEvo) * praosSlotsPerKESPeriod) + +genHash :: Gen (Hash Blake2b_256 a) +genHash = coerce . hash <$> gen32Bytes + +gen32Bytes :: Gen ByteString +gen32Bytes = BS.pack <$> vectorOf 32 arbitrary