diff --git a/.github/workflows/smoke-test.yaml b/.github/workflows/smoke-test.yaml index b590ab94110..1daef05fd7e 100644 --- a/.github/workflows/smoke-test.yaml +++ b/.github/workflows/smoke-test.yaml @@ -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-* diff --git a/hydra-cluster/exe/hydra-cluster/Main.hs b/hydra-cluster/exe/hydra-cluster/Main.hs index 1a699c4d2b4..753515fd032 100644 --- a/hydra-cluster/exe/hydra-cluster/Main.hs +++ b/hydra-cluster/exe/hydra-cluster/Main.hs @@ -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) @@ -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 () @@ -23,14 +25,12 @@ 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 @@ -38,6 +38,16 @@ run options = 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 diff --git a/hydra-cluster/hydra-cluster.cabal b/hydra-cluster/hydra-cluster.cabal index 6eb648a6132..16413680ea8 100644 --- a/hydra-cluster/hydra-cluster.cabal +++ b/hydra-cluster/hydra-cluster.cabal @@ -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 diff --git a/hydra-cluster/src/CardanoClient.hs b/hydra-cluster/src/CardanoClient.hs index 1af3112333b..71d68247801 100644 --- a/hydra-cluster/src/CardanoClient.hs +++ b/hydra-cluster/src/CardanoClient.hs @@ -167,3 +167,4 @@ data RunningNode = RunningNode , blockTime :: NominalDiffTime -- ^ Expected time between blocks (varies a lot on testnets) } + deriving (Show, Eq) diff --git a/hydra-cluster/src/CardanoNode.hs b/hydra-cluster/src/CardanoNode.hs index 3d44148865d..631f34446e0 100644 --- a/hydra-cluster/src/CardanoNode.hs +++ b/hydra-cluster/src/CardanoNode.hs @@ -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), (.=)) @@ -17,6 +17,7 @@ import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) import Hydra.Cardano.Api ( AsType (AsPaymentKey), File (..), + GenesisParameters (..), NetworkId, NetworkMagic (..), PaymentKey, @@ -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) @@ -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. @@ -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 @@ -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 @@ -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 @@ -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 @@ -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! diff --git a/hydra-cluster/src/Hydra/Cluster/Fixture.hs b/hydra-cluster/src/Hydra/Cluster/Fixture.hs index 2d02d2944ab..3e6edb132b4 100644 --- a/hydra-cluster/src/Hydra/Cluster/Fixture.hs +++ b/hydra-cluster/src/Hydra/Cluster/Fixture.hs @@ -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) diff --git a/hydra-cluster/test/Test/CardanoNodeSpec.hs b/hydra-cluster/test/Test/CardanoNodeSpec.hs index 69b9533449c..64464474898 100644 --- a/hydra-cluster/test/Test/CardanoNodeSpec.hs +++ b/hydra-cluster/test/Test/CardanoNodeSpec.hs @@ -4,6 +4,7 @@ import Hydra.Prelude import Test.Hydra.Prelude import CardanoNode ( + findRunningCardanoNode, getCardanoNodeVersion, withCardanoNodeDevnet, withCardanoNodeOnKnownNetwork, @@ -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 @@ -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)