Skip to content

Commit

Permalink
Merge pull request #1285 from input-output-hk/run-hydra-cluster-on-ex…
Browse files Browse the repository at this point in the history
…isting-node

Run smoke tests on running node
  • Loading branch information
ch1bo authored Feb 16, 2024
2 parents daee214 + ca2d3c1 commit a19389f
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 58 deletions.
5 changes: 0 additions & 5 deletions .github/workflows/smoke-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ jobs:
extra_nix_config: |
accept-flake-config = true
- name: 🧹 Delete cardano-node db (when using mithril)
if: ${{ inputs.use-mithril }}
run: |
rm -rf ${state_dir}/db
- name: 🧹 Cleanup hydra-node state
run: |
rm -rf ${state_dir}/state-*
Expand Down
22 changes: 16 additions & 6 deletions hydra-cluster/exe/hydra-cluster/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Main where

import Hydra.Prelude

import CardanoNode (waitForFullySynchronized, withCardanoNodeDevnet, withCardanoNodeOnKnownNetwork)
import CardanoNode (findRunningCardanoNode, waitForFullySynchronized, withCardanoNodeDevnet, withCardanoNodeOnKnownNetwork)
import Hydra.Cluster.Faucet (publishHydraScriptsAs)
import Hydra.Cluster.Fixture (Actor (Faucet))
import Hydra.Cluster.Mithril (downloadLatestSnapshotTo)
Expand All @@ -11,6 +11,8 @@ import Hydra.Cluster.Scenarios (EndToEndLog (..), singlePartyHeadFullLifeCycle,
import Hydra.Logging (Verbosity (Verbose), traceWith, withTracer)
import HydraNode (HydraClient (..))
import Options.Applicative (ParserInfo, execParser, fullDesc, header, helper, info, progDesc)
import System.Directory (removeDirectoryRecursive)
import System.FilePath ((</>))
import Test.Hydra.Prelude (withTempDir)

main :: IO ()
Expand All @@ -23,21 +25,29 @@ run options =
let fromCardanoNode = contramap FromCardanoNode tracer
withStateDirectory $ \workDir ->
case knownNetwork of
Just network -> do
when (useMithril == UseMithril) $
downloadLatestSnapshotTo (contramap FromMithril tracer) network workDir
withCardanoNodeOnKnownNetwork fromCardanoNode workDir network $ \node -> do
Just network ->
withRunningCardanoNode tracer workDir network $ \node -> do
waitForFullySynchronized fromCardanoNode node
publishOrReuseHydraScripts tracer node
>>= singlePartyHeadFullLifeCycle tracer workDir node
Nothing ->
Nothing -> do
withCardanoNodeDevnet fromCardanoNode workDir $ \node -> do
txId <- publishOrReuseHydraScripts tracer node
singlePartyOpenAHead tracer workDir node txId $ \HydraClient{} -> do
forever (threadDelay 60) -- do nothing
where
Options{knownNetwork, stateDirectory, publishHydraScripts, useMithril} = options

withRunningCardanoNode tracer workDir network action =
findRunningCardanoNode workDir network >>= \case
Just node ->
action node
Nothing -> do
when (useMithril == UseMithril) $ do
removeDirectoryRecursive $ workDir </> "db"
downloadLatestSnapshotTo (contramap FromMithril tracer) network workDir
withCardanoNodeOnKnownNetwork (contramap FromCardanoNode tracer) workDir network action

withStateDirectory action = case stateDirectory of
Nothing -> withTempDir ("hydra-cluster-" <> show knownNetwork) action
Just sd -> action sd
Expand Down
2 changes: 2 additions & 0 deletions hydra-cluster/hydra-cluster.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ executable hydra-cluster
main-is: Main.hs
ghc-options: -threaded -rtsopts
build-depends:
, directory
, filepath
, hydra-cluster
, hydra-node
, hydra-prelude
Expand Down
1 change: 1 addition & 0 deletions hydra-cluster/src/CardanoClient.hs
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,4 @@ data RunningNode = RunningNode
, blockTime :: NominalDiffTime
-- ^ Expected time between blocks (varies a lot on testnets)
}
deriving (Show, Eq)
61 changes: 48 additions & 13 deletions hydra-cluster/src/CardanoNode.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module CardanoNode where
import Hydra.Prelude

import Cardano.Slotting.Time (diffRelativeTime, getRelativeTime, toRelativeTime)
import CardanoClient (QueryPoint (QueryTip), RunningNode (..), queryEraHistory, querySystemStart, queryTipSlotNo)
import CardanoClient (QueryPoint (QueryTip), RunningNode (..), queryEraHistory, queryGenesisParameters, querySystemStart, queryTipSlotNo)
import Control.Lens ((?~), (^?!))
import Control.Tracer (Tracer, traceWith)
import Data.Aeson (Value (String), (.=))
Expand All @@ -17,6 +17,7 @@ import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds)
import Hydra.Cardano.Api (
AsType (AsPaymentKey),
File (..),
GenesisParameters (..),
NetworkId,
NetworkMagic (..),
PaymentKey,
Expand All @@ -28,7 +29,7 @@ import Hydra.Cardano.Api (
getVerificationKey,
)
import Hydra.Cardano.Api qualified as Api
import Hydra.Cluster.Fixture (KnownNetwork (..))
import Hydra.Cluster.Fixture (KnownNetwork (..), toNetworkId)
import Hydra.Cluster.Util (readConfigFile)
import Network.HTTP.Simple (getResponseBody, httpBS, parseRequestThrow)
import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile)
Expand Down Expand Up @@ -125,6 +126,32 @@ getCardanoNodeVersion :: IO String
getCardanoNodeVersion =
readProcess "cardano-node" ["--version"] ""

-- | Tries to find an communicate with an existing cardano-node running in given
-- work directory. NOTE: This is using the default node socket name as defined
-- by 'defaultCardanoNodeArgs'.
findRunningCardanoNode :: FilePath -> KnownNetwork -> IO (Maybe RunningNode)
findRunningCardanoNode workDir knownNetwork = do
try (queryGenesisParameters knownNetworkId socketPath QueryTip) >>= \case
Left (_ :: SomeException) ->
pure Nothing
Right GenesisParameters{protocolParamActiveSlotsCoefficient, protocolParamSlotLength} ->
pure $
Just
RunningNode
{ networkId = knownNetworkId
, nodeSocket = socketPath
, blockTime =
computeBlockTime
protocolParamSlotLength
protocolParamActiveSlotsCoefficient
}
where
knownNetworkId = toNetworkId knownNetwork

socketPath = File $ workDir </> nodeSocket

CardanoNodeArgs{nodeSocket} = defaultCardanoNodeArgs

-- | Start a single cardano-node devnet using the config from config/ and
-- credentials from config/credentials/. Only the 'Faucet' actor will receive
-- "initialFunds". Use 'seedFromFaucet' to distribute funds other wallets.
Expand All @@ -147,9 +174,9 @@ withCardanoNodeOnKnownNetwork ::
KnownNetwork ->
(RunningNode -> IO a) ->
IO a
withCardanoNodeOnKnownNetwork tracer workDir knownNetwork action = do
withCardanoNodeOnKnownNetwork tracer stateDirectory knownNetwork action = do
copyKnownNetworkFiles
withCardanoNode tracer workDir args action
withCardanoNode tracer stateDirectory args action
where
args =
defaultCardanoNodeArgs
Expand All @@ -172,9 +199,9 @@ withCardanoNodeOnKnownNetwork tracer workDir knownNetwork action = do
, "conway-genesis.json"
]
$ \fn -> do
createDirectoryIfMissing True $ workDir </> takeDirectory fn
createDirectoryIfMissing True $ stateDirectory </> takeDirectory fn
fetchConfigFile (knownNetworkPath </> fn)
>>= writeFileBS (workDir </> fn)
>>= writeFileBS (stateDirectory </> fn)

knownNetworkPath =
knownNetworkConfigBaseURL </> knownNetworkName
Expand Down Expand Up @@ -277,7 +304,7 @@ withCardanoNode tr stateDirectory args action = do
Left{} -> error "should never been reached"
Right a -> pure a
where
CardanoNodeArgs{nodeSocket, nodeShelleyGenesisFile} = args
CardanoNodeArgs{nodeSocket} = args

process = cardanoNodeProcess (Just stateDirectory) args

Expand All @@ -290,17 +317,22 @@ withCardanoNode tr stateDirectory args action = do
traceWith tr $ MsgNodeStarting{stateDirectory}
waitForSocket nodeSocketPath
traceWith tr $ MsgSocketIsReady nodeSocketPath
shelleyGenesis :: Aeson.Value <- readShelleyGenesisJSON $ stateDirectory </> nodeShelleyGenesisFile
shelleyGenesis <- readShelleyGenesisJSON $ stateDirectory </> nodeShelleyGenesisFile args
action
RunningNode
{ nodeSocket = nodeSocketPath
{ nodeSocket = File (stateDirectory </> nodeSocket)
, networkId = getShelleyGenesisNetworkId shelleyGenesis
, blockTime = getShelleyGenesisBlockTime shelleyGenesis
}

cleanupSocketFile =
whenM (doesFileExist socketPath) $
removeFile socketPath

readShelleyGenesisJSON = readFileBS >=> unsafeDecodeJson

-- Read 'NetworkId' from shelley genesis JSON file
getShelleyGenesisNetworkId :: Value -> NetworkId
getShelleyGenesisNetworkId json = do
if json ^?! key "networkId" == "Mainnet"
then Api.Mainnet
Expand All @@ -309,14 +341,17 @@ withCardanoNode tr stateDirectory args action = do
Api.Testnet (Api.NetworkMagic $ truncate magic)

-- Read expected time between blocks from shelley genesis
getShelleyGenesisBlockTime :: Value -> NominalDiffTime
getShelleyGenesisBlockTime json = do
let slotLength = json ^?! key "slotLength" . _Number
let activeSlotsCoeff = json ^?! key "activeSlotsCoeff" . _Number
realToFrac $ slotLength / activeSlotsCoeff
computeBlockTime (realToFrac slotLength) (toRational activeSlotsCoeff)

cleanupSocketFile =
whenM (doesFileExist socketPath) $
removeFile socketPath
-- | Compute the block time (expected time between blocks) given a slot length
-- as diff time and active slot coefficient.
computeBlockTime :: NominalDiffTime -> Rational -> NominalDiffTime
computeBlockTime slotLength activeSlotsCoeff =
slotLength / realToFrac activeSlotsCoeff

-- | Wait until the node is fully caught up with the network. This can take a
-- while!
Expand Down
7 changes: 7 additions & 0 deletions hydra-cluster/src/Hydra/Cluster/Fixture.hs
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,10 @@ data KnownNetwork
| Sanchonet
deriving stock (Generic, Show, Eq, Enum, Bounded)
deriving anyclass (ToJSON, FromJSON)

toNetworkId :: KnownNetwork -> NetworkId
toNetworkId = \case
Mainnet -> Api.Mainnet
Preproduction -> Api.Testnet (Api.NetworkMagic 1)
Preview -> Api.Testnet (Api.NetworkMagic 2)
Sanchonet -> Api.Testnet (Api.NetworkMagic 4)
77 changes: 43 additions & 34 deletions hydra-cluster/test/Test/CardanoNodeSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Hydra.Prelude
import Test.Hydra.Prelude

import CardanoNode (
findRunningCardanoNode,
getCardanoNodeVersion,
withCardanoNodeDevnet,
withCardanoNodeOnKnownNetwork,
Expand All @@ -12,8 +13,8 @@ import CardanoNode (
import CardanoClient (RunningNode (..), queryTipSlotNo)
import Hydra.Cardano.Api (NetworkId (Testnet), NetworkMagic (NetworkMagic), unFile)
import Hydra.Cardano.Api qualified as NetworkId
import Hydra.Cluster.Fixture (KnownNetwork (Mainnet))
import Hydra.Logging (showLogsOnFailure)
import Hydra.Cluster.Fixture (KnownNetwork (..))
import Hydra.Logging (Tracer, showLogsOnFailure)
import System.Directory (doesFileExist)

spec :: Spec
Expand All @@ -24,35 +25,43 @@ spec = do
it "has expected cardano-node version available" $
getCardanoNodeVersion >>= (`shouldContain` "8.7.3")

it "withCardanoNodeDevnet does start a block-producing devnet within 5 seconds" $
failAfter 5 $
showLogsOnFailure "CardanoNodeSpec" $ \tr ->
withTempDir "hydra-cluster" $ \tmp ->
withCardanoNodeDevnet tr tmp $
\RunningNode{nodeSocket, networkId, blockTime} -> do
doesFileExist (unFile nodeSocket) `shouldReturn` True
-- NOTE: We hard-code the expected networkId and blockTime here to
-- detect any change to the genesis-shelley.json
networkId `shouldBe` Testnet (NetworkMagic 42)
blockTime `shouldBe` 0.1
-- Should produce blocks (tip advances)
slot1 <- queryTipSlotNo networkId nodeSocket
threadDelay 1
slot2 <- queryTipSlotNo networkId nodeSocket
slot2 `shouldSatisfy` (> slot1)

it "withCardanoNodeOnKnownNetwork on mainnet starts synchronizing within 5 seconds" $
-- NOTE: This implies that withCardanoNodeOnKnownNetwork does not
-- synchronize the whole chain before continuing.
failAfter 5 $
showLogsOnFailure "CardanoNodeSpec" $ \tr ->
withTempDir "hydra-cluster" $ \tmp ->
withCardanoNodeOnKnownNetwork tr tmp Mainnet $
\RunningNode{nodeSocket, networkId, blockTime} -> do
networkId `shouldBe` NetworkId.Mainnet
blockTime `shouldBe` 20
-- Should synchronize blocks (tip advances)
slot1 <- queryTipSlotNo networkId nodeSocket
threadDelay 1
slot2 <- queryTipSlotNo networkId nodeSocket
slot2 `shouldSatisfy` (> slot1)
around (failAfter 5 . setupTracerAndTempDir) $ do
it "withCardanoNodeDevnet does start a block-producing devnet within 5 seconds" $ \(tr, tmp) ->
withCardanoNodeDevnet tr tmp $ \RunningNode{nodeSocket, networkId, blockTime} -> do
doesFileExist (unFile nodeSocket) `shouldReturn` True
-- NOTE: We hard-code the expected networkId and blockTime here to
-- detect any change to the genesis-shelley.json
networkId `shouldBe` Testnet (NetworkMagic 42)
blockTime `shouldBe` 0.1
-- Should produce blocks (tip advances)
slot1 <- queryTipSlotNo networkId nodeSocket
threadDelay 1
slot2 <- queryTipSlotNo networkId nodeSocket
slot2 `shouldSatisfy` (> slot1)

it "withCardanoNodeOnKnownNetwork on mainnet starts synchronizing within 5 seconds" $ \(tr, tmp) ->
-- NOTE: This implies that withCardanoNodeOnKnownNetwork does not
-- synchronize the whole chain before continuing.
withCardanoNodeOnKnownNetwork tr tmp Mainnet $ \RunningNode{nodeSocket, networkId, blockTime} -> do
networkId `shouldBe` NetworkId.Mainnet
blockTime `shouldBe` 20
-- Should synchronize blocks (tip advances)
slot1 <- queryTipSlotNo networkId nodeSocket
threadDelay 1
slot2 <- queryTipSlotNo networkId nodeSocket
slot2 `shouldSatisfy` (> slot1)

describe "findRunningCardanoNode" $ do
it "returns Nothing on non-matching network" $ \(tr, tmp) -> do
withCardanoNodeOnKnownNetwork tr tmp Preview $ \_ -> do
findRunningCardanoNode tmp Preproduction `shouldReturn` Nothing

it "returns Just running node on matching network" $ \(tr, tmp) -> do
withCardanoNodeOnKnownNetwork tr tmp Preview $ \runningNode -> do
findRunningCardanoNode tmp Preview `shouldReturn` Just runningNode

setupTracerAndTempDir :: ToJSON msg => ((Tracer IO msg, FilePath) -> IO a) -> IO a
setupTracerAndTempDir action =
showLogsOnFailure "CardanoNodeSpec" $ \tr ->
withTempDir "hydra-cluster" $ \tmp ->
action (tr, tmp)

0 comments on commit a19389f

Please sign in to comment.