Skip to content

Commit d58363b

Browse files
authored
Merge pull request #6563 from IntersectMBO/split-nodes-list-into-spo-and-relay
cardano-testnet: Enforce SPOs come first and split node list into SPO and relay
2 parents 9643487 + eb5f40e commit d58363b

9 files changed

Lines changed: 100 additions & 85 deletions

File tree

cardano-node-chairman/test/Spec/Chairman/Cardano.hs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ module Spec.Chairman.Cardano where
88
import Cardano.Testnet
99

1010
import Data.Default.Class
11-
import Data.List.NonEmpty (NonEmpty ((:|)))
1211
import Testnet.Property.Util (integrationRetryWorkspace)
1312

1413
import qualified Hedgehog as H
@@ -20,7 +19,7 @@ hprop_chairman :: H.Property
2019
hprop_chairman = integrationRetryWorkspace 2 "cardano-chairman" $ \tempAbsPath' -> H.runWithDefaultWatchdog_ $ do
2120
conf <- mkConf tempAbsPath'
2221

23-
let creationOptions = def{ creationNodes = SpoNodeOptions [] :| [RelayNodeOptions [], RelayNodeOptions []] }
22+
let creationOptions = def{ creationNodes = cardanoDefaultTestnetNodeOptions }
2423
allNodes <- testnetNodes <$> createAndRunTestnet creationOptions def conf
2524

2625
chairmanOver 120 50 conf allNodes
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
### Changed
2+
3+
- Refactored `NodeOption` from a sum type into a record with a `TestnetNodeOptions` container
4+
that enforces at the type level that SPO nodes come first and at least one is present.
5+
- `readNodeOptionsFromEnv` now validates that node directories are consecutively numbered
6+
and that SPOs come before relays.

cardano-testnet/src/Cardano/Testnet.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ module Cardano.Testnet (
1414
TestnetRuntimeOptions(..),
1515
TestnetEnvOptions(..),
1616
RpcSupport(..),
17-
NodeOption(..),
17+
TestnetNodeOptions(..),
18+
NodeOptions(..),
1819
cardanoDefaultTestnetNodeOptions,
1920
getDefaultAlonzoGenesis,
2021
getDefaultShelleyGenesis,

cardano-testnet/src/Parsers/Cardano.hs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,21 @@ pKesSource = OA.flag UseKesKeyFile UseKesSocket
105105
<> OA.showDefault
106106
)
107107

108-
pTestnetNodeOptions :: Parser (NonEmpty NodeOption)
108+
pTestnetNodeOptions :: Parser TestnetNodeOptions
109109
pTestnetNodeOptions =
110-
-- If `--num-pool-nodes N` is present, return N nodes with option `SpoNodeOptions []`.
111-
-- Otherwise, return `cardanoDefaultTestnetNodeOptions`
112-
fmap (maybe cardanoDefaultTestnetNodeOptions (\num -> defaultSpoOptions :| L.replicate (num - 1) defaultSpoOptions)) <$>
110+
fmap (maybe cardanoDefaultTestnetNodeOptions mkPoolNodes) <$>
113111
optional $ OA.option ensureAtLeastOne
114112
( OA.long "num-pool-nodes"
115113
<> OA.help "Number of pool nodes. Note this uses a default node configuration for all nodes."
116114
<> OA.metavar "COUNT"
117115
)
118116
where
119-
defaultSpoOptions = SpoNodeOptions []
117+
defaultSpoOption = NodeOptions []
118+
119+
mkPoolNodes num = TestnetNodeOptions
120+
{ optSpoNodes = defaultSpoOption :| L.replicate (num - 1) defaultSpoOption
121+
, optRelayNodes = []
122+
}
120123

121124
ensureAtLeastOne :: OA.ReadM Int
122125
ensureAtLeastOne = readerAsk >>= \arg ->

cardano-testnet/src/Testnet/Start/Cardano.hs

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ module Testnet.Start.Cardano
1717
, TestnetCreationOptions(..)
1818
, TestnetRuntimeOptions(..)
1919
, TestnetEnvOptions(..)
20-
, NodeOption(..)
20+
, TestnetNodeOptions(..)
21+
, NodeOptions(..)
2122
, cardanoDefaultTestnetNodeOptions
2223

2324
, TestnetRuntime (..)
@@ -92,16 +93,6 @@ import RIO.State (put)
9293
import UnliftIO.Async
9394
import UnliftIO.Exception (stringException)
9495

95-
-- | There are certain conditions that need to be met in order to run
96-
-- a valid node cluster.
97-
testMinimumConfigurationRequirements :: ()
98-
=> HasCallStack
99-
=> MonadIO m
100-
=> NonEmpty NodeOption -> m ()
101-
testMinimumConfigurationRequirements nodes = withFrozenCallStack $ do
102-
unless (any isSpoNodeOptions nodes) $ do
103-
throwString "Need at least one SPO node to produce blocks, but got none."
104-
10596
liftToIntegration :: HasCallStack => RIO ResourceMap a -> H.Integration a
10697
liftToIntegration r = do
10798
rMap <- lift $ lift getInternalState
@@ -118,15 +109,13 @@ createTestnetEnv :: ()
118109
createTestnetEnv
119110
creationOptions@TestnetCreationOptions
120111
{ creationEra=asbe
121-
, creationNodes
112+
, creationNodes=TestnetNodeOptions{optSpoNodes, optRelayNodes}
122113
}
123114
Conf
124115
{ genesisHashesPolicy
125116
, tempAbsPath=TmpAbsolutePath tmpAbsPath
126117
} = do
127118

128-
testMinimumConfigurationRequirements creationNodes
129-
130119
AnyShelleyBasedEra sbe <- pure asbe
131120

132121
_ <- createSPOGenesisAndFiles
@@ -141,13 +130,16 @@ createTestnetEnv
141130

142131
liftIOAnnotated . LBS.writeFile configurationFile $ A.encodePretty $ Object config
143132

144-
portNumbers <- forM (NEL.zip (1 :| [2..]) creationNodes)
133+
let allNodes = NEL.toList optSpoNodes ++ optRelayNodes
134+
numberedNodes = zip [1..] allNodes
135+
nodeIds = map fst numberedNodes
136+
137+
portNumbers <- forM numberedNodes
145138
(\(i, _nodeOption) -> (i,) <$> H.randomPort testnetDefaultIpv4Address)
146139

147-
let portNumbersMap = Map.fromList (NEL.toList portNumbers)
140+
let portNumbersMap = Map.fromList portNumbers
148141

149142
-- Create network topology and write port files
150-
let nodeIds = fst <$> NEL.zip (1 :| [2..]) creationNodes
151143
forM_ nodeIds $ \i -> do
152144
let nodeDataDir = tmpAbsPath </> Defaults.defaultNodeDataDir i
153145
liftIOAnnotated $ IO.createDirectoryIfMissing True nodeDataDir
@@ -157,7 +149,7 @@ createTestnetEnv
157149
Just port -> liftIOAnnotated $ writeFile (tmpAbsPath </> defaultPortFile i) (show port)
158150
Nothing -> throwString $ "Port not found for node " <> show i
159151

160-
producers <- mapM (idToRemoteAddressP2P portNumbersMap) $ NodeId <$> NEL.filter (/= i) nodeIds
152+
producers <- mapM (idToRemoteAddressP2P portNumbersMap) $ NodeId <$> filter (/= i) nodeIds
161153
let topology = Defaults.defaultP2PTopology producers
162154
liftIOAnnotated . LBS.writeFile (nodeDataDir </> "topology.json") $ A.encodePretty topology
163155

@@ -232,12 +224,12 @@ cardanoTestnet
232224
=> MonadResource m
233225
=> MonadCatch m
234226
=> MonadFail m
235-
=> NonEmpty NodeOption -- ^ The nodes to start
227+
=> TestnetNodeOptions -- ^ The nodes to start
236228
-> TestnetRuntimeOptions -- ^ Runtime options
237229
-> Conf -- ^ Path to the test sandbox
238230
-> m TestnetRuntime
239231
cardanoTestnet
240-
cardanoNodes
232+
TestnetNodeOptions{optSpoNodes=cardanoSpoNodes, optRelayNodes=cardanoRelayNodes}
241233
TestnetRuntimeOptions
242234
{ runtimeEnableNewEpochStateLogging=enableNewEpochStateLogging
243235
, runtimeEnableRpc=cardanoEnableRpc
@@ -247,8 +239,8 @@ cardanoTestnet
247239
{ tempAbsPath=TmpAbsolutePath tmpAbsPath
248240
, updateTimestamps
249241
} = do
250-
testMinimumConfigurationRequirements cardanoNodes
251-
let nPools = NumPools $ length $ NEL.filter isSpoNodeOptions cardanoNodes
242+
let nPools = NumPools $ NEL.length cardanoSpoNodes
243+
allNodes = map (True,) (NEL.toList cardanoSpoNodes) ++ map (False,) cardanoRelayNodes
252244
nodeConfigFile = tmpAbsPath </> defaultConfigFile
253245
byronGenesisFile = tmpAbsPath </> "byron-genesis.json"
254246
shelleyGenesisFile = tmpAbsPath </> "shelley-genesis.json"
@@ -279,7 +271,7 @@ cardanoTestnet
279271
}
280272

281273
-- Read port numbers from disk (written by createTestnetEnv)
282-
portNumbers <- forM (NEL.zip (1 :| [2..]) cardanoNodes) $ \(i, _nodeOption) -> do
274+
portNumbers <- forM (zip [1..] allNodes) $ \(i, _) -> do
283275
let nodeDataDir = tmpAbsPath </> Defaults.defaultNodeDataDir i
284276
portPath = tmpAbsPath </> defaultPortFile i
285277
portStr <- liftIOAnnotated $ readFile portPath
@@ -316,19 +308,16 @@ cardanoTestnet
316308
let shelleyGenesis' = shelleyGenesis{sgSystemStart = startTime}
317309
liftIOAnnotated . LBS.writeFile shelleyGenesisFile $ A.encodePretty shelleyGenesis'
318310

319-
let portNumbersMap = Map.fromList (NEL.toList portNumbers)
311+
let portNumbersMap = Map.fromList portNumbers
320312

321-
eTestnetNodes <- forConcurrently (NEL.zip (1 :| [2..]) cardanoNodes) $ \(i, nodeOptions) -> do
313+
eTestnetNodes <- forConcurrently (zip [1..] allNodes) $ \(i, (isSpo, nodeOptions)) -> do
322314
port <- case Map.lookup i portNumbersMap of
323315
Just p -> pure p
324316
Nothing -> throwString $ "Port not found for node " <> show i
325317
let nodeName = Defaults.defaultNodeName i
326318
nodeDataDir = tmpAbsPath </> Defaults.defaultNodeDataDir i
327319
nodePoolKeysDir = tmpAbsPath </> Defaults.defaultSpoKeysDir i
328-
(mKeys, spoNodeCliArgs) <-
329-
case nodeOptions of
330-
RelayNodeOptions{} -> pure (Nothing, [])
331-
SpoNodeOptions{} -> do
320+
(mKeys, spoNodeCliArgs) <- if not isSpo then pure (Nothing, []) else do
332321
-- depending on testnet configuration, either start a 'kes-agent' or use a key from disk
333322
kesSourceCliArg <-
334323
case cardanoKESSource of
@@ -370,11 +359,11 @@ cardanoTestnet
370359
, "--database-path", nodeDataDir </> "db"
371360
]
372361
<> spoNodeCliArgs
373-
<> extraCliArgs nodeOptions
362+
<> nodeExtraCliArgs nodeOptions
374363
<> ["--grpc-enable" | RpcEnabled <- [cardanoEnableRpc]]
375364
pure $ eRuntime <&> \rt -> rt{poolKeys=mKeys}
376365

377-
let (failedNodes, testnetNodes') = partitionEithers (NEL.toList eTestnetNodes)
366+
let (failedNodes, testnetNodes') = partitionEithers eTestnetNodes
378367
unless (null failedNodes) $ do
379368
throwString $ "Some nodes failed to start:\n" ++ show (vsep $ prettyError <$> failedNodes)
380369

@@ -417,9 +406,6 @@ cardanoTestnet
417406

418407
pure runtime
419408
where
420-
extraCliArgs = \case
421-
SpoNodeOptions args -> args
422-
RelayNodeOptions args -> args
423409
-- TODO: This should come from the configuration!
424410
makePathsAbsolute :: (Element a ~ FilePath, MonoFunctor a) => a -> a
425411
makePathsAbsolute = omap (tmpAbsPath </>)
@@ -511,19 +497,32 @@ retryOnAddressInUseError act = withFrozenCallStack $ go maximumTimeout retryTime
511497
retryTimeout = 5
512498

513499
-- | Read node options from an existing testnet environment directory.
514-
-- Scans @node-data/@ for node directories and checks @pools-keys/@ to
515-
-- classify each node as SPO or relay.
516-
readNodeOptionsFromEnv :: MonadIO m => FilePath -> m (NonEmpty NodeOption)
500+
-- Scans @node-data/@ for node directories numbered @node1, node2, ...@
501+
-- and checks @pools-keys/@ to classify each as SPO or relay.
502+
-- Validates that nodes are consecutively numbered starting from 1,
503+
-- and that all SPO nodes come before relay nodes.
504+
readNodeOptionsFromEnv :: HasCallStack => MonadIO m => FilePath -> m TestnetNodeOptions
517505
readNodeOptionsFromEnv envDir = do
518506
entries <- liftIO $ IO.listDirectory (envDir </> "node-data")
519507
let nodeNums = sort $ mapMaybe parseNodeNum entries
520-
case nodeNums of
521-
[] -> throwString "No node directories found in environment"
522-
(n:ns) -> mapM classifyNode (n :| ns)
508+
when (null nodeNums) $
509+
throwString "No node directories found in environment"
510+
when (nodeNums /= [1 .. length nodeNums]) $
511+
throwString $ "Node directories are not consecutively numbered from 1: " <> show nodeNums
512+
isSpoFlags <- forM nodeNums $ \i ->
513+
liftIO $ IO.doesDirectoryExist (envDir </> Defaults.defaultSpoKeysDir i)
514+
let (spoFlags, relayFlags) = span id isSpoFlags
515+
unless (all not relayFlags) $
516+
throwString "SPO nodes must come before relay nodes in the environment"
517+
when (null spoFlags) $
518+
throwString "No SPO node directories found in environment"
519+
let nSpos = length spoFlags
520+
let spoOpts = map (const (NodeOptions [])) [1 .. nSpos]
521+
relayOpts = map (const (NodeOptions [])) [nSpos + 1 .. length nodeNums]
522+
case spoOpts of
523+
(s:ss) -> pure $ TestnetNodeOptions { optSpoNodes = s :| ss, optRelayNodes = relayOpts }
524+
[] -> throwString "No SPO node directories found in environment"
523525
where
524526
parseNodeNum s = do
525527
rest <- stripPrefix "node" s
526528
readMaybe rest :: Maybe Int
527-
classifyNode i = do
528-
hasPools <- liftIO $ IO.doesDirectoryExist (envDir </> Defaults.defaultSpoKeysDir i)
529-
pure $ if hasPools then SpoNodeOptions [] else RelayNodeOptions []

cardano-testnet/src/Testnet/Start/Types.hs

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ module Testnet.Start.Types
3131
, UpdateTimestamps(..)
3232
, TestnetOnChainParams(..)
3333
, mainnetParamsRequest
34-
, NodeOption(..)
35-
, isSpoNodeOptions
36-
, isRelayNodeOptions
34+
, TestnetNodeOptions(..)
35+
, NodeOptions(..)
3736
, cardanoDefaultTestnetNodeOptions
3837
, GenesisOptions(..)
3938
, UserProvidedData(..)
@@ -177,7 +176,7 @@ data RpcSupport
177176
-- 'Testnet.Start.Cardano.createAndRunTestnet' in tests.
178177
data TestnetCreationOptions = TestnetCreationOptions
179178
{ -- | Options controlling how many nodes to create and of which type.
180-
creationNodes :: NonEmpty NodeOption
179+
creationNodes :: TestnetNodeOptions
181180
, creationEra :: AnyShelleyBasedEra -- ^ The era to start at
182181
, creationMaxSupply :: Word64 -- ^ The amount of Lovelace you are starting your testnet with (forwarded to shelley genesis)
183182
-- TODO move me to GenesisOptions when https://github.com/IntersectMBO/cardano-cli/pull/874 makes it to cardano-node
@@ -225,11 +224,11 @@ newtype InputNodeConfigFile = InputNodeConfigFile FilePath
225224

226225
creationNumPools :: TestnetCreationOptions -> NumPools
227226
creationNumPools TestnetCreationOptions{creationNodes} =
228-
NumPools $ length $ NEL.filter isSpoNodeOptions creationNodes
227+
NumPools $ NEL.length $ optSpoNodes creationNodes
229228

230229
creationNumRelays :: TestnetCreationOptions -> NumRelays
231230
creationNumRelays TestnetCreationOptions{creationNodes} =
232-
NumRelays $ length $ NEL.filter isRelayNodeOptions creationNodes
231+
NumRelays $ length $ optRelayNodes creationNodes
233232

234233
-- | Number of stake pool nodes
235234
newtype NumPools = NumPools Int
@@ -259,12 +258,17 @@ instance Default GenesisOptions where
259258
, genesisActiveSlotsCoeff = 0.05
260259
}
261260

262-
-- | Whether a node should be an SPO or just a relay.
263-
-- The '@String' arguments will be appended to the default options when starting the node.
264-
data NodeOption
265-
= SpoNodeOptions [String]
266-
| RelayNodeOptions [String]
267-
deriving (Eq, Show)
261+
-- | Configuration specific to each node
262+
newtype NodeOptions = NodeOptions
263+
{ nodeExtraCliArgs :: [String] -- ^ Extra CLI arguments passed to @cardano-node run@
264+
} deriving (Eq, Show)
265+
266+
-- | Specifies the nodes to create for the testnet, split by role (SPO and relay).
267+
-- SPO nodes participate in block production. Relay nodes only forward blocks.
268+
data TestnetNodeOptions = TestnetNodeOptions
269+
{ optSpoNodes :: NonEmpty NodeOptions -- ^ SPO (stake pool operator) nodes. Must have at least one.
270+
, optRelayNodes :: [NodeOptions] -- ^ Relay (non-producing) nodes
271+
} deriving (Eq, Show)
268272

269273
-- | Type used to track whether the user is providing its data (node configuration file path, genesis file, etc.)
270274
-- or whether it needs to be programmatically generated by @cardanoTestnet@ and friends.
@@ -276,19 +280,13 @@ data UserProvidedData a =
276280
instance Default (UserProvidedData a) where
277281
def = NoUserProvidedData
278282

279-
isSpoNodeOptions :: NodeOption -> Bool
280-
isSpoNodeOptions SpoNodeOptions{} = True
281-
isSpoNodeOptions RelayNodeOptions{} = False
282-
283-
isRelayNodeOptions :: NodeOption -> Bool
284-
isRelayNodeOptions SpoNodeOptions{} = False
285-
isRelayNodeOptions RelayNodeOptions{} = True
286-
287-
cardanoDefaultTestnetNodeOptions :: NonEmpty NodeOption
288-
cardanoDefaultTestnetNodeOptions =
289-
SpoNodeOptions [] :| [ RelayNodeOptions []
290-
, RelayNodeOptions []
291-
]
283+
cardanoDefaultTestnetNodeOptions :: TestnetNodeOptions
284+
cardanoDefaultTestnetNodeOptions = TestnetNodeOptions
285+
{ optSpoNodes = NodeOptions [] :| []
286+
, optRelayNodes = [ NodeOptions []
287+
, NodeOptions []
288+
]
289+
}
292290

293291
data NodeLoggingFormat
294292
= NodeLoggingFormatAsJson

cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/LeadershipSchedule.hs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,13 @@ hprop_leadershipSchedule = integrationRetryWorkspace 2 "leadership-schedule" $ \
7171
cTestnetOptions = def
7272
{ creationEra = asbe
7373
, creationNodes =
74-
SpoNodeOptions [] :|
75-
[ SpoNodeOptions []
76-
, SpoNodeOptions []
77-
]
74+
TestnetNodeOptions
75+
{ optSpoNodes = NodeOptions [] :|
76+
[ NodeOptions []
77+
, NodeOptions []
78+
]
79+
, optRelayNodes = []
80+
}
7881
}
7982
eraString = eraToString sbe
8083

cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/ProposeNewConstitutionSPO.hs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,13 @@ hprop_ledger_events_propose_new_constitution_spo = integrationRetryWorkspace 2 "
5959
creationOptions = def
6060
{ creationEra = AnyShelleyBasedEra sbe
6161
, creationNodes =
62-
SpoNodeOptions [] :|
63-
[ SpoNodeOptions []
64-
, SpoNodeOptions []
65-
]
62+
TestnetNodeOptions
63+
{ optSpoNodes = NodeOptions [] :|
64+
[ NodeOptions []
65+
, NodeOptions []
66+
]
67+
, optRelayNodes = []
68+
}
6669
, creationGenesisOptions = def { genesisEpochLength = 100 }
6770
}
6871

cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/Shutdown.hs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,10 @@ hprop_shutdownOnSlotSynced = integrationRetryWorkspace 2 "shutdown-on-slot-synce
208208
slotLen = 0.1
209209
let creationOptions = def
210210
{ creationNodes =
211-
SpoNodeOptions ["--shutdown-on-slot-synced", show maxSlot] :| []
211+
TestnetNodeOptions
212+
{ optSpoNodes = NodeOptions ["--shutdown-on-slot-synced", show maxSlot] :| []
213+
, optRelayNodes = []
214+
}
212215
, creationGenesisOptions = def
213216
{ genesisEpochLength = epochLength
214217
, genesisSlotLength = slotLen

0 commit comments

Comments
 (0)