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 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
006d94b
Add module to generate headers
abailly-iohk Oct 16, 2024
71437d3
Make header validation test compile (and fail)
abailly-iohk Oct 17, 2024
f44e7f5
Can validate KES signature part of a valid header
abailly-iohk Oct 17, 2024
aaf3233
Can Validate VRF proof for a pool owning 100% of the stake
abailly-iohk Oct 17, 2024
9e9b6f0
Introduce very basic faulty headers test
abailly-iohk Oct 17, 2024
0527f6d
Separate context generation from header generation
abailly-iohk Oct 18, 2024
fa97fdd
Scaffold executable to generate headers
abailly-iohk Oct 18, 2024
93b16f1
Ensure test vectors can be serialised to/from JSON
abailly-iohk Oct 18, 2024
3cf5afd
Restructure code to separate IOs from "pure" generators
abailly-iohk Oct 18, 2024
80904b8
Mutate KES signature with different key than expected
abailly-iohk Oct 18, 2024
112a983
Better classification of test samples
abailly-iohk Oct 18, 2024
77c4d76
Mutate header signing cert with a different cold key
abailly-iohk Oct 18, 2024
c1f9253
Provide simple 'generate' and 'validate' commands
abailly-iohk Oct 18, 2024
499be18
Mutate KES period of certificate to be after current period
abailly-iohk Oct 18, 2024
c92d8b6
Mutate KES period of certificate to be too old (before current)
abailly-iohk Oct 19, 2024
210c1ac
Remove dead code
abailly-iohk Oct 19, 2024
167ec92
Can mutate both context and header
abailly-iohk Oct 19, 2024
aa8bc4d
Add operational certificate counters in generator context
abailly-iohk Oct 19, 2024
21f90ab
Mutate context's operational counter to be lower than certificate
abailly-iohk Oct 19, 2024
18c3279
Mutate context's operational counter to be greater than certificate's
abailly-iohk Oct 19, 2024
c0baef4
Replace CBOR encoding with raw encoding for context data
abailly-iohk Oct 20, 2024
abc4796
Use Hex encoding instead of Base64 in JSON
abailly-iohk Oct 22, 2024
2105bfe
Generate active slot coefficient as part of test context
abailly-iohk Oct 23, 2024
3ff32df
Add some documentation for (?!) combinator
abailly-iohk Oct 24, 2024
fbfdb6e
Format code using script
abailly Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions ouroboros-consensus-cardano/app/GenHeader/Parsers.hs
Original file line number Diff line number Diff line change
@@ -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"
)
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.14,
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
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) | 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)]
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

headerView = validateView @ConwayBlock undefined (mkShelleyHeader header)
validateKES = doValidateKESSignature maxKESEvo praosSlotsPerKESPeriod poolDistr ocertCounters headerView
validateVRF = doValidateVRFSignature nonce poolDistr activeSlotCoeff headerView
15 changes: 8 additions & 7 deletions ouroboros-consensus-cardano/test/shelley-test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
116 changes: 62 additions & 54 deletions ouroboros-consensus-cardano/test/tools-test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
Loading
Loading