Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Praos headers validation properties and generators #1285

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
23 changes: 23 additions & 0 deletions ouroboros-consensus-cardano/app/GenHeader/Parsers.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module GenHeader.Parsers where

import Cardano.Tools.Headers (Options (..))
import Data.Version (showVersion)
import Options.Applicative (Parser, ParserInfo, execParser, helper, info, progDesc, (<**>))
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 = pure Options
12 changes: 12 additions & 0 deletions ouroboros-consensus-cardano/app/gen-header.hs
Original file line number Diff line number Diff line change
@@ -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
27 changes: 25 additions & 2 deletions ouroboros-consensus-cardano/ouroboros-consensus-cardano.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.13.2,
Expand Down Expand Up @@ -661,9 +663,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeApplications #-}

-- | Tooling to generate and validate (Praos) headers.
module Cardano.Tools.Headers where

import Cardano.Crypto.DSIGN (deriveVerKeyDSIGN)
import Cardano.Crypto.VRF (VRFAlgorithm (deriveVerKeyVRF, hashVerKeyVRF))
import Cardano.Ledger.Api (ConwayEra, StandardCrypto)
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.Protocol.TPraos.OCert (ocertN)
import Control.Monad.Except (runExcept)
import qualified Data.Aeson as Json
import qualified Data.ByteString.Lazy as LBS
import Data.Char (isSpace)
import qualified Data.Map as Map
import Data.Maybe (fromJust)
import qualified Data.Text as Text
import Ouroboros.Consensus.Block (validateView)
import Ouroboros.Consensus.Protocol.Praos (
Praos,
doValidateKESSignature,
doValidateVRFSignature,
)
import Ouroboros.Consensus.Protocol.Praos.Header (Header, hbOCert, pattern Header)
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 = Options

run :: Options -> IO ()
run Options = do
sample <- generateSamples
LBS.putStr $ Json.encode sample <> "\n"

data ValidationResult = Valid Mutation | Invalid Mutation String
deriving (Eq, Show)

validate :: GeneratorContext -> MutatedHeader -> ValidationResult
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a sense yet of how difficult it would be to reuse the implementation's validation logic instead of re-specifying it here? Maybe we could do some rearranging upstream (eg factor out a "worker") to make it easier to invoke in this incomplete calling context.

Unless you were intending for this code to be independent?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Esgen also mentioned that maybe the Agda spec would be amenable to reuse here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EG you're missing the chainChecks and the opcert checks. But I suppose that makes sense considering you're not so much interested in the actual incoming ledger/header state.

But maybe your generator could also generate a header state and the ledger view alongside each header? IE all three ingredients necessary to invoke Ouroboros.Consensus.HeaderValidation.validateHeader.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the validate function in its current form is cumbersome, but ultimately it should just be a thin wrapper over calling the VRF and KES validation functions from Praos module. Creating the necessary environment to directly call validateHeader seemed like a more ambitious task at this stage, and something that could be done in a later PR to avoid "scope creep" as there really are 2 independent stages in the header validation.

re Agda spec: that's definitely something I think would be interesting to reuse as testing against an independent specification of the validation logic makes a lot of sense. This means the specification should be modular enough and separate the validation steps perhaps?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re Agda spec: that's definitely something I think would be interesting to reuse as testing against an independent specification of the validation logic makes a lot of sense. This means the specification should be modular enough and separate the validation steps perhaps?

The spec is structured in STS style, with a DAG of rules, see e.g. Figure 6 in the Agda spec here. I guess that when exposing the spec as runnable Haskell functions (which I think shouldn't be that far off after #1270), this could be modular to e.g. have one function per STS sub-rule.1

While being more granular that what is present in the current implementation, there is not directly something that corresponds exactly to VRF + KES validation. OCERT corresponds to KES validation, and PRTCL does VRF validation, but also a few more things like nonce evolution (not very complicated).

Footnotes

  1. (Of course, we could also structure the existing Haskell code in such a way that different STS sub-rules can be invoked independently.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the links. Of course, modelling the tests after the specification's transitions or logic rules would be desirable. BTW, having a hyperlinked HTML rendering of the Agda spec would be much more convenient than a PDF. Or did I miss the link somehow?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, having a hyperlinked HTML rendering of the Agda spec would be much more convenient than a PDF. Or did I miss the link somehow?

Good point, we don't have that ATM, might to it later when I find the time 👍

validate context MutatedHeader{header, mutation} =
case (runExcept $ validateKES >> validateVRF, mutation) of
(Left err, mut) | ctor err == expectedError mut -> Valid mut
abailly marked this conversation as resolved.
Show resolved Hide resolved
(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} = context
-- TODO: get these from the context
maxKESEvo = 63
coin = fromJust . toCompact . Coin
slotCoeff = mkActiveSlotCoeff $ fromJust $ boundRational @PositiveUnitInterval $ 1
ownsAllStake vrfKey = IndividualPoolStake 1 (coin 1) vrfKey
poolDistr = Map.fromList [(poolId, ownsAllStake hashVRFKey)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps helpful to note that the generator context doesn't necessarily need a full stake distribution: it only needs the relative stake of the vrf keys in the headers you expect that context to validate.

poolId = hashKey $ VKey $ deriveVerKeyDSIGN coldSignKey
hashVRFKey = hashVerKeyVRF $ deriveVerKeyVRF vrfSignKey
Header body _ = header
certCounter = ocertN . hbOCert $ body
ocertCounters = Map.fromList [(poolId, certCounter)]

headerView = validateView @ConwayBlock undefined (mkShelleyHeader header)
validateKES = doValidateKESSignature maxKESEvo praosSlotsPerKESPeriod poolDistr ocertCounters headerView
validateVRF = doValidateVRFSignature nonce poolDistr slotCoeff headerView

ctor :: (Show e) => e -> String
ctor err = Text.unpack $ head $ concatMap (Text.split isSpace) $ Text.split (== '(') $ Text.pack $ show err
23 changes: 13 additions & 10 deletions ouroboros-consensus-cardano/test/shelley-test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@ import qualified Test.Consensus.Shelley.Coherence (tests)
import qualified Test.Consensus.Shelley.Golden (tests)
import qualified Test.Consensus.Shelley.Serialisation (tests)
import qualified Test.Consensus.Shelley.SupportedNetworkProtocolVersion (tests)
import Test.Tasty
import Test.Tasty
import qualified Test.ThreadNet.Shelley (tests)
import Test.Util.TestEnv (defaultMainWithTestEnv,
defaultTestEnvConfig)
import Test.Util.TestEnv (
defaultMainWithTestEnv,
defaultTestEnvConfig,
)

main :: IO ()
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
]
Loading
Loading