From ca2d3c1d47620c5cce9f11aa6f65c1e2fda81d2c Mon Sep 17 00:00:00 2001 From: Sebastian Nagel Date: Wed, 14 Feb 2024 19:27:47 +0100 Subject: [PATCH] Use already running cardano-node in hydra-cluster At first we tried to read the genesis shelly json files, but we don't know the location of shelley genesis keys and should also not rely on knowing that when using an existing cardano-node. By fetching the genesis parameters we have a chance to compute the right block time (assuming the node is not in Byron era) and provide a RunningNode handle without knowing the genesis json paths. Co-authored-by: Franco Testagrossa --- .github/workflows/smoke-test.yaml | 5 -- hydra-cluster/exe/hydra-cluster/Main.hs | 22 +++++-- hydra-cluster/hydra-cluster.cabal | 2 + hydra-cluster/src/CardanoClient.hs | 1 + hydra-cluster/src/CardanoNode.hs | 61 +++++++++++++---- hydra-cluster/src/Hydra/Cluster/Fixture.hs | 7 ++ hydra-cluster/test/Test/CardanoNodeSpec.hs | 77 ++++++++++++---------- 7 files changed, 117 insertions(+), 58 deletions(-) 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)