diff --git a/ouroboros-consensus-cardano/app/GenHeader/Parsers.hs b/ouroboros-consensus-cardano/app/GenHeader/Parsers.hs new file mode 100644 index 0000000000..34058a8bef --- /dev/null +++ b/ouroboros-consensus-cardano/app/GenHeader/Parsers.hs @@ -0,0 +1,46 @@ +module GenHeader.Parsers (parseOptions) 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 89998def89..df32ce0c7f 100644 --- a/ouroboros-consensus-cardano/ouroboros-consensus-cardano.cabal +++ b/ouroboros-consensus-cardano/ouroboros-consensus-cardano.cabal @@ -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 @@ -552,6 +553,7 @@ library unstable-cardano-tools ouroboros-consensus ^>=0.21, ouroboros-consensus-cardano, ouroboros-consensus-diffusion ^>=0.18, + ouroboros-consensus-protocol:unstable-protocol-testlib, ouroboros-consensus-protocol ^>=0.9, ouroboros-network, ouroboros-network-api, @@ -662,9 +664,35 @@ test-suite tools-test hs-source-dirs: test/tools-test main-is: Main.hs build-depends: + QuickCheck, + 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, 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 + + autogen-modules: + 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..f98b8b60c5 --- /dev/null +++ b/ouroboros-consensus-cardano/src/unstable-cardano-tools/Cardano/Tools/Headers.hs @@ -0,0 +1,81 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} + +-- | Tooling to generate and validate (Praos) headers. +module Cardano.Tools.Headers ( + Options (..) + , ValidationResult (..) + , run + , validate + ) where + +import Cardano.Crypto.DSIGN (deriveVerKeyDSIGN) +import Cardano.Crypto.VRF + (VRFAlgorithm (deriveVerKeyVRF, hashVerKeyVRF)) +import Cardano.Ledger.Api (ConwayEra, StandardCrypto) +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.Lazy as LBS +import qualified Data.Map as Map +import Data.Maybe (fromJust) +import Ouroboros.Consensus.Block (validateView) +import Ouroboros.Consensus.Protocol.Praos (Praos, + doValidateKESSignature, doValidateVRFSignature) +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, praosMaxKESEvo, nonce, coldSignKey, vrfSignKey, ocertCounters, activeSlotCoeff} = context + -- TODO: get these from the context + 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 praosMaxKESEvo praosSlotsPerKESPeriod poolDistr ocertCounters headerView + validateVRF = doValidateVRFSignature nonce poolDistr activeSlotCoeff headerView diff --git a/ouroboros-consensus-cardano/test/tools-test/Main.hs b/ouroboros-consensus-cardano/test/tools-test/Main.hs index 66e59c3d5d..0ff98843e6 100644 --- a/ouroboros-consensus-cardano/test/tools-test/Main.hs +++ b/ouroboros-consensus-cardano/test/tools-test/Main.hs @@ -8,6 +8,7 @@ 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 @@ -114,6 +115,7 @@ tests :: TestTree tests = testGroup "cardano-tools" [ testCaseSteps "synthesize and analyse: blockCount\n" blockCountTest + , Test.Cardano.Tools.Headers.tests ] main :: IO () 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..d259f30c28 --- /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/changelog.d/20241029_062000_abailly_header_validation_test.md b/ouroboros-consensus-protocol/changelog.d/20241029_062000_abailly_header_validation_test.md new file mode 100644 index 0000000000..17497d3350 --- /dev/null +++ b/ouroboros-consensus-protocol/changelog.d/20241029_062000_abailly_header_validation_test.md @@ -0,0 +1,4 @@ +### Patch + +- Expose functions to simplify thorough testing of header validation + logic, and introduce generators and properties to actually test it. diff --git a/ouroboros-consensus-protocol/ouroboros-consensus-protocol.cabal b/ouroboros-consensus-protocol/ouroboros-consensus-protocol.cabal index 45fc293847..8e04317c92 100644 --- a/ouroboros-consensus-protocol/ouroboros-consensus-protocol.cabal +++ b/ouroboros-consensus-protocol/ouroboros-consensus-protocol.cabal @@ -84,16 +84,27 @@ 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..3ddbcf200b 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) @@ -345,6 +348,7 @@ data PraosValidationErr c !Word -- current KES Period !Word -- KES start period !Word -- expected KES evolutions + !Word64 -- max KES evolutions !String -- error message given by Consensus Layer | NoCounterForKeyHashOCERT !(KeyHash 'BlockIssuer c) -- stake pool key hash @@ -527,7 +531,20 @@ validateVRFSignature :: ActiveSlotCoeff -> Views.HeaderView c -> Except (PraosValidationErr c) () -validateVRFSignature eta0 (Views.lvPoolDistr -> SL.PoolDistr pd _) f b = do +validateVRFSignature eta0 (Views.lvPoolDistr -> SL.PoolDistr pd _) = + doValidateVRFSignature eta0 pd + +-- 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 @@ -557,12 +574,25 @@ validateKESSignature :: Except (PraosValidationErr c) () validateKESSignature _cfg@( PraosConfig - PraosParams {praosMaxKESEvo, praosSlotsPerKESPeriod} - _ei - ) - Views.LedgerView {Views.lvPoolDistr} - ocertCounters - b = do + 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 @@ -573,7 +603,7 @@ validateKESSignature 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 + InvalidKesSignatureOCERT kp_ c0_ t praosMaxKESEvo case currentIssueNo of Nothing -> do @@ -594,7 +624,7 @@ validateKESSignature currentIssueNo :: Maybe Word64 currentIssueNo | Map.member hk ocertCounters = Map.lookup hk ocertCounters - | Set.member (coerceKeyRole hk) (Map.keysSet $ SL.unPoolDistr lvPoolDistr) = + | Set.member (coerceKeyRole hk) (Map.keysSet stakeDistribution) = Just 0 | otherwise = Nothing @@ -727,6 +757,7 @@ instance 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..09c9f65c6e --- /dev/null +++ b/ouroboros-consensus-protocol/src/unstable-protocol-testlib/Test/Ouroboros/Consensus/Protocol/Praos/Header.hs @@ -0,0 +1,456 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE ImpredicativeTypes #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeApplications #-} + +module Test.Ouroboros.Consensus.Protocol.Praos.Header ( + GeneratorContext (..) + , MutatedHeader (..) + , Mutation (..) + , Sample (..) + , expectedError + , genContext + , genMutatedHeader + , genSample + , generateSamples + ) where + +import Cardano.Crypto.DSIGN + (DSIGNAlgorithm (SignKeyDSIGN, genKeyDSIGN, rawSerialiseSignKeyDSIGN), + Ed25519DSIGN, deriveVerKeyDSIGN, + rawDeserialiseSignKeyDSIGN) +import Cardano.Crypto.Hash (Blake2b_256, Hash, hashFromBytes, + hashToBytes, hashWith) +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, Nonce (..), + PositiveUnitInterval, ProtVer (..), Version, activeSlotVal, + boundRational, mkActiveSlotCoeff, natVersion) +import Cardano.Ledger.Binary (MaxVersion, decCBOR, + 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 (defaultOptions, (.:), (.=)) +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 GHC.Generics (Generic) +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, 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 + +newtype 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 + header <- genHeader context + mutation <- genMutation header + mutate context header mutation + +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 + OCert{ocertKESPeriod = KESPeriod kesPeriod} = hbOCert body + newSlotNo <- genSlotAfterKESPeriod (fromIntegral kesPeriod) praosMaxKESEvo praosSlotsPerKESPeriod + let rho' = mkInputVRF newSlotNo nonce + period' = unSlotNo newSlotNo `div` praosSlotsPerKESPeriod + hbVrfRes = VRF.evalCertified () rho' vrfSignKey + newBody = body{hbSlotNo = newSlotNo, hbVrfRes} + sig' = KES.signKES () (fromIntegral period' - kesPeriod) newBody kesSignKey + pure (context, Header newBody (KES.SignedKES sig')) + MutateCounterOver1 -> do + let poolId = coerce $ hashKey $ VKey $ deriveVerKeyDSIGN coldSignKey + Header body _ = header + OCert{ocertN} = hbOCert body + newCounter <- choose (0, ocertN - 2) + 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, Generic) + +instance Json.ToJSON Mutation where + toEncoding = Json.genericToEncoding defaultOptions + +instance Json.FromJSON 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 :: Header StandardCrypto -> Gen Mutation +genMutation header = + frequency $ + [ (4, pure NoMutation) + , (1, pure MutateKESKey) + , (1, pure MutateColdKey) + , (1, pure MutateKESPeriod) + , (1, pure MutateKESPeriodBefore) + , (1, pure MutateCounterUnder) + ] + <> maybeCounterOver1 + where + Header body _ = header + OCert{ocertN} = hbOCert body + maybeCounterOver1 = + if ocertN > 10 + then [(1, pure MutateCounterOver1)] + else [] + +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 = do + -- kp_ < c0_ + praosMaxKESEvo + -- ! => + -- kp >= c0_ + praosMaxKESEvo + -- c0 <= kp - praosMaxKESEvo + SlotNo <$> arbitrary `suchThat` (> threshold) + where + threshold = (ocertKESPeriod + praosMaxKESEvo + 1) * praosSlotsPerKESPeriod + +genHash :: Gen (Hash Blake2b_256 a) +genHash = coerce . hashWith id <$> gen32Bytes + +gen32Bytes :: Gen ByteString +gen32Bytes = BS.pack <$> vectorOf 32 arbitrary