Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions cardano-node-chairman/test/Spec/Chairman/Cardano.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ hprop_chairman :: H.Property
hprop_chairman = integrationRetryWorkspace 2 "cardano-chairman" $ \tempAbsPath' -> H.runWithDefaultWatchdog_ $ do
conf <- mkConf tempAbsPath'

let testnetOptions = def{ cardanoNodes = SpoNodeOptions [] :| [RelayNodeOptions [], RelayNodeOptions []] }
allNodes <- testnetNodes <$> createAndRunTestnet testnetOptions def conf
let creationOptions = def{ creationNodes = SpoNodeOptions [] :| [RelayNodeOptions [], RelayNodeOptions []] }
allNodes <- testnetNodes <$> createAndRunTestnet creationOptions def conf

chairmanOver 120 50 conf allNodes
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

### Removed

- Removed `CardanoTestnetOptions` type and `CreateEnvOptions` wrapper (replaced by purpose-specific types).
- Removed dead fields `cardanoNodeLoggingFormat` and `cardanoOutputDir`.

### Changed

- Split `CardanoTestnetOptions` into `TestnetCreationOptions` and `TestnetRuntimeOptions` so each function receives only the fields it uses.
- `CardanoTestnetCliOptions` is now a sum type (`StartFromScratch | StartFromEnv`), making `--node-env` and `--num-pool-nodes` structurally mutually exclusive in the CLI parser.
- Simplified `CardanoTestnetCreateEnvOptions` and `createTestnetEnv` signatures (fewer arguments, genesis options and on-chain params folded into `TestnetCreationOptions`).

### Added

- `readNodeOptionsFromEnv`: scans an existing testnet environment directory to classify nodes as SPO or relay.

4 changes: 3 additions & 1 deletion cardano-testnet/src/Cardano/Testnet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ module Cardano.Testnet (
retryOnAddressInUseError,

-- ** Testnet options
CardanoTestnetOptions(..),
TestnetCreationOptions(..),
TestnetRuntimeOptions(..),
TestnetEnvOptions(..),
RpcSupport(..),
NodeOption(..),
cardanoDefaultTestnetNodeOptions,
Expand Down
149 changes: 78 additions & 71 deletions cardano-testnet/src/Parsers/Cardano.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ module Parsers.Cardano
) where

import Cardano.Api (AnyShelleyBasedEra (..))
import Cardano.Api.Pretty

import Cardano.CLI.EraBased.Common.Option hiding (pNetworkId)
import Cardano.Prelude (readMaybe)
Expand All @@ -26,73 +25,93 @@ import Options.Applicative.Types (readerAsk)
import Testnet.Defaults (defaultEra)
import Testnet.Start.Cardano
import Testnet.Start.Types
import Testnet.Types (readNodeLoggingFormat)

optsTestnet :: Parser CardanoTestnetCliOptions
optsTestnet = CardanoTestnetCliOptions
<$> pCardanoTestnetCliOptions
<*> pGenesisOptions
<*> pNodeEnvironment
<*> pUpdateTimestamps
<*> pOnChainParams
optsTestnet = mkCliOptions <$> pModeOptions <*> pRuntimeOptions
where
pModeOptions =
Left <$> pFromEnv
<|> Right <$> ((,) <$> pFromScratch <*> pScratchOutputDir)
mkCliOptions (Left envOpts) rt = StartFromEnv (StartFromEnvOptions envOpts rt)
mkCliOptions (Right (creation, outDir)) rt = StartFromScratch (StartFromScratchOptions creation outDir rt)

optsCreateTestnet :: Parser CardanoTestnetCreateEnvOptions
optsCreateTestnet = CardanoTestnetCreateEnvOptions
<$> pCardanoTestnetCliOptions
<*> pGenesisOptions
<$> pCreationOptions
<*> pEnvOutputDir
<*> pCreateEnvOptions

-- We can't fill in the optional Genesis files at parse time, because we want to be in a monad
-- to properly parse JSON. We delegate this task to the caller.
pCreateEnvOptions :: Parser CreateEnvOptions
pCreateEnvOptions = CreateEnvOptions
<$> pOnChainParams
pFromEnv :: Parser TestnetEnvOptions
pFromEnv = TestnetEnvOptions
<$> OA.strOption
( OA.long "node-env"
<> OA.metavar "FILEPATH"
<> OA.help "Path to the node's environment (which is generated otherwise). You can generate a default environment with the 'create-env' command, then modify it and pass it with this argument."
)
<*> pUpdateTimestamps

pCardanoTestnetCliOptions :: Parser CardanoTestnetOptions
pCardanoTestnetCliOptions = CardanoTestnetOptions
pFromScratch :: Parser TestnetCreationOptions
pFromScratch = TestnetCreationOptions
<$> pTestnetNodeOptions
<*> pure (AnyShelleyBasedEra defaultEra)
<*> pMaxLovelaceSupply
<*> OA.option (OA.eitherReader readNodeLoggingFormat)
( OA.long "node-logging-format"
<> OA.help "Node logging format (json|text)"
<> OA.metavar "LOGGING_FORMAT"
<> OA.showDefaultWith prettyShow
<> OA.value (cardanoNodeLoggingFormat def)
)
<*> OA.option OA.auto
( OA.long "num-dreps"
<> OA.help "Number of delegate representatives (DReps) to generate. Ignored if a node environment is passed."
<> OA.metavar "NUMBER"
<> OA.showDefault
<> OA.value 3
)
<*> OA.switch
( OA.long "enable-new-epoch-state-logging"
<> OA.help "Enable new epoch state logging to logs/ledger-epoch-state.log"
<> OA.showDefault
)
<*> (maybe NoUserProvidedEnv UserProvidedEnv <$> optional (OA.strOption
( OA.long "output-dir"
<> OA.help "Directory where to store files, sockets, and so on. It is created if it doesn't exist. If unset, a temporary directory is used."
<> OA.metavar "DIRECTORY"
)))
<*> OA.flag RpcDisabled RpcEnabled
( OA.long "enable-grpc"
<> OA.help "[EXPERIMENTAL] Enable gRPC endpoint on all of testnet nodes. The listening socket file will be the same directory as node's N2C socket."
<> OA.showDefault
)
<*> OA.flag UseKesKeyFile UseKesSocket
( OA.long "use-kes-agent"
<> OA.help "Get Praos block forging credentials from kes-agent via the default socket path"
<> OA.showDefault
)
<*> pNumDReps
<*> pGenesisOptions
<*> pOnChainParams

pCreationOptions :: Parser TestnetCreationOptions
pCreationOptions = TestnetCreationOptions
<$> pTestnetNodeOptions
<*> pure (AnyShelleyBasedEra defaultEra)
<*> pMaxLovelaceSupply
<*> pNumDReps
<*> pGenesisOptions
<*> pOnChainParams

pRuntimeOptions :: Parser TestnetRuntimeOptions
pRuntimeOptions = TestnetRuntimeOptions
<$> pEnableNewEpochStateLogging
<*> pEnableRpc
<*> pKesSource

pScratchOutputDir :: Parser (Maybe FilePath)
pScratchOutputDir = optional $ OA.strOption
( OA.long "output-dir"
<> OA.help "Directory where to store files, sockets, and so on. It is created if it doesn't exist. If unset, a temporary directory is used."
<> OA.metavar "DIRECTORY"
)

pNumDReps :: Parser NumDReps
pNumDReps = OA.option OA.auto
( OA.long "num-dreps"
<> OA.help "Number of delegate representatives (DReps) to generate."
<> OA.metavar "NUMBER"
<> OA.showDefault
<> OA.value 3
)

pEnableNewEpochStateLogging :: Parser Bool
pEnableNewEpochStateLogging = OA.switch
( OA.long "enable-new-epoch-state-logging"
<> OA.help "Enable new epoch state logging to logs/ledger-epoch-state.log"
<> OA.showDefault
)

pEnableRpc :: Parser RpcSupport
pEnableRpc = OA.flag RpcDisabled RpcEnabled
( OA.long "enable-grpc"
<> OA.help "[EXPERIMENTAL] Enable gRPC endpoint on all of testnet nodes. The listening socket file will be the same directory as node's N2C socket."
<> OA.showDefault
)

pKesSource :: Parser PraosCredentialsSource
pKesSource = OA.flag UseKesKeyFile UseKesSocket
( OA.long "use-kes-agent"
<> OA.help "Get Praos block forging credentials from kes-agent via the default socket path"
<> OA.showDefault
)

pTestnetNodeOptions :: Parser (NonEmpty NodeOption)
pTestnetNodeOptions =
-- If `--num-pool-nodes N` is present, return N nodes with option `SpoNodeOptions []`.
-- Otherwise, return `cardanoDefaultTestnetNodeOptions`
fmap (maybe cardanoDefaultTestnetNodeOptions (\num -> defaultSpoOptions :| L.replicate (num - 1) defaultSpoOptions)) <$>
optional $ OA.option ensureAtLeastOne
( OA.long "num-pool-nodes"
Expand All @@ -108,14 +127,6 @@ pTestnetNodeOptions =
Just n | n >= 1 -> pure n
_ -> fail "Need at least one SPO node to produce blocks, but got none."

pNodeEnvironment :: Parser UserProvidedEnv
pNodeEnvironment = fmap (maybe NoUserProvidedEnv UserProvidedEnv) <$>
optional $ OA.strOption
( OA.long "node-env"
<> OA.metavar "FILEPATH"
<> OA.help "Path to the node's environment (which is generated otherwise). You can generate a default environment with the 'create-env' command, then modify it and pass it with this argument."
)

pOnChainParams :: Parser TestnetOnChainParams
pOnChainParams = fmap (fromMaybe DefaultParams) <$> optional $
pCustomParamsFile <|> pMainnetParams
Expand All @@ -135,10 +146,6 @@ pMainnetParams = OA.flag' OnChainParamsMainnet

pUpdateTimestamps :: Parser UpdateTimestamps
pUpdateTimestamps =
-- Default to UpdateTimestamps, because when using the two-step flow
-- (cardano-testnet create-env → cardano-testnet cardano --node-env),
-- genesis timestamps can become stale.
-- See https://github.com/IntersectMBO/cardano-node/issues/6455
OA.flag' UpdateTimestamps (OA.long "update-time" <> OA.help "Update the time stamps in genesis files to current date (default, kept for backward compatibility)" <> OA.internal)
<|> OA.flag' DontUpdateTimestamps (OA.long "preserve-timestamps" <> OA.help "Do not update the time stamps in genesis files to current date.")
<|> pure UpdateTimestamps
Expand All @@ -161,23 +168,23 @@ pGenesisOptions =
pEpochLength =
OA.option OA.auto
( OA.long "epoch-length"
<> OA.help "Epoch length, in number of slots. Ignored if a node environment is passed."
<> OA.help "Epoch length, in number of slots."
<> OA.metavar "SLOTS"
<> OA.showDefault
<> OA.value (genesisEpochLength def)
)
pSlotLength =
OA.option OA.auto
( OA.long "slot-length"
<> OA.help "Slot length. Ignored if a node environment is passed."
<> OA.help "Slot length."
<> OA.metavar "SECONDS"
<> OA.showDefault
<> OA.value (genesisSlotLength def)
)
pActiveSlotCoeffs =
OA.option OA.auto
( OA.long "active-slots-coeff"
<> OA.help "Active slots coefficient. Ignored if a node environment is passed."
<> OA.help "Active slots coefficient."
<> OA.metavar "DOUBLE"
<> OA.showDefault
<> OA.value (genesisActiveSlotsCoeff def)
Expand All @@ -203,8 +210,8 @@ pMaxLovelaceSupply :: Parser Word64
pMaxLovelaceSupply =
OA.option OA.auto
( OA.long "max-lovelace-supply"
<> OA.help "Max lovelace supply that your testnet starts with. Ignored if a node environment is passed."
<> OA.help "Max lovelace supply that your testnet starts with."
<> OA.metavar "WORD64"
<> OA.showDefault
<> OA.value (cardanoMaxSupply def)
<> OA.value (creationMaxSupply def)
)
60 changes: 25 additions & 35 deletions cardano-testnet/src/Parsers/Run.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
Expand All @@ -13,9 +14,11 @@ module Parsers.Run
import Cardano.CLI.Environment

import Control.Monad (void)
import Data.Maybe (fromMaybe)
import Options.Applicative
import qualified Options.Applicative as Opt

import Testnet.Filepath (unTmpAbsPath)
import Testnet.Start.Cardano
import Testnet.Start.Types

Expand Down Expand Up @@ -56,50 +59,37 @@ runTestnetCmd = \case

createEnvOptions :: CardanoTestnetCreateEnvOptions -> IO ()
createEnvOptions CardanoTestnetCreateEnvOptions
{ createEnvTestnetOptions=testnetOptions
, createEnvGenesisOptions=genesisOptions
{ createEnvCreationOptions=creationOptions
, createEnvOutputDir=outputDir
, createEnvCreateEnvOptions=ceOptions
} = do
conf <- mkConfigAbs outputDir
void $ createTestnetEnv
testnetOptions genesisOptions ceOptions
creationOptions
-- Do not add hashes to the main config file, so that genesis files
-- can be modified without having to recompute hashes every time.
conf{genesisHashesPolicy = WithoutHashes}

runCardanoOptions :: CardanoTestnetCliOptions -> IO ()
runCardanoOptions CardanoTestnetCliOptions
{ cliTestnetOptions=testnetOptions
, cliGenesisOptions=genesisOptions
, cliNodeEnvironment=env
, cliUpdateTimestamps=updateTimestamps'
, cliOnChainParams=onChainParams
} = do
case env of
NoUserProvidedEnv -> do
-- Create the sandbox, then run cardano-testnet.
-- It is not necessary to honor `cliUpdateTimestamps` here, because
-- the genesis files will be created with up-to-date stamps already.
conf <- mkConfigAbs "testnet"
runSimpleApp . runResourceT $ do
logInfo $ "Creating environment: " <> display (tempAbsPath conf)
createTestnetEnv testnetOptions genesisOptions (CreateEnvOptions onChainParams) conf
logInfo $ "Starting testnet in environment: " <> display (tempAbsPath conf)
void $ cardanoTestnet testnetOptions conf
logInfo "Testnet started"
waitForShutdown
UserProvidedEnv nodeEnvPath -> do
-- Run cardano-testnet in the sandbox provided by the user
-- In that case, 'cardanoOutputDir' is not used
conf <- mkConfigAbs nodeEnvPath
runSimpleApp . runResourceT $ do
logInfo $ "Starting testnet in environment: " <> display (tempAbsPath conf)
void $ cardanoTestnet
testnetOptions
conf{updateTimestamps=updateTimestamps'}
logInfo "Testnet started"
waitForShutdown
runCardanoOptions = \case
StartFromScratch StartFromScratchOptions{scratchCreationOptions, scratchOutputDir, scratchRuntimeOptions} -> do
let dirName = fromMaybe "testnet" scratchOutputDir
conf <- mkConfigAbs dirName
runSimpleApp . runResourceT $ do
logInfo $ "Creating environment: " <> display (tempAbsPath conf)
createTestnetEnv scratchCreationOptions conf
logInfo $ "Starting testnet in environment: " <> display (tempAbsPath conf)
void $ cardanoTestnet (creationNodes scratchCreationOptions) scratchRuntimeOptions conf
logInfo "Testnet started"
waitForShutdown
StartFromEnv StartFromEnvOptions{fromEnvOptions, fromEnvRuntimeOptions} -> do
conf <- mkConfigAbs (envPath fromEnvOptions)
nodes <- readNodeOptionsFromEnv (unTmpAbsPath (tempAbsPath conf))
runSimpleApp . runResourceT $ do
logInfo $ "Starting testnet in environment: " <> display (tempAbsPath conf)
void $ cardanoTestnet nodes fromEnvRuntimeOptions
conf{updateTimestamps = envUpdateTimestamps fromEnvOptions}
logInfo "Testnet started"
waitForShutdown
where
waitForShutdown = do
logInfo "Waiting for shutdown (Ctrl+C)"
Expand Down
Loading
Loading