cardano-testnet: Add --nodes flag for per-node binary configuration#6559
cardano-testnet: Add --nodes flag for per-node binary configuration#6559
--nodes flag for per-node binary configuration#6559Conversation
f0bcf8d to
88a49cf
Compare
--nodes flag for per-node binary configuration
9ca8af4 to
77fa7d8
Compare
88a49cf to
450f24b
Compare
carbolymer
left a comment
There was a problem hiding this comment.
Exception handling is most important here
def6fd7 to
393ad15
Compare
450f24b to
6530ec1
Compare
393ad15 to
eb5f40e
Compare
6530ec1 to
8bb24b5
Compare
744a4ed to
68aa58b
Compare
Co-authored-by: Mateusz Galazyn <228866+carbolymer@users.noreply.github.com>
ebdfb5a to
0bbd001
Compare
| Create a sandbox for Cardano testnet | ||
|
|
||
| Available options: | ||
| --nodes SPEC[,SPEC...] Comma-separated node specifications. SPO nodes must |
There was a problem hiding this comment.
My question would be is do we expect more options to be specified beyond what is added today? If so then we may be better off specifying a file.
There was a problem hiding this comment.
It is hard to tell, I imagine there must be things you could potentially want to specify per node, but then there is also the possibility of creating the env and modifying before executing. The functionality that I was thinking about that seems would be convenient would be to have multipliers, like 3x:spo,4x:relay:node-bin=<...>.
There was a problem hiding this comment.
Pull request overview
Adds per-node configuration support to cardano-testnet via a new --nodes CLI flag, allowing each node to be designated as SPO/relay and optionally pinned to a specific cardano-node binary (recorded in the generated env and reused when starting from --node-env).
Changes:
- Introduces
--nodes SPEC[,SPEC...]parsing (with role ordering validation and optionalnode-bin=path) and updates CLI help golden files. - Extends testnet node configuration types to carry an optional per-node binary path, and persists that choice into
node-data/nodeN/env. - Updates node startup to optionally use a custom binary via a new
procCustomhelper and astartNodesignature change.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/Shutdown.hs | Updates test to new node options type (NodeWithOptions). |
| cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/ProposeNewConstitutionSPO.hs | Updates test to new node options type (NodeWithOptions). |
| cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/LeadershipSchedule.hs | Updates test node creation + startNode call to pass optional custom binary. |
| cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/KesPeriodInfo.hs | Updates startNode call to pass optional custom binary. |
| cardano-testnet/test/cardano-testnet-golden/files/golden/help/create-env.cli | Updates golden help output to document --nodes. |
| cardano-testnet/test/cardano-testnet-golden/files/golden/help/cardano.cli | Updates golden help output to document --nodes. |
| cardano-testnet/test/cardano-testnet-golden/files/golden/help.cli | Updates top-level golden help usage to show --nodes alternative. |
| cardano-testnet/src/Testnet/Start/Types.hs | Replaces exported node option types with NodeWithOptions/TestnetNodesWithOptions and adds nodeBin. |
| cardano-testnet/src/Testnet/Start/Cardano.hs | Writes/reads per-node env YAML for custom binaries; threads nodeBin into node startup. |
| cardano-testnet/src/Testnet/Runtime.hs | Extends startNode to accept an optional custom node binary. |
| cardano-testnet/src/Testnet/Process/RunIO.hs | Adds procCustom helper for running an explicitly provided binary path. |
| cardano-testnet/src/Parsers/Run.hs | Uses new readNodesWithOptionsFromEnv reader. |
| cardano-testnet/src/Parsers/Cardano.hs | Adds --nodes flag and Parsec-based parseNodeSpecs implementation. |
| cardano-testnet/src/Cardano/Testnet.hs | Updates re-exports to the renamed node options API. |
| cardano-testnet/changelog.d/20260506_035740_palas_testnet_specify_node_bin_per_node.md | Adds changelog entry for --nodes. |
| cardano-testnet/cardano-testnet.cabal | Adds parsec dependency for --nodes parsing. |
| cardano-node/src/Cardano/Node/Testnet/Paths.hs | Adds defaultNodeEnvFile path helper. |
| cardano-node-chairman/test/Spec/Chairman/Cardano.hs | Updates chairman test to use the new default node options value. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -517,12 +530,48 @@ readNodeOptionsFromEnv envDir = do | |||
| when (null spoFlags) $ | |||
| throwString "No SPO node directories found in environment" | |||
| readNodeBinFromEnvFile :: (HasCallStack, MonadIO m) => FilePath -> m (Maybe FilePath) | ||
| readNodeBinFromEnvFile envFile = runMaybeT $ do | ||
| guard =<< liftIOAnnotated (IO.doesFileExist envFile) | ||
| NodeEnv{nodeBinary} <- either failParse pure =<< liftIOAnnotated (Yaml.decodeFileEither envFile) |
| , UpdateTimestamps(..) | ||
| , TestnetOnChainParams(..) | ||
| , mainnetParamsRequest | ||
| , TestnetNodeOptions(..) | ||
| , NodeOptions(..) | ||
| , cardanoDefaultTestnetNodeOptions | ||
| , TestnetNodesWithOptions(..) | ||
| , NodeWithOptions(..) | ||
| , cardanoDefaultTestnetNodesWithOptions |
| -- | Parse a @--nodes@ argument string into 'TestnetNodesWithOptions'. | ||
| parseNodeSpecs :: String -> Either Parsec.ParseError TestnetNodesWithOptions | ||
| parseNodeSpecs = parse (nodeSpecsParser <* eof) "Error parsing node specifications" | ||
| where | ||
| nodeSpecsParser :: Parsec.Parsec String () TestnetNodesWithOptions | ||
| nodeSpecsParser = do | ||
| specs <- nodeSpec `sepBy1` char ',' | ||
| let (spos, relays) = span (\(role, _) -> role == Spo) specs | ||
| unless (all (\(role, _) -> role == Relay) relays) $ | ||
| fail "SPO nodes must come before relay nodes. Example: --nodes spo,spo,relay,relay" | ||
| case map snd spos of | ||
| [] -> fail "Need at least one SPO node to produce blocks." | ||
| (s:ss) -> pure $ TestnetNodesWithOptions | ||
| { optSpoNodes = s :| ss | ||
| , optRelayNodes = map snd relays | ||
| } | ||
|
|
||
| nodeSpec :: Parsec.Parsec String () (NodeRole, NodeWithOptions) | ||
| nodeSpec = do | ||
| role <- nodeRole | ||
| bin <- optional $ char ':' *> nodeBinKV | ||
| pure (role, NodeWithOptions bin []) | ||
|
|
||
| nodeRole :: Parsec.Parsec String () NodeRole | ||
| nodeRole = | ||
| Spo <$ try (string "spo" <* notFollowedBy (noneOf ",:\"\\")) | ||
| <|> Relay <$ try (string "relay" <* notFollowedBy (noneOf ",:\"\\")) | ||
| <?> "node role (\"spo\" or \"relay\")" | ||
|
|
||
| nodeBinKV :: Parsec.Parsec String () FilePath | ||
| nodeBinKV = string "node-bin=" *> (quotedPath <|> unquotedPath) <?> "\"node-bin=<path>\", where <path> is the path to the node binary, optionally quoted if it contains special characters" | ||
|
|
||
| quotedPath :: Parsec.Parsec String () FilePath | ||
| quotedPath = char '"' *> Parsec.many quotedChar <* char '"' | ||
| where | ||
| quotedChar = try (char '\\' *> (char '"' <|> char '\\')) <|> noneOf "\"" | ||
|
|
||
| unquotedPath :: Parsec.Parsec String () FilePath | ||
| unquotedPath = many1 (noneOf ",:\"\\") | ||
|
|
||
| data NodeRole = Spo | Relay deriving Eq |
| pNodes = OA.option readNodeSpecs | ||
| ( OA.long "nodes" | ||
| <> OA.help "Comma-separated node specifications. SPO nodes must come before relay nodes. \ | ||
| \Each spec is a role (spo or relay) optionally followed by :node-bin=<path>. \ |
There was a problem hiding this comment.
This needs to exist in documentation somewhere it can't only be in the help text.
| specs <- nodeSpec `sepBy1` char ',' | ||
| let (spos, relays) = span (\(role, _) -> role == Spo) specs | ||
| unless (all (\(role, _) -> role == Relay) relays) $ | ||
| fail "SPO nodes must come before relay nodes. Example: --nodes spo,spo,relay,relay" |
There was a problem hiding this comment.
Why? Does the order actually matter or is it a matter of the presence of SPOs? Can you also separate the parsing and validation logic here?
| parseNodeSpecs :: String -> Either Parsec.ParseError TestnetNodesWithOptions | ||
| parseNodeSpecs = parse (nodeSpecsParser <* eof) "Error parsing node specifications" | ||
| where | ||
| nodeSpecsParser :: Parsec.Parsec String () TestnetNodesWithOptions |
There was a problem hiding this comment.
Can you write a basic property test for this that confirms we get the expected number of SPOs and relays?
| bin <- readNodeBinFromEnvFile (envDir </> defaultNodeEnvFile i) | ||
| pure $ NodeWithOptions bin [] | ||
|
|
||
| data NodeEnv = NodeEnv |
| newtype NodeOptions = NodeOptions | ||
| { nodeExtraCliArgs :: [String] -- ^ Extra CLI arguments passed to @cardano-node run@ | ||
| data NodeWithOptions = NodeWithOptions | ||
| { nodeBin :: Maybe FilePath -- ^ Path to the @cardano-node@ binary to use for running this node. 'Nothing' uses the default resolution mechanism. |
There was a problem hiding this comment.
Can you expand on this a little. What's a potential purpose of specifying the node binary and roughly what is the default resolution mechanism?
Description
Adds a
--nodesCLI flag tocardano-testnetthat allows specifying the role (SPO or relay) and optionally a customcardano-nodebinary for each node in the testnet. This enables running testnets with mixed node versions, which is needed for testing compatibility across node releases.The new flag coexists with the existing
--num-pool-nodesvia<|>in the parser, so either can be used:At creation time, custom binaries are validated by running
--versionand the result is recorded in a YAML env file (node-data/nodeN/env). At runtime, the env file is read to resolve the binary, falling back to the default resolution when no env file exists.Base PR: #6563 (enforces SPOs come first and splits node list into SPO and relay)
Checklist
See Running tests for more details
CHANGELOG.mdfor affected package.cabalfiles are updatedhlint. See.github/workflows/check-hlint.ymlto get thehlintversionstylish-haskell. See.github/workflows/stylish-haskell.ymlto get thestylish-haskellversionghc-9.6andghc-9.12