From f87b8a8854a12d7f1d256991f505be9f46e53a3f Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Mon, 23 Feb 2026 11:55:26 +0100 Subject: [PATCH 1/8] Add cardano-rpc cabal dependency --- .github/ISSUE_TEMPLATE/config.yml | 3 ++ .github/workflows/haskell.yml | 3 ++ cabal.project | 34 ++++++++++++++--- cardano-node/cardano-node.cabal | 3 ++ cardano-testnet/cardano-testnet.cabal | 7 ++++ flake.lock | 6 +-- flake.nix | 14 ++----- nix/haskell.nix | 55 ++++++++++++++++++++++++++- nix/pkgs.nix | 12 +++++- 9 files changed, 115 insertions(+), 22 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 87e1b14265e..b470fd332f9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,3 +6,6 @@ contact_links: - name: Cardano API Issues url: https://github.com/IntersectMBO/cardano-api/issues about: Report API related issues here + - name: Cardano gRPC Issues + url: https://github.com/IntersectMBO/cardano-api/issues + about: Report gRPC endpoint related issues here diff --git a/.github/workflows/haskell.yml b/.github/workflows/haskell.yml index 4b719cf27e7..d1b798c1caa 100644 --- a/.github/workflows/haskell.yml +++ b/.github/workflows/haskell.yml @@ -99,6 +99,9 @@ jobs: with: use-sodium-vrf: true # default is true + - name: Install gRPC dependencies + uses: intersectmbo/cardano-api/.github/actions/grpc-deps@a7bd74dfa6ccb1eb04f69791f978a3b9e0cc63ca + - uses: actions/checkout@v4 - name: Cache and install Cabal dependencies diff --git a/cabal.project b/cabal.project index 1a821ddc7db..439e3f85f73 100644 --- a/cabal.project +++ b/cabal.project @@ -14,7 +14,7 @@ repository cardano-haskell-packages -- you need to run if you change them index-state: , hackage.haskell.org 2026-02-06T20:27:32Z - , cardano-haskell-packages 2026-03-03T10:50:34Z + , cardano-haskell-packages 2026-03-11T22:13:22Z constraints: -- haskell.nix patch does not work for 1.6.8 @@ -68,12 +68,34 @@ allow-newer: if impl(ghc >= 9.12) allow-newer: - -- https://github.com/kapralVV/Unique/issues/11 - , Unique:hashable - - -- https://github.com/Gabriella439/Haskell-Pipes-Safe-Library/pull/70 - , pipes-safe:base + -- we need newer io-classes: https://github.com/input-output-hk/typed-protocols/tree/coot/io-classes-1.9 + , io-classes:time + , ouroboros-network:time + , nothunks:time + , network-mux:time + , cardano-ping:time -- IMPORTANT -- Do NOT add more source-repository-package stanzas here unless they are strictly -- temporary! Please read the section in CONTRIBUTING about updating dependencies. + +-- GHC 9.12 support https://github.com/google/proto-lens/pull/519 +source-repository-package + type: git + location: https://github.com/carbolymer/proto-lens + tag: 732ff478957507bdbdaf72606281df3fcb6b0121 + --sha256: sha256-DR2hxFDNMICcueggBObhi+L5bKeake/Mj4N0078P3SA= + subdir: + discrimination-ieee754 + proto-lens-arbitrary + proto-lens-benchmarks + proto-lens-discrimination + proto-lens-optparse + proto-lens-protobuf-types + proto-lens-protoc + proto-lens-runtime + proto-lens-setup + proto-lens-tests-dep + proto-lens-tests + proto-lens + diff --git a/cardano-node/cardano-node.cabal b/cardano-node/cardano-node.cabal index 9f463f9b347..ad7a5b1b0c6 100644 --- a/cardano-node/cardano-node.cabal +++ b/cardano-node/cardano-node.cabal @@ -113,6 +113,7 @@ library Cardano.Node.Tracing.Tracers.NodeVersion Cardano.Node.Tracing.Tracers.P2P Cardano.Node.Tracing.Tracers.Resources + Cardano.Node.Tracing.Tracers.Rpc Cardano.Node.Tracing.Tracers.Shutdown Cardano.Node.Tracing.Tracers.Startup Cardano.Node.Types @@ -155,6 +156,7 @@ library , cardano-prelude , cardano-protocol-tpraos >= 1.4 , cardano-slotting >= 0.2 + , cardano-rpc ^>= 10.1 , cborg ^>= 0.2.4 , containers , contra-tracer @@ -252,6 +254,7 @@ test-suite cardano-node-test , cardano-crypto-class , cardano-crypto-wrapper , cardano-api + , cardano-rpc , cardano-protocol-tpraos , cardano-node , cardano-slotting diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal index a959470b641..b1a8a8f8485 100644 --- a/cardano-testnet/cardano-testnet.cabal +++ b/cardano-testnet/cardano-testnet.cabal @@ -59,6 +59,7 @@ library , cardano-node , cardano-ping ^>= 0.9 , cardano-prelude + , cardano-rpc , contra-tracer , containers , data-default-class @@ -235,6 +236,8 @@ test-suite cardano-testnet-test Cardano.Testnet.Test.Gov.TreasuryDonation Cardano.Testnet.Test.Gov.TreasuryGrowth Cardano.Testnet.Test.Gov.TreasuryWithdrawal + Cardano.Testnet.Test.Rpc.Query + Cardano.Testnet.Test.Rpc.Transaction Cardano.Testnet.Test.Misc Cardano.Testnet.Test.Node.Shutdown Cardano.Testnet.Test.MainnetParams @@ -253,12 +256,15 @@ test-suite cardano-testnet-test , bytestring , cardano-api , cardano-cli:{cardano-cli, cardano-cli-test-lib} + , cardano-ledger-api , cardano-crypto-class + , cardano-ledger-binary , cardano-ledger-conway , cardano-ledger-core , cardano-ledger-shelley , cardano-node , cardano-prelude + , cardano-rpc , cardano-slotting , cardano-strict-containers ^>= 0.1 , cardano-testnet @@ -278,6 +284,7 @@ test-suite cardano-testnet-test , mtl , process , resourcet + , rio , regex-compat , rio , tasty ^>= 1.5 diff --git a/flake.lock b/flake.lock index 7c09d075789..84ba0817f2d 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "CHaP": { "flake": false, "locked": { - "lastModified": 1772623894, - "narHash": "sha256-95NCPKIcDnQ+vja6ofTsnFJKoH9AjT0opOj8zdGvWSw=", + "lastModified": 1773268394, + "narHash": "sha256-0mSHymwEXZHGzZWt0iyiochY3v4V9ErVxFMtQ02K8YM=", "owner": "intersectmbo", "repo": "cardano-haskell-packages", - "rev": "e140e457e9c9db8591f0d8b0c35597ffb65955fc", + "rev": "4b8eec5dd32020a1afacef4005baaa15b4013b5d", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index edfb1e89323..a056d082a1f 100644 --- a/flake.nix +++ b/flake.nix @@ -80,6 +80,8 @@ # on darwin which calls this binary to find certificates pkgs.writeScriptBin "security" ''exec /usr/bin/security "$@"''; + windowsCompilerNixName = "ghc9122"; + supportedSystems = import ./nix/supported-systems.nix; defaultSystem = head supportedSystems; customConfig = @@ -87,13 +89,6 @@ (import ./nix/custom-config.nix customConfig) input.customConfig; - abseilOverlay = final: prev: - prev.lib.optionalAttrs prev.stdenv.hostPlatform.isWindows { - abseil-cpp = prev.abseil-cpp.overrideAttrs (finalAttrs: previousAttrs: { - buildInputs = previousAttrs.buildInputs ++ [prev.pkgs.windows.pthreads]; - }); - }; - overlays = [ # Crypto needs to come before haskell.nix. # FIXME: _THIS_IS_BAD_ @@ -114,7 +109,6 @@ // import ./nix/svclib.nix {inherit (final) pkgs;}; }) (import ./nix/pkgs.nix) - abseilOverlay self.overlay ]; @@ -365,7 +359,7 @@ # Once building, windowsProject candidate for win-arm64 is project.projectCross.ucrtAarch64. // optionalAttrs (elem system ["x86_64-linux"]) { windows = let - windowsProject = project.projectCross.mingwW64; + windowsProject = (project.appendModule { compiler-nix-name = windowsCompilerNixName; }).projectCross.mingwW64; projectExes = collectExes windowsProject; in projectExes @@ -491,7 +485,7 @@ cardanoNodeProject = (import ./nix/haskell.nix { inherit (final) haskell-nix; - inherit CHaP incl; + inherit CHaP incl windowsCompilerNixName; macOS-security = macOS-security (final.pkgs); }) .appendModule [ diff --git a/nix/haskell.nix b/nix/haskell.nix index fcb73320ab1..671476edf57 100644 --- a/nix/haskell.nix +++ b/nix/haskell.nix @@ -5,6 +5,7 @@ , incl , CHaP , macOS-security +, windowsCompilerNixName }: let @@ -21,7 +22,7 @@ let { src = ../.; name = "cardano-node"; - compiler-nix-name = lib.mkDefault (if pkgs.stdenv.hostPlatform.isWindows then "ghc9122" else "ghc967"); + compiler-nix-name = lib.mkDefault (if pkgs.stdenv.hostPlatform.isWindows then windowsCompilerNixName else "ghc967"); # Extra-compilers # flake.variants = lib.genAttrs ["ghc$VERSION"] (x: {compiler-nix-name = x;}); cabalProjectLocal = '' @@ -310,7 +311,11 @@ let # also needs them to be quoted) export WORKDIR=$TMP/testTracerExt ''; - }) + }) + ({pkgs, ...}: { + packages.proto-lens-protobuf-types.components.library.build-tools = [ pkgs.buildPackages.protobuf ]; + packages.cardano-rpc.components.library.build-tools = [ pkgs.buildPackages.protobuf ]; + }) ({ lib, pkgs, ... }: lib.mkIf (!pkgs.stdenv.hostPlatform.isDarwin) { # Needed for profiled builds to fix an issue loading recursion-schemes part of makeBaseFunctor # that is missing from the `_p` output. See https://gitlab.haskell.org/ghc/ghc/-/issues/18320 @@ -368,6 +373,52 @@ let # TODO add flags to packages (like cs-ledger) so we can turn off tests that will # not build for windows on a per package bases (rather than using --disable-tests). # configureArgs = lib.optionalString stdenv.hostPlatform.isWindows "--disable-tests"; + + # TODO remove this module when removing proto-lens SRP + # Override proto-lens source to fetch submodules and fix symlinks + ({ + pkgs, + lib, + ... + }: let + protoLensSrc = pkgs.fetchgit { + url = "https://github.com/carbolymer/proto-lens"; + rev = "732ff478957507bdbdaf72606281df3fcb6b0121"; + sha256 = "sha256-DR2hxFDNMICcueggBObhi+L5bKeake/Mj4N0078P3SA="; + fetchSubmodules = true; + }; + # Fix proto-lens source by copying google protobuf files alongside proto-lens subdirectory + fixProtoLensSubdir = subdir: + pkgs.runCommand "proto-lens-${subdir}-fixed" {} '' + mkdir -p $out + cp -r ${protoLensSrc}/${subdir}/* $out/ + chmod -R +w $out + # Fix proto-lens-imports symlink in proto-lens + if [ -d $out/proto-lens-imports ]; then + rm -f $out/proto-lens-imports/google + cp -r ${protoLensSrc}/google/protobuf/src/google $out/proto-lens-imports/ + fi + # Fix proto-src symlink in proto-lens-protobuf-types + if [ -L $out/proto-src ]; then + rm -f $out/proto-src + cp -r ${protoLensSrc}/google/protobuf/src $out/proto-src + fi + chmod -R -w $out + ''; + in { + packages.proto-lens.src = lib.mkForce (fixProtoLensSubdir "proto-lens"); + packages.proto-lens-arbitrary.src = lib.mkForce (protoLensSrc + "/proto-lens-arbitrary"); + packages.proto-lens-discrimination.src = lib.mkForce (protoLensSrc + "/proto-lens-discrimination"); + packages.proto-lens-optparse.src = lib.mkForce (protoLensSrc + "/proto-lens-optparse"); + packages.proto-lens-protobuf-types.src = lib.mkForce (fixProtoLensSubdir "proto-lens-protobuf-types"); + packages.proto-lens-protoc.src = lib.mkForce (protoLensSrc + "/proto-lens-protoc"); + packages.proto-lens-runtime.src = lib.mkForce (protoLensSrc + "/proto-lens-runtime"); + packages.proto-lens-setup.src = lib.mkForce (protoLensSrc + "/proto-lens-setup"); + packages.proto-lens-tests-dep.src = lib.mkForce (protoLensSrc + "/proto-lens-tests-dep"); + packages.proto-lens-tests.src = lib.mkForce (protoLensSrc + "/proto-lens-tests"); + packages.discrimination-ieee754.src = lib.mkForce (protoLensSrc + "/discrimination-ieee754"); + packages.proto-lens-benchmarks.src = lib.mkForce (protoLensSrc + "/proto-lens-benchmarks"); + }) ]; }); in diff --git a/nix/pkgs.nix b/nix/pkgs.nix index 5b596cc04ed..e02b2af85ea 100644 --- a/nix/pkgs.nix +++ b/nix/pkgs.nix @@ -4,7 +4,7 @@ final: prev: let inherit (builtins) foldl' fromJSON listToAttrs map readFile; inherit (final) pkgs; - inherit (prev.pkgs) lib; + inherit (prev) lib; inherit (prev) customConfig; # Parametrized helper entrypoint for the workbench development environment. workbench = import ./workbench @@ -215,3 +215,13 @@ in with final; }; }; } +// lib.optionalAttrs prev.stdenv.hostPlatform.isWindows { + abseil-cpp = prev.abseil-cpp.overrideAttrs (finalAttrs: previousAttrs: { + buildInputs = previousAttrs.buildInputs ++ [prev.windows.pthreads]; + }); +} +// lib.optionalAttrs prev.stdenv.hostPlatform.isMusl { + snappy = prev.snappy.overrideAttrs (old: { + cmakeFlags = map (f: if f == "-DBUILD_SHARED_LIBS=ON" then "-DBUILD_SHARED_LIBS=OFF" else f) (old.cmakeFlags or []); + }); +} From 13b5255d11a7f7544d8a3c740c9023f118264123 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Thu, 19 Feb 2026 17:16:26 +0100 Subject: [PATCH 2/8] Add cardano-rpc configuration to node configuration file --- .../src/Cardano/Node/Configuration/POM.hs | 25 +++++++++++- cardano-node/src/Cardano/Node/Parsers.hs | 38 +++++++++++++++---- cardano-node/src/Cardano/Node/Types.hs | 11 ++++++ .../test/Test/Cardano/Node/FilePermissions.hs | 5 ++- cardano-node/test/Test/Cardano/Node/POM.hs | 8 ++++ .../files/golden/help.cli | 6 ++- .../files/golden/help/cardano.cli | 11 ++++-- .../files/golden/help/create-env.cli | 11 ++++-- 8 files changed, 95 insertions(+), 20 deletions(-) diff --git a/cardano-node/src/Cardano/Node/Configuration/POM.hs b/cardano-node/src/Cardano/Node/Configuration/POM.hs index 13f9052837d..c80be17d1ed 100644 --- a/cardano-node/src/Cardano/Node/Configuration/POM.hs +++ b/cardano-node/src/Cardano/Node/Configuration/POM.hs @@ -10,6 +10,8 @@ {-# OPTIONS_GHC -Wno-noncanonical-monoid-instances #-} +{- HLINT ignore "Functor law" -} + module Cardano.Node.Configuration.POM ( NodeConfiguration (..) , ResponderCoreAffinityPolicy (..) @@ -34,6 +36,8 @@ import Cardano.Node.Configuration.Socket (SocketConfig (..)) import Cardano.Node.Handlers.Shutdown import Cardano.Node.Protocol.Types (Protocol (..)) import Cardano.Node.Types +import Cardano.Rpc.Server.Config (PartialRpcConfig, RpcConfig, RpcConfigF (..), + makeRpcConfig) import Cardano.Tracing.Config import Cardano.Tracing.OrphanInstances.Network () import Ouroboros.Consensus.Ledger.SupportsMempool @@ -196,6 +200,9 @@ data NodeConfiguration , ncGenesisConfig :: GenesisConfig , ncResponderCoreAffinityPolicy :: ResponderCoreAffinityPolicy + + -- gRPC + , ncRpcConfig :: RpcConfig } deriving (Eq, Show) -- | We expose the `Ouroboros.Network.Mux.ForkPolicy` as a `NodeConfiguration` field. @@ -282,6 +289,7 @@ data PartialNodeConfiguration , pncSyncTargetOfKnownBigLedgerPeers :: !(Last Int) , pncSyncTargetOfEstablishedBigLedgerPeers :: !(Last Int) , pncSyncTargetOfActiveBigLedgerPeers :: !(Last Int) + -- Minimum number of active big ledger peers we must be connected to -- in Genesis mode , pncMinBigLedgerPeersForTrustedState :: !(Last NumberOfBigLedgerPeers) @@ -296,6 +304,9 @@ data PartialNodeConfiguration , pncGenesisConfigFlags :: !(Last GenesisConfigFlags) , pncResponderCoreAffinityPolicy :: !(Last ResponderCoreAffinityPolicy) + + -- gRPC + , pncRpcConfig :: !PartialRpcConfig } deriving (Eq, Generic, Show) instance AdjustFilePaths PartialNodeConfiguration where @@ -412,6 +423,12 @@ instance FromJSON PartialNodeConfiguration where <$> v .:? "ResponderCoreAffinityPolicy" <*> v .:? "ForkPolicy" -- deprecated + pncRpcConfig <- + RpcConfig + <$> (Last <$> v .:? "EnableRpc") + <*> (Last <$> v .:? "RpcSocketPath") + <*> pure mempty + pure PartialNodeConfiguration { pncProtocolConfig , pncSocketConfig = Last . Just $ SocketConfig mempty mempty mempty pncSocketPath @@ -459,6 +476,7 @@ instance FromJSON PartialNodeConfiguration where , pncPeerSharing , pncGenesisConfigFlags , pncResponderCoreAffinityPolicy + , pncRpcConfig } where parseMempoolCapacityBytesOverride v = parseNoOverride <|> parseOverride @@ -724,6 +742,7 @@ defaultPartialNodeConfiguration = , pncGenesisConfigFlags = Last (Just defaultGenesisConfigFlags) -- https://ouroboros-consensus.cardano.intersectmbo.org/haddocks/ouroboros-consensus-diffusion/Ouroboros-Consensus-Node-Genesis.html#v:defaultGenesisConfigFlags , pncResponderCoreAffinityPolicy = Last $ Just NoResponderCoreAffinity + , pncRpcConfig = mempty } lastOption :: Parser a -> Parser (Last a) @@ -821,7 +840,7 @@ makeNodeConfiguration pnc = do , getLast (pncMempoolTimeoutHard pnc) , getLast (pncMempoolTimeoutCapacity pnc) ) - (ncMempoolTimeoutSoft, ncMempoolTimeoutHard, ncMempoolTimeoutCapacity) <- + (ncMempoolTimeoutSoft, ncMempoolTimeoutHard, ncMempoolTimeoutCapacity) <- case mempoolTimeouts of (Just s, Just h, Just c) -> pure (s, h, c) (Nothing, Nothing, Nothing) -> pure (1, 1.5, 5) @@ -874,6 +893,9 @@ makeNodeConfiguration pnc = do experimentalProtocols <- lastToEither "Missing ExperimentalProtocolsEnabled" $ pncExperimentalProtocolsEnabled pnc + + ncRpcConfig <- makeRpcConfig $ (pncRpcConfig pnc){nodeSocketPath=ncSocketPath socketConfig} + return $ NodeConfiguration { ncConfigFile = configFile , ncTopologyFile = topologyFile @@ -922,6 +944,7 @@ makeNodeConfiguration pnc = do , ncConsensusMode , ncGenesisConfig , ncResponderCoreAffinityPolicy + , ncRpcConfig } ncProtocol :: NodeConfiguration -> Protocol diff --git a/cardano-node/src/Cardano/Node/Parsers.hs b/cardano-node/src/Cardano/Node/Parsers.hs index b6ec0c7441b..b940928ad0c 100644 --- a/cardano-node/src/Cardano/Node/Parsers.hs +++ b/cardano-node/src/Cardano/Node/Parsers.hs @@ -15,19 +15,20 @@ module Cardano.Node.Parsers import Cardano.Logging.Types import qualified Cardano.Logging.Types as Net -import Cardano.Node.Configuration.NodeAddress ( - NodeHostIPv4Address (NodeHostIPv4Address), File (..), +import Cardano.Node.Configuration.NodeAddress (File (..), + NodeHostIPv4Address (NodeHostIPv4Address), NodeHostIPv6Address (NodeHostIPv6Address), PortNumber, SocketPath) import Cardano.Node.Configuration.POM (PartialNodeConfiguration (..), lastOption) import Cardano.Node.Configuration.Socket import Cardano.Node.Handlers.Shutdown import Cardano.Node.Types import Cardano.Prelude (ConvertText (..)) +import Cardano.Rpc.Server.Config (PartialRpcConfig, RpcConfigF (..)) import Ouroboros.Consensus.Ledger.SupportsMempool import Ouroboros.Consensus.Node -import Data.Foldable import Data.Char (isDigit) +import Data.Foldable import Data.Maybe (fromMaybe) import Data.Monoid (Last (..)) import Data.Text (Text) @@ -57,7 +58,7 @@ nodeRunParser = do topFp <- lastOption parseTopologyFile dbFp <- lastOption parseNodeDatabasePaths validate <- lastOption parseValidateDB - socketFp <- lastOption $ parseSocketPath "Path to a cardano-node socket" + socketFp <- lastOption $ parseSocketPath "socket-path" "Path to a cardano-node socket" traceForwardSocket <- lastOption parseTracerSocketMode nodeConfigFp <- lastOption parseConfigFile @@ -84,6 +85,9 @@ nodeRunParser = do -- Hidden options (to be removed eventually) maybeMempoolCapacityOverride <- lastOption parseMempoolCapacityOverride + -- gRPC + pncRpcConfig <- parseRpcConfig + pure $ PartialNodeConfiguration { pncSocketConfig = Last . Just $ SocketConfig @@ -144,12 +148,15 @@ nodeRunParser = do , pncPeerSharing = mempty , pncGenesisConfigFlags = mempty , pncResponderCoreAffinityPolicy = mempty + , pncRpcConfig } -parseSocketPath :: Text -> Parser SocketPath -parseSocketPath helpMessage = +parseSocketPath :: Text -- ^ option name + -> Text -- ^ help text + -> Parser SocketPath +parseSocketPath optionName helpMessage = fmap File $ strOption $ mconcat - [ long "socket-path" + [ long (toS optionName) , help (toS helpMessage) , completer (bashCompleter "file") , metavar "FILEPATH" @@ -423,6 +430,23 @@ parseStartAsNonProducingNode = ] ] +parseRpcConfig :: Parser PartialRpcConfig +parseRpcConfig = do + isEnabled <- lastOption parseRpcToggle + socketPath <- lastOption parseRpcSocketPath + pure $ RpcConfig isEnabled socketPath mempty + where + parseRpcToggle :: Parser Bool + parseRpcToggle = + Opt.flag' True $ mconcat + [ long "grpc-enable" + , help "[EXPERIMENTAL] Enable node gRPC endpoint." + ] + parseRpcSocketPath :: Parser SocketPath + parseRpcSocketPath = + parseSocketPath + "grpc-socket-path" + "[EXPERIMENTAL] gRPC socket path. Defaults to rpc.sock in the same directory as node socket." -- | Produce just the brief help header for a given CLI option parser, -- without the options. diff --git a/cardano-node/src/Cardano/Node/Types.hs b/cardano-node/src/Cardano/Node/Types.hs index 321d038cdc1..56497bea386 100644 --- a/cardano-node/src/Cardano/Node/Types.hs +++ b/cardano-node/src/Cardano/Node/Types.hs @@ -47,6 +47,7 @@ import qualified Cardano.Crypto.Hash as Crypto import Cardano.Network.ConsensusMode (ConsensusMode (..)) import Cardano.Node.Configuration.Socket (SocketConfig (..)) import Cardano.Node.Orphans () +import Cardano.Rpc.Server.Config (RpcConfigF (..)) import Ouroboros.Network.NodeToNode (DiffusionMode (..)) import Control.Exception @@ -498,6 +499,16 @@ instance AdjustFilePaths a => AdjustFilePaths (Maybe a) where instance AdjustFilePaths a => AdjustFilePaths (Last a) where adjustFilePaths f = fmap (adjustFilePaths f) +instance AdjustFilePaths (File a b) where + adjustFilePaths f (File p) = File $ f p + +instance Functor f => AdjustFilePaths (RpcConfigF f) where + adjustFilePaths f (RpcConfig isEnabled rpcSocketPath nodeSocketPath) = + RpcConfig + isEnabled + (adjustFilePaths f <$> rpcSocketPath) + (adjustFilePaths f <$> nodeSocketPath) + data VRFPrivateKeyFilePermissionError = OtherPermissionsExist FilePath | GroupPermissionsExist FilePath diff --git a/cardano-node/test/Test/Cardano/Node/FilePermissions.hs b/cardano-node/test/Test/Cardano/Node/FilePermissions.hs index 524e1ee3593..0aa16e86453 100644 --- a/cardano-node/test/Test/Cardano/Node/FilePermissions.hs +++ b/cardano-node/test/Test/Cardano/Node/FilePermissions.hs @@ -39,6 +39,7 @@ import Hedgehog.Internal.Property (Group (..), failWith) import System.IO (FilePath, IO) import Text.Show (Show (..)) import Cardano.Node.Types (VRFPrivateKeyFilePermissionError (..)) +import Control.Exception (bracket) #ifdef UNIX @@ -47,7 +48,7 @@ import System.Posix.IO (closeFd, createFile) import System.Posix.Types (FileMode) import Hedgehog -import Hedgehog.Extras +import qualified Hedgehog.Extras as H import qualified Hedgehog.Gen as Gen #endif @@ -134,7 +135,7 @@ prop_sanityCheck_checkVRFFilePermissions = (const . liftIO . runExceptT $ checkVRFFilePermissions capturingTracer vrfPrivateKeyGroup) case groupResult of Left (GroupPermissionsExist _) -> do - note_ "Group permissions check should not fail" + H.note_ "Group permissions check should not fail" failure Left err -> failWith Nothing $ "checkVRFFilePermissions should not have failed with error: " diff --git a/cardano-node/test/Test/Cardano/Node/POM.hs b/cardano-node/test/Test/Cardano/Node/POM.hs index d4de440fbd7..b738c0f1368 100644 --- a/cardano-node/test/Test/Cardano/Node/POM.hs +++ b/cardano-node/test/Test/Cardano/Node/POM.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TemplateHaskell #-} @@ -14,6 +15,7 @@ import Cardano.Node.Configuration.POM import Cardano.Node.Configuration.Socket import Cardano.Node.Handlers.Shutdown import Cardano.Node.Types +import Cardano.Rpc.Server.Config (makeRpcConfig) import Cardano.Tracing.Config (PartialTraceOptions (..), defaultPartialTraceConfiguration, partialTraceSelectionToEither) import Ouroboros.Consensus.Node (NodeDatabasePaths (..)) @@ -27,7 +29,9 @@ import Ouroboros.Network.NodeToNode (AcceptedConnectionsLimit (..), DiffusionMode (InitiatorAndResponderDiffusionMode)) import Ouroboros.Network.PeerSelection.PeerSharing (PeerSharing (..)) +import Data.Bifunctor (first) import Data.Monoid (Last (..)) +import Data.String import Data.Text (Text) import Hedgehog (Property, discover, withTests, (===)) @@ -174,6 +178,7 @@ testPartialYamlConfig = , pncResponderCoreAffinityPolicy = mempty , pncLedgerDbConfig = mempty , pncEgressPollInterval = mempty + , pncRpcConfig = mempty } -- | Example partial configuration theoretically created @@ -227,6 +232,7 @@ testPartialCliConfig = , pncResponderCoreAffinityPolicy = mempty , pncLedgerDbConfig = mempty , pncEgressPollInterval = mempty + , pncRpcConfig = mempty } -- | Expected final NodeConfiguration @@ -234,6 +240,7 @@ eExpectedConfig :: Either Text NodeConfiguration eExpectedConfig = do traceOptions <- partialTraceSelectionToEither (return $ PartialTracingOnLegacy defaultPartialTraceConfiguration) + ncRpcConfig <- first fromString $ makeRpcConfig mempty return $ NodeConfiguration { ncSocketConfig = SocketConfig mempty mempty mempty mempty , ncShutdownConfig = ShutdownConfig Nothing (Just . ASlot $ SlotNo 42) @@ -286,6 +293,7 @@ eExpectedConfig = do , ncGenesisConfig = disableGenesisConfig , ncResponderCoreAffinityPolicy = NoResponderCoreAffinity , ncLedgerDbConfig = LedgerDbConfiguration DefaultNumOfDiskSnapshots DefaultSnapshotInterval DefaultQueryBatchSize V2InMemory noDeprecatedOptions + , ncRpcConfig } -- ----------------------------------------------------------------------------- diff --git a/cardano-testnet/test/cardano-testnet-golden/files/golden/help.cli b/cardano-testnet/test/cardano-testnet-golden/files/golden/help.cli index 90b3c89d4b7..a588a089f04 100644 --- a/cardano-testnet/test/cardano-testnet-golden/files/golden/help.cli +++ b/cardano-testnet/test/cardano-testnet-golden/files/golden/help.cli @@ -2,11 +2,12 @@ Usage: cardano-testnet (cardano | create-env | version | help) Usage: cardano-testnet cardano [--num-pool-nodes COUNT] [--max-lovelace-supply WORD64] - [--nodeLoggingFormat LOGGING_FORMAT] + [--node-logging-format LOGGING_FORMAT] [--num-dreps NUMBER] [--enable-new-epoch-state-logging] [--generate-tx-generator-config] [--output-dir DIRECTORY] + [--enable-grpc] [--testnet-magic INT] [--epoch-length SLOTS] [--slot-length SECONDS] @@ -19,11 +20,12 @@ Usage: cardano-testnet cardano [--num-pool-nodes COUNT] Usage: cardano-testnet create-env [--num-pool-nodes COUNT] [--max-lovelace-supply WORD64] - [--nodeLoggingFormat LOGGING_FORMAT] + [--node-logging-format LOGGING_FORMAT] [--num-dreps NUMBER] [--enable-new-epoch-state-logging] [--generate-tx-generator-config] [--output-dir DIRECTORY] + [--enable-grpc] [--testnet-magic INT] [--epoch-length SLOTS] [--slot-length SECONDS] diff --git a/cardano-testnet/test/cardano-testnet-golden/files/golden/help/cardano.cli b/cardano-testnet/test/cardano-testnet-golden/files/golden/help/cardano.cli index a4ad433beaa..e277537f732 100644 --- a/cardano-testnet/test/cardano-testnet-golden/files/golden/help/cardano.cli +++ b/cardano-testnet/test/cardano-testnet-golden/files/golden/help/cardano.cli @@ -1,10 +1,11 @@ Usage: cardano-testnet cardano [--num-pool-nodes COUNT] [--max-lovelace-supply WORD64] - [--nodeLoggingFormat LOGGING_FORMAT] + [--node-logging-format LOGGING_FORMAT] [--num-dreps NUMBER] [--enable-new-epoch-state-logging] [--generate-tx-generator-config] [--output-dir DIRECTORY] + [--enable-grpc] [--testnet-magic INT] [--epoch-length SLOTS] [--slot-length SECONDS] @@ -22,9 +23,8 @@ Available options: Max lovelace supply that your testnet starts with. Ignored if a node environment is passed. (default: 100000020000000) - --nodeLoggingFormat LOGGING_FORMAT - Node logging format (json|text) - (default: NodeLoggingFormatAsJson) + --node-logging-format LOGGING_FORMAT + Node logging format (json|text) (default: json) --num-dreps NUMBER Number of delegate representatives (DReps) to generate. Ignored if a node environment is passed. (default: 3) @@ -37,6 +37,9 @@ Available options: --output-dir DIRECTORY Directory where to store files, sockets, and so on. It is created if it doesn't exist. If unset, a temporary directory is used. + --enable-grpc [EXPERIMENTAL] Enable gRPC endpoint on all of testnet + nodes. The listening socket file will be the same + directory as node's N2C socket. --testnet-magic INT Specify a testnet magic id. (default: 42) --epoch-length SLOTS Epoch length, in number of slots. Ignored if a node environment is passed. (default: 500) diff --git a/cardano-testnet/test/cardano-testnet-golden/files/golden/help/create-env.cli b/cardano-testnet/test/cardano-testnet-golden/files/golden/help/create-env.cli index 6a9c346496e..089232720a6 100644 --- a/cardano-testnet/test/cardano-testnet-golden/files/golden/help/create-env.cli +++ b/cardano-testnet/test/cardano-testnet-golden/files/golden/help/create-env.cli @@ -1,10 +1,11 @@ Usage: cardano-testnet create-env [--num-pool-nodes COUNT] [--max-lovelace-supply WORD64] - [--nodeLoggingFormat LOGGING_FORMAT] + [--node-logging-format LOGGING_FORMAT] [--num-dreps NUMBER] [--enable-new-epoch-state-logging] [--generate-tx-generator-config] [--output-dir DIRECTORY] + [--enable-grpc] [--testnet-magic INT] [--epoch-length SLOTS] [--slot-length SECONDS] @@ -21,9 +22,8 @@ Available options: Max lovelace supply that your testnet starts with. Ignored if a node environment is passed. (default: 100000020000000) - --nodeLoggingFormat LOGGING_FORMAT - Node logging format (json|text) - (default: NodeLoggingFormatAsJson) + --node-logging-format LOGGING_FORMAT + Node logging format (json|text) (default: json) --num-dreps NUMBER Number of delegate representatives (DReps) to generate. Ignored if a node environment is passed. (default: 3) @@ -36,6 +36,9 @@ Available options: --output-dir DIRECTORY Directory where to store files, sockets, and so on. It is created if it doesn't exist. If unset, a temporary directory is used. + --enable-grpc [EXPERIMENTAL] Enable gRPC endpoint on all of testnet + nodes. The listening socket file will be the same + directory as node's N2C socket. --testnet-magic INT Specify a testnet magic id. (default: 42) --epoch-length SLOTS Epoch length, in number of slots. Ignored if a node environment is passed. (default: 500) From e3a5465169df950249627c1cb324002927b6d9d8 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Thu, 19 Feb 2026 17:16:50 +0100 Subject: [PATCH 3/8] Add cardano-rpc tracing --- cardano-node/src/Cardano/Node/Tracing.hs | 4 +- .../src/Cardano/Node/Tracing/Documentation.hs | 13 +- .../src/Cardano/Node/Tracing/Tracers.hs | 5 + .../src/Cardano/Node/Tracing/Tracers/Rpc.hs | 125 ++++++++++++++++++ cardano-node/src/Cardano/Tracing/Tracers.hs | 17 +-- 5 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs diff --git a/cardano-node/src/Cardano/Node/Tracing.hs b/cardano-node/src/Cardano/Node/Tracing.hs index 2a751a58562..127a1e75ce0 100644 --- a/cardano-node/src/Cardano/Node/Tracing.hs +++ b/cardano-node/src/Cardano/Node/Tracing.hs @@ -10,6 +10,7 @@ module Cardano.Node.Tracing ) where import Cardano.Logging.Resources +import qualified Cardano.Network.Diffusion as Cardano.Diffusion import Cardano.Node.Handlers.Shutdown (ShutdownTrace) import Cardano.Node.Startup (NodeInfo, NodeStartupInfo, StartupTrace (..)) import Cardano.Node.Tracing.StateRep (NodeState) @@ -17,12 +18,12 @@ import Cardano.Node.Tracing.Tracers.ConsensusStartupException (ConsensusStartupException (..)) import Cardano.Node.Tracing.Tracers.LedgerMetrics (LedgerMetrics) import Cardano.Node.Tracing.Tracers.NodeVersion (NodeVersionTrace) +import Cardano.Rpc.Server (TraceRpc) import qualified Ouroboros.Consensus.Network.NodeToClient as NodeToClient import qualified Ouroboros.Consensus.Network.NodeToNode as NodeToNode import qualified Ouroboros.Consensus.Node.Tracers as Consensus import qualified Ouroboros.Consensus.Storage.ChainDB as ChainDB import Ouroboros.Network.ConnectionId -import qualified Cardano.Network.Diffusion as Cardano.Diffusion import Prelude (IO) @@ -50,4 +51,5 @@ data Tracers peer localPeer blk m = Tracers , nodeStateTracer :: !(Tracer IO NodeState) , resourcesTracer :: !(Tracer IO ResourceStats) , ledgerMetricsTracer :: !(Tracer IO LedgerMetrics) + , rpcTracer :: !(Tracer IO TraceRpc) } diff --git a/cardano-node/src/Cardano/Node/Tracing/Documentation.hs b/cardano-node/src/Cardano/Node/Tracing/Documentation.hs index 1658bed634d..d3e341754fa 100644 --- a/cardano-node/src/Cardano/Node/Tracing/Documentation.hs +++ b/cardano-node/src/Cardano/Node/Tracing/Documentation.hs @@ -24,6 +24,9 @@ import Cardano.Git.Rev (gitRev) import Cardano.Logging as Logging import Cardano.Logging.Resources import Cardano.Logging.Resources.Types () +import qualified Cardano.Network.PeerSelection.ExtraRootPeers as Cardano.PublicRootPeers +import qualified Cardano.Network.PeerSelection.Governor.PeerSelectionState as Cardano +import qualified Cardano.Network.PeerSelection.Governor.Types as Cardano import Cardano.Network.PeerSelection.PeerTrustable (PeerTrustable (..)) import Cardano.Node.Handlers.Shutdown (ShutdownTrace) import Cardano.Node.Startup @@ -45,11 +48,10 @@ import Cardano.Node.Tracing.Tracers.NodeToClient () import Cardano.Node.Tracing.Tracers.NodeToNode () import Cardano.Node.Tracing.Tracers.NodeVersion (NodeVersionTrace) import Cardano.Node.Tracing.Tracers.P2P () +import Cardano.Node.Tracing.Tracers.Rpc () import Cardano.Node.Tracing.Tracers.Shutdown () import Cardano.Node.Tracing.Tracers.Startup () -import qualified Cardano.Network.PeerSelection.Governor.PeerSelectionState as Cardano -import qualified Cardano.Network.PeerSelection.Governor.Types as Cardano -import qualified Cardano.Network.PeerSelection.ExtraRootPeers as Cardano.PublicRootPeers +import Cardano.Rpc.Server (TraceRpc) import Cardano.Tracing.OrphanInstances.Network () import Ouroboros.Consensus.Block.SupportsSanityCheck (SanityCheckIssue) import Ouroboros.Consensus.BlockchainTime.WallClock.Types (RelativeTime) @@ -699,6 +701,9 @@ docTracersFirstPhase condConfigFileName = do internalTrDoc <- documentTracer (internalTr :: Logging.Trace IO TraceDispatcherMessage) + rpcTr <- mkCardanoTracer trBase trForward mbTrEKG ["RPC"] + configureTracers configReflection trConfig [rpcTr] + rpcTrDoc <- documentTracer (rpcTr :: Logging.Trace IO TraceRpc) let bl = nodeInfoDpDoc <> nodeStartupInfoDpDoc @@ -767,6 +772,8 @@ docTracersFirstPhase condConfigFileName = do <> localServerTrDoc <> localInboundGovernorTrDoc <> dtAcceptPolicyTrDoc +-- gRPC + <> rpcTrDoc -- Internal tracer <> internalTrDoc diff --git a/cardano-node/src/Cardano/Node/Tracing/Tracers.hs b/cardano-node/src/Cardano/Node/Tracing/Tracers.hs index 485d28e71f0..f7f8e7b21ab 100644 --- a/cardano-node/src/Cardano/Node/Tracing/Tracers.hs +++ b/cardano-node/src/Cardano/Node/Tracing/Tracers.hs @@ -36,6 +36,7 @@ import Cardano.Node.Tracing.Tracers.NodeToClient () import Cardano.Node.Tracing.Tracers.NodeToNode () import Cardano.Node.Tracing.Tracers.NodeVersion (getNodeVersion) import Cardano.Node.Tracing.Tracers.P2P () +import Cardano.Node.Tracing.Tracers.Rpc () import Cardano.Node.Tracing.Tracers.Shutdown () import Cardano.Node.Tracing.Tracers.Startup () import Ouroboros.Consensus.Ledger.Inspect (LedgerEvent) @@ -152,6 +153,9 @@ mkDispatchTracers nodeKernel trBase trForward mbTrEKG trDataPoint trConfig p = d !churnModeTr <- mkCardanoTracer trBase trForward mbTrEKG ["Net", "PeerSelection", "ChurnMode"] configureTracers configReflection trConfig [churnModeTr] + !rpcTr <- mkCardanoTracer trBase trForward mbTrEKG ["RPC"] + configureTracers configReflection trConfig [rpcTr] + traceTracerInfo trBase trForward configReflection let warnings = checkNodeTraceConfiguration' trConfig @@ -183,6 +187,7 @@ mkDispatchTracers nodeKernel trBase trForward mbTrEKG trDataPoint trConfig p = d , nodeVersionTracer = Tracer (traceWith nodeVersionTr) , resourcesTracer = Tracer (traceWith resourcesTr) , ledgerMetricsTracer = Tracer (traceWith ledgerMetricsTr) + , rpcTracer = Tracer (traceWith rpcTr) } mkConsensusTracers :: forall blk. diff --git a/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs b/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs new file mode 100644 index 00000000000..d81458625fb --- /dev/null +++ b/cardano-node/src/Cardano/Node/Tracing/Tracers/Rpc.hs @@ -0,0 +1,125 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +module Cardano.Node.Tracing.Tracers.Rpc () where + +import Cardano.Api.Pretty + +import Cardano.Logging hiding (nsInner) +import Cardano.Rpc.Server (TraceRpc (..), TraceRpcQuery (..), TraceRpcSubmit (..), + TraceSpanEvent (..)) + +import Data.Aeson (Object, Value (..), (.=)) + +instance LogFormatting TraceRpc where + forMachine _dtal tr = + mconcat $ + ("reason" .= prettyShow tr) + : case tr of + TraceRpcFatalError _ -> ["kind" .= String "FatalError"] + TraceRpcError _ -> ["kind" .= String "Error"] + TraceRpcQuery queryTrace -> + ["kind" .= String "QueryService"] + <> case queryTrace of + TraceRpcQueryParamsSpan s -> + [ "queryName" .= String "ReadParams" + , spanToObject s + ] + TraceRpcQueryReadUtxosSpan s -> + [ "queryName" .= String "ReadUtxos" + , spanToObject s + ] + TraceRpcSubmit submitTrace -> + ["kind" .= String "SubmitService"] + <> case submitTrace of + TraceRpcSubmitN2cConnectionError _ -> [] + TraceRpcSubmitTxDecodingError _ -> [] + TraceRpcSubmitTxValidationError _ -> [] + TraceRpcSubmitSpan s -> [spanToObject s] + + forHuman = docToText . pretty + + asMetrics = \case + -- metrics for each rpc request + -- query names here are taken from UTXORPC spec: https://utxorpc.org/query/intro/#operations + TraceRpcQuery (TraceRpcQueryParamsSpan (SpanBegin _)) -> [CounterM "rpc.request.QueryService.ReadParams" Nothing] + TraceRpcQuery (TraceRpcQueryReadUtxosSpan (SpanBegin _)) -> [CounterM "rpc.request.QueryService.ReadUtxos" Nothing] + TraceRpcSubmit (TraceRpcSubmitSpan (SpanBegin _)) -> [CounterM "rpc.request.SubmitService.SubmitTx" Nothing] + _ -> [] + +instance MetaTrace TraceRpc where + namespaceFor = + Namespace [] . \case + TraceRpcFatalError _ -> ["FatalError"] + TraceRpcError _ -> ["Error"] + TraceRpcQuery queryTrace -> + "QueryService" + : case queryTrace of + TraceRpcQueryParamsSpan _ -> ["ReadParams", "Span"] + TraceRpcQueryReadUtxosSpan _ -> ["ReadUtxos", "Span"] + TraceRpcSubmit submitTrace -> + "SubmitService" + : case submitTrace of + TraceRpcSubmitN2cConnectionError _ -> ["N2cConnectionError"] + TraceRpcSubmitTxDecodingError _ -> ["TxDecodingError"] + TraceRpcSubmitTxValidationError _ -> ["TxValidationError"] + TraceRpcSubmitSpan _ -> ["SubmitTx", "Span"] + + severityFor (Namespace _ nsInner) _ = case nsInner of + ["FatalError"] -> Just Error -- RPC server startup errors + ["Error"] -> Just Debug -- those are normal operation errors, like request errors, hide them by default + ["QueryService", "ReadParams", "Span"] -> Just Debug + ["QueryService", "ReadUtxos", "Span"] -> Just Debug + ["SubmitService", "SubmitTx", "Span"] -> Just Debug + ["SubmitService", "N2cConnectionError"] -> Just Warning -- this is a more serious error, this shouldn't happen + ["SubmitService", "TxDecodingError"] -> Just Debug -- request error + ["SubmitService", "TxValidationError"] -> Just Debug -- request error + _ -> Nothing + + documentFor (Namespace _ nsInner) = case nsInner of + ["FatalError"] -> Just "RPC startup critical error." + ["Error"] -> Just "Normal operation errors such as request errors. Those are not harmful to the RPC server itself." + ["QueryService", "ReadParams", "Span"] -> Just "Span for the ReadParams UTXORPC method." + ["QueryService", "ReadUtxos", "Span"] -> Just "Span for the ReadUtxos UTXORPC method." + ["SubmitService", "SubmitTx", "Span"] -> Just "Span for the SubmitTx UTXORPC method." + ["SubmitService", "N2cConnectionError"] -> + Just + "Node connection error. This should not happen, as this means that there is an issue in cardano-rpc configuration." + ["SubmitService", "TxDecodingError"] -> Just "A regular request error, when submitted transaction decoding fails." + ["SubmitService", "TxValidationError"] -> Just "A regular request error, when submitted transaction is invalid." + _ -> Nothing + + metricsDocFor (Namespace _ nsInner) = case nsInner of + ["QueryService", "ReadParams", "Span"] -> + [("rpc.request.QueryService.ReadParams", "Span for the ReadParams UTXORPC method.")] + ["QueryService", "ReadUtxos", "Span"] -> + [("rpc.request.QueryService.ReadUtxos", "Span for the ReadUtxos UTXORPC method.")] + ["SubmitService", "SubmitTx", "Span"] -> + [("rpc.request.SubmitService.SubmitTx", "Span for the SubmitTx UTXORPC method.")] + _ -> [] + + allNamespaces = + Namespace [] + <$> [ ["FatalError"] + , ["Error"] + , ["QueryService", "ReadParams", "Span"] + , ["QueryService", "ReadUtxos", "Span"] + , ["SubmitService", "SubmitTx", "Span"] + , ["SubmitService", "N2cConnectionError"] + , ["SubmitService", "TxDecodingError"] + , ["SubmitService", "TxValidationError"] + ] + +-- helper functions + +spanToObject :: TraceSpanEvent -> Object +spanToObject = + mconcat . \case + SpanBegin spanId -> ["span" .= String "begin", "spanId" .= spanId] + SpanEnd spanId -> ["span" .= String "end", "spanId" .= spanId] diff --git a/cardano-node/src/Cardano/Tracing/Tracers.hs b/cardano-node/src/Cardano/Tracing/Tracers.hs index 869a3015eed..109a16c764d 100644 --- a/cardano-node/src/Cardano/Tracing/Tracers.hs +++ b/cardano-node/src/Cardano/Tracing/Tracers.hs @@ -35,6 +35,9 @@ import Cardano.BM.Data.Transformers import Cardano.BM.Internal.ElidingTracer import Cardano.BM.Trace (traceNamedObject) import Cardano.BM.Tracing +import Cardano.Network.Diffusion (CardanoPeerSelectionCounters) +import qualified Cardano.Network.Diffusion.Types as Cardano.Diffusion +import qualified Cardano.Network.PeerSelection.Governor.Types as Cardano import Cardano.Node.Configuration.Logging import Cardano.Node.Protocol.Byron () import Cardano.Node.Protocol.Shelley () @@ -46,7 +49,7 @@ import Cardano.Node.Tracing import qualified Cardano.Node.Tracing.Tracers.Consensus as ConsensusTracers import qualified Cardano.Node.Tracing.Tracers.Diffusion as DiffusionTracers import Cardano.Node.Tracing.Tracers.NodeVersion -import Cardano.Network.Diffusion (CardanoPeerSelectionCounters) +import Cardano.Node.Tracing.Tracers.Rpc () import Cardano.Protocol.TPraos.OCert (KESPeriod (..)) import Cardano.Slotting.Slot (EpochNo (..), SlotNo (..), WithOrigin (..)) import Cardano.Tracing.Config @@ -66,8 +69,8 @@ import Ouroboros.Consensus.Ledger.Abstract (LedgerErr, LedgerState) import Ouroboros.Consensus.Ledger.Extended (ledgerState) import Ouroboros.Consensus.Ledger.Inspect (InspectLedger, LedgerEvent) import Ouroboros.Consensus.Ledger.Query (BlockQuery, Query) -import Ouroboros.Consensus.Ledger.SupportsMempool (ApplyTxErr, GenTx, GenTxId, HasTxs, - LedgerSupportsMempool, ByteSize32 (..)) +import Ouroboros.Consensus.Ledger.SupportsMempool (ApplyTxErr, ByteSize32 (..), GenTx, + GenTxId, HasTxs, LedgerSupportsMempool) import Ouroboros.Consensus.Ledger.SupportsProtocol (LedgerSupportsProtocol) import Ouroboros.Consensus.Mempool (MempoolSize (..), TraceEventMempool (..)) import Ouroboros.Consensus.MiniProtocol.BlockFetch.Server @@ -84,9 +87,6 @@ import Ouroboros.Consensus.Util.Enclose import qualified Network.Mux as Mux -import qualified Cardano.Network.Diffusion.Types as Cardano.Diffusion -import qualified Cardano.Network.PeerSelection.Governor.Types as Cardano - import qualified Ouroboros.Network.AnchoredFragment as AF import Ouroboros.Network.Block (BlockNo (..), ChainUpdate (..), HasHeader (..), Point, StandardHash, blockNo, pointSlot, unBlockNo) @@ -104,8 +104,7 @@ import Ouroboros.Network.InboundGovernor.State as InboundGovernor import Ouroboros.Network.NodeToClient (LocalAddress) import Ouroboros.Network.NodeToNode (RemoteAddress) import Ouroboros.Network.PeerSelection.Churn (ChurnCounters (..)) -import Ouroboros.Network.PeerSelection.Governor ( - PeerSelectionView (..)) +import Ouroboros.Network.PeerSelection.Governor (PeerSelectionView (..)) import qualified Ouroboros.Network.PeerSelection.Governor as Governor import Ouroboros.Network.Point (fromWithOrigin, withOrigin) import Ouroboros.Network.Protocol.LocalStateQuery.Type (LocalStateQuery, ShowQuery) @@ -364,6 +363,7 @@ mkTracers blockConfig tOpts@(TracingOnLegacy trSel) tr nodeKern ekgDirect = do , nodeStateTracer = nullTracer , resourcesTracer = nullTracer , ledgerMetricsTracer = nullTracer + , rpcTracer = nullTracer } where traceForgeEnabledMetric :: Maybe EKGDirect -> StartupTrace blk -> IO () @@ -537,6 +537,7 @@ mkTracers _ _ _ _ _ = , nodeVersionTracer = nullTracer , resourcesTracer = nullTracer , ledgerMetricsTracer = nullTracer + , rpcTracer = nullTracer } -------------------------------------------------------------------------------- From 971fb11dde6f2144f045421032cbaeff7e8791eb Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Thu, 19 Feb 2026 17:18:00 +0100 Subject: [PATCH 4/8] Add cardano-rpc to node startup --- cardano-node/src/Cardano/Node/Run.hs | 171 ++++++++++++++------------- 1 file changed, 92 insertions(+), 79 deletions(-) diff --git a/cardano-node/src/Cardano/Node/Run.hs b/cardano-node/src/Cardano/Node/Run.hs index edc43f58078..5c55043e5b5 100644 --- a/cardano-node/src/Cardano/Node/Run.hs +++ b/cardano-node/src/Cardano/Node/Run.hs @@ -53,6 +53,8 @@ import Cardano.Node.Protocol.Shelley (PraosLeaderCredentialsError (..) ShelleyProtocolInstantiationError (PraosLeaderCredentialsError)) import Cardano.Node.Protocol.Types import Cardano.Node.Queries +import Cardano.Rpc.Server +import Cardano.Rpc.Server.Config import Cardano.Node.Startup import Cardano.Node.TraceConstraints (TraceConstraints) import Cardano.Node.Tracing.API @@ -122,6 +124,7 @@ import Ouroboros.Network.Protocol.ChainSync.Codec import Control.Applicative (empty) import Control.Concurrent (killThread, mkWeakThreadId, myThreadId, getNumCapabilities) +import Control.Concurrent.Async import Control.Concurrent.Class.MonadSTM.Strict import Control.Exception (try, Exception, IOException) import qualified Control.Exception as Exception @@ -154,6 +157,7 @@ import Network.HostName (getHostName) import Network.Socket (Socket) import System.Directory (canonicalizePath, createDirectoryIfMissing, makeAbsolute) import System.Environment (lookupEnv) +import System.FilePath (takeDirectory, ()) import System.IO (hPutStrLn) #ifdef UNIX import GHC.Weak (deRefWeak) @@ -164,9 +168,8 @@ import System.Posix.Types (FileMode) import System.Win32.File #endif import Paths_cardano_node (version) - -import Paths_cardano_node (version) -import Ouroboros.Consensus.Mempool (MempoolTimeoutConfig(..)) +import Ouroboros.Consensus.Mempool (MempoolTimeoutConfig(..)) +import GHC.Stack {- HLINT ignore "Fuse concatMap/map" -} {- HLINT ignore "Redundant <$>" -} @@ -176,37 +179,45 @@ runNode :: PartialNodeConfiguration -> IO () runNode cmdPc = do - installSigTermHandler - - Crypto.cryptoInit + installSigTermHandler - configYamlPc <- parseNodeConfigurationFP . getLast $ pncConfigFile cmdPc + Crypto.cryptoInit - nc <- case makeNodeConfiguration $ defaultPartialNodeConfiguration <> configYamlPc <> cmdPc of - Left err -> error $ "Error in creating the NodeConfiguration: " <> err - Right nc' -> return nc' + nc@NodeConfiguration + { ncProtocolConfig + , ncProtocolFiles=ncProtocolFiles@ProtocolFilepaths{shelleyVRFFile=mShelleyVrfFile} + } <- buildNodeConfiguration cmdPc - putStrLn $ "Node configuration: " <> show nc + let earlyTracer = stdoutTracer + traceWith earlyTracer $ "Node configuration: " <> show nc - case ncProtocolFiles nc of - ProtocolFilepaths{shelleyVRFFile=Just vrfFp} -> - runThrowExceptT $ - checkVRFFilePermissions stdoutTracer (File vrfFp) - _ -> pure () + forM_ mShelleyVrfFile $ + runThrowExceptT . checkVRFFilePermissions earlyTracer . File - consensusProtocol <- - runThrowExceptT $ - mkConsensusProtocol - (ncProtocolConfig nc) - -- TODO: Convert ncProtocolFiles to Maybe as relay nodes - -- don't need these. - (Just $ ncProtocolFiles nc) + consensusProtocol <- + runThrowExceptT $ + mkConsensusProtocol + ncProtocolConfig + -- TODO: Convert ncProtocolFiles to Maybe as relay nodes + -- don't need these. + (Just ncProtocolFiles) - handleNodeWithTracers cmdPc nc consensusProtocol + handleNodeWithTracers cmdPc nc consensusProtocol runThrowExceptT :: Exception e => ExceptT e IO a -> IO a runThrowExceptT act = runExceptT act >>= either Exception.throwIO pure +-- | Read node configuration from a file specified in 'PartialNodeConfiguration' +buildNodeConfiguration :: HasCallStack + => PartialNodeConfiguration -- ^ defaults + -> IO NodeConfiguration +buildNodeConfiguration partialConf = do + configYamlPc <- parseNodeConfigurationFP . getLast $ pncConfigFile partialConf + either + (\err -> error $ "Error in creating the NodeConfiguration: " <> err) + pure + $ makeNodeConfiguration (defaultPartialNodeConfiguration <> configYamlPc <> partialConf) + -- | Workaround to ensure that the main thread throws an async exception on -- receiving a SIGTERM signal. installSigTermHandler :: IO () @@ -511,63 +522,65 @@ handleSimpleNode blockType runP tracers nc onKernel = do #endif nForkPolicy <- getForkPolicy $ ncResponderCoreAffinityPolicy nc cForkPolicy <- getForkPolicy $ ncResponderCoreAffinityPolicy nc - void $ - let diffusionNodeArguments :: Cardano.Diffusion.CardanoNodeArguments IO - diffusionNodeArguments = Cardano.Diffusion.CardanoNodeArguments { - Cardano.Diffusion.consensusMode = ncConsensusMode nc, - Cardano.Diffusion.genesisPeerTargets = - PeerSelectionTargets { - targetNumberOfRootPeers = ncSyncTargetOfRootPeers nc, - targetNumberOfKnownPeers = ncSyncTargetOfKnownPeers nc, - targetNumberOfEstablishedPeers = ncSyncTargetOfEstablishedPeers nc, - targetNumberOfActivePeers = ncSyncTargetOfActivePeers nc, - targetNumberOfKnownBigLedgerPeers = ncSyncTargetOfKnownBigLedgerPeers nc, - targetNumberOfEstablishedBigLedgerPeers = ncSyncTargetOfEstablishedBigLedgerPeers nc, - targetNumberOfActiveBigLedgerPeers = ncSyncTargetOfActiveBigLedgerPeers nc - }, - Cardano.Diffusion.minNumOfBigLedgerPeers = ncMinBigLedgerPeersForTrustedState nc, - Cardano.Diffusion.tracerChurnMode = churnModeTracer tracers - } + let diffusionNodeArguments :: Cardano.Diffusion.CardanoNodeArguments IO + diffusionNodeArguments = Cardano.Diffusion.CardanoNodeArguments { + Cardano.Diffusion.consensusMode = ncConsensusMode nc, + Cardano.Diffusion.genesisPeerTargets = + PeerSelectionTargets { + targetNumberOfRootPeers = ncSyncTargetOfRootPeers nc, + targetNumberOfKnownPeers = ncSyncTargetOfKnownPeers nc, + targetNumberOfEstablishedPeers = ncSyncTargetOfEstablishedPeers nc, + targetNumberOfActivePeers = ncSyncTargetOfActivePeers nc, + targetNumberOfKnownBigLedgerPeers = ncSyncTargetOfKnownBigLedgerPeers nc, + targetNumberOfEstablishedBigLedgerPeers = ncSyncTargetOfEstablishedBigLedgerPeers nc, + targetNumberOfActiveBigLedgerPeers = ncSyncTargetOfActiveBigLedgerPeers nc + }, + Cardano.Diffusion.minNumOfBigLedgerPeers = ncMinBigLedgerPeersForTrustedState nc, + Cardano.Diffusion.tracerChurnMode = churnModeTracer tracers + } - diffusionConfiguration :: Cardano.Diffusion.CardanoConfiguration IO - diffusionConfiguration = - mkDiffusionConfiguration - publicIPv4SocketOrAddr - publicIPv6SocketOrAddr - localSocketOrPath - publicPeerSelectionVar - nForkPolicy cForkPolicy - (readTVar localRootsVar) - (readTVar publicRootsVar) - (readTVar useLedgerVar) - (readTVar ledgerPeerSnapshotVar) - nc - in - Node.run - nodeArgs { - rnNodeKernelHook = \registry nodeKernel -> do - -- reinstall `SIGHUP` handler - installSigHUPHandler (startupTracer tracers) (Consensus.kesAgentTracer $ consensusTracers tracers) blockType nc nodeKernel - localRootsVar publicRootsVar useLedgerVar useBootstrapVar - ledgerPeerSnapshotPathVar ledgerPeerSnapshotVar - rnNodeKernelHook nodeArgs registry nodeKernel - } - StdRunNodeArgs - { srnBfcMaxConcurrencyBulkSync = unMaxConcurrencyBulkSync <$> ncMaxConcurrencyBulkSync nc - , srnBfcMaxConcurrencyDeadline = unMaxConcurrencyDeadline <$> ncMaxConcurrencyDeadline nc - , srnChainDbValidateOverride = ncValidateDB nc - , srnDatabasePath = dbPath - , srnDiffusionConfiguration = diffusionConfiguration - , srnDiffusionArguments = diffusionNodeArguments - , srnDiffusionTracers = diffusionTracers tracers - , srnEnableInDevelopmentVersions = ncExperimentalProtocolsEnabled nc - , srnTraceChainDB = chainDBTracer tracers - , srnMaybeMempoolCapacityOverride = ncMaybeMempoolCapacityOverride nc - , srnChainSyncIdleTimeout = customizeChainSyncTimeout - , srnSnapshotPolicyArgs = snapshotPolicyArgs - , srnQueryBatchSize = queryBatchSize - , srnLdbFlavorArgs = selectorToArgs ldbBackend + diffusionConfiguration :: Cardano.Diffusion.CardanoConfiguration IO + diffusionConfiguration = + mkDiffusionConfiguration + publicIPv4SocketOrAddr + publicIPv6SocketOrAddr + localSocketOrPath + publicPeerSelectionVar + nForkPolicy cForkPolicy + (readTVar localRootsVar) + (readTVar publicRootsVar) + (readTVar useLedgerVar) + (readTVar ledgerPeerSnapshotVar) + nc + + ProtocolInfo{pInfoConfig} = fst $ Api.protocolInfo @IO runP + networkMagic :: Api.NetworkMagic = getNetworkMagic $ Consensus.configBlock pInfoConfig + withAsync (runRpcServer (rpcTracer tracers) (ncRpcConfig nc, networkMagic)) $ \_ -> + Node.run + nodeArgs { + rnNodeKernelHook = \registry nodeKernel -> do + -- reinstall `SIGHUP` handler + installSigHUPHandler (startupTracer tracers) (Consensus.kesAgentTracer $ consensusTracers tracers) blockType nc nodeKernel + localRootsVar publicRootsVar useLedgerVar useBootstrapVar + ledgerPeerSnapshotPathVar ledgerPeerSnapshotVar + rnNodeKernelHook nodeArgs registry nodeKernel } + StdRunNodeArgs + { srnBfcMaxConcurrencyBulkSync = unMaxConcurrencyBulkSync <$> ncMaxConcurrencyBulkSync nc + , srnBfcMaxConcurrencyDeadline = unMaxConcurrencyDeadline <$> ncMaxConcurrencyDeadline nc + , srnChainDbValidateOverride = ncValidateDB nc + , srnDatabasePath = dbPath + , srnDiffusionConfiguration = diffusionConfiguration + , srnDiffusionArguments = diffusionNodeArguments + , srnDiffusionTracers = diffusionTracers tracers + , srnEnableInDevelopmentVersions = ncExperimentalProtocolsEnabled nc + , srnTraceChainDB = chainDBTracer tracers + , srnMaybeMempoolCapacityOverride = ncMaybeMempoolCapacityOverride nc + , srnChainSyncIdleTimeout = customizeChainSyncTimeout + , srnSnapshotPolicyArgs = snapshotPolicyArgs + , srnQueryBatchSize = queryBatchSize + , srnLdbFlavorArgs = selectorToArgs ldbBackend + } where customizeChainSyncTimeout :: ChainSyncIdleTimeout customizeChainSyncTimeout = case ncChainSyncIdleTimeout nc of From 9ad4fb5f030691f2503f59610d6fd6bb9eca60ea Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Thu, 19 Feb 2026 17:21:24 +0100 Subject: [PATCH 5/8] Add cardano-rpc support in cardano-testnet --- cardano-testnet/src/Cardano/Testnet.hs | 2 ++ cardano-testnet/src/Parsers/Cardano.hs | 12 ++++++--- cardano-testnet/src/Testnet/Defaults.hs | 28 +++++++++++++++++++- cardano-testnet/src/Testnet/Runtime.hs | 2 +- cardano-testnet/src/Testnet/Start/Cardano.hs | 3 ++- cardano-testnet/src/Testnet/Start/Types.hs | 20 +++++++++++++- cardano-testnet/src/Testnet/Types.hs | 6 +++++ 7 files changed, 66 insertions(+), 7 deletions(-) diff --git a/cardano-testnet/src/Cardano/Testnet.hs b/cardano-testnet/src/Cardano/Testnet.hs index a5c88dee0e6..66f8a0346d7 100644 --- a/cardano-testnet/src/Cardano/Testnet.hs +++ b/cardano-testnet/src/Cardano/Testnet.hs @@ -11,6 +11,7 @@ module Cardano.Testnet ( -- ** Testnet options CardanoTestnetOptions(..), + RpcSupport(..), NodeOption(..), cardanoDefaultTestnetNodeOptions, getDefaultAlonzoGenesis, @@ -46,6 +47,7 @@ module Cardano.Testnet ( TestnetNode(..), isTestnetNodeSpo, nodeSocketPath, + nodeRpcSocketPath, ) where import Testnet.Components.Query diff --git a/cardano-testnet/src/Parsers/Cardano.hs b/cardano-testnet/src/Parsers/Cardano.hs index 12dc8f77e85..6d6a200401f 100644 --- a/cardano-testnet/src/Parsers/Cardano.hs +++ b/cardano-testnet/src/Parsers/Cardano.hs @@ -6,6 +6,7 @@ module Parsers.Cardano ) where import Cardano.Api (AnyShelleyBasedEra (..)) +import Cardano.Api.Pretty import Cardano.CLI.EraBased.Common.Option hiding (pNetworkId) import Cardano.Prelude (readMaybe) @@ -54,10 +55,10 @@ pCardanoTestnetCliOptions = CardanoTestnetOptions <*> pure (AnyShelleyBasedEra defaultEra) <*> pMaxLovelaceSupply <*> OA.option (OA.eitherReader readNodeLoggingFormat) - ( OA.long "nodeLoggingFormat" + ( OA.long "node-logging-format" <> OA.help "Node logging format (json|text)" <> OA.metavar "LOGGING_FORMAT" - <> OA.showDefault + <> OA.showDefaultWith prettyShow <> OA.value (cardanoNodeLoggingFormat def) ) <*> OA.option OA.auto @@ -67,7 +68,7 @@ pCardanoTestnetCliOptions = CardanoTestnetOptions <> OA.showDefault <> OA.value 3 ) - <*> OA.flag False True + <*> OA.switch ( OA.long "enable-new-epoch-state-logging" <> OA.help "Enable new epoch state logging to logs/ledger-epoch-state.log" <> OA.showDefault @@ -82,6 +83,11 @@ pCardanoTestnetCliOptions = CardanoTestnetOptions <> OA.help "Directory where to store files, sockets, and so on. It is created if it doesn't exist. If unset, a temporary directory is used." <> OA.metavar "DIRECTORY" ))) + <*> OA.flag RpcDisabled RpcEnabled + ( OA.long "enable-grpc" + <> OA.help "[EXPERIMENTAL] Enable gRPC endpoint on all of testnet nodes. The listening socket file will be the same directory as node's N2C socket." + <> OA.showDefault + ) pTestnetNodeOptions :: Parser (NonEmpty NodeOption) pTestnetNodeOptions = diff --git a/cardano-testnet/src/Testnet/Defaults.hs b/cardano-testnet/src/Testnet/Defaults.hs index e93e7b17aa1..0b624fcfbc4 100644 --- a/cardano-testnet/src/Testnet/Defaults.hs +++ b/cardano-testnet/src/Testnet/Defaults.hs @@ -188,7 +188,7 @@ defaultYamlHardforkViaConfig :: ShelleyBasedEra era -> Aeson.KeyMap Aeson.Value defaultYamlHardforkViaConfig sbe = defaultYamlConfig <> tracers - <> [("TraceOptions", Aeson.Object mempty)] + <> [("TraceOptions", traceOptions)] <> protocolVersions sbe <> hardforkViaConfig sbe where @@ -302,6 +302,32 @@ defaultYamlHardforkViaConfig sbe = , (proxyName (Proxy @TraceTxSubmissionProtocol), False) ] + traceOptions = Aeson.Object mempty + -- Uncomment this to enable prometheus endpoint on a cardano-testnet. + -- N.B. Every testnet node will start trying to listen on PrometheusSimple endpoint + -- meaning you can only run a one-node testnet, otherwise there will be a port collision. + -- This is because all testnet nodes share config with each other. + -- For a proper solution, use cardano-tracer to consume all the logs and just expose a single + -- stream of traces from all testnet nodes. + -- See also: + -- * https://developers.cardano.org/docs/get-started/infrastructure/node/new-tracing-system/cardano-tracer/ + -- * ./cardano-tracer/docs/cardano-tracer.md + -- + -- traceOptions = do + -- Aeson.object + -- [ "" .= Aeson.object + -- [ "backends" .= Aeson.Array + -- [ "EKGBackend" + -- , "PrometheusSimple suffix 0.0.0.0 12798" + -- -- , "Stdout MachineFormat" + -- , "Stdout HumanFormatColoured" + -- ] + -- , "detail" .= ("DNormal" :: Aeson.Value) + -- -- , "severity" .= ("Notice" :: Aeson.Value) + -- , "severity" .= ("Debug" :: Aeson.Value) + -- ] + -- ] + defaultYamlConfig :: Aeson.KeyMap Aeson.Value defaultYamlConfig = Aeson.fromList diff --git a/cardano-testnet/src/Testnet/Runtime.hs b/cardano-testnet/src/Testnet/Runtime.hs index 1ef96d658e3..8431209fa2d 100644 --- a/cardano-testnet/src/Testnet/Runtime.hs +++ b/cardano-testnet/src/Testnet/Runtime.hs @@ -146,7 +146,7 @@ startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do left MaxSprocketLengthExceededError let socketAbsPath = H.sprocketSystemName sprocket - completeNodeCmd = nodeCmd ++ + completeNodeCmd = nodeCmd <> [ "--socket-path", H.sprocketArgumentName sprocket , "--port", show port , "--host-addr", showIpv4Address ipv4 diff --git a/cardano-testnet/src/Testnet/Start/Cardano.hs b/cardano-testnet/src/Testnet/Start/Cardano.hs index 4fb0f28242a..894afd83772 100644 --- a/cardano-testnet/src/Testnet/Start/Cardano.hs +++ b/cardano-testnet/src/Testnet/Start/Cardano.hs @@ -235,6 +235,7 @@ cardanoTestnet let CardanoTestnetOptions { cardanoEnableNewEpochStateLogging=enableNewEpochStateLogging , cardanoNodes + , cardanoEnableRpc } = testnetOptions nPools = cardanoNumPools testnetOptions nodeConfigFile = tmpAbsPath "configuration.yaml" @@ -346,7 +347,7 @@ cardanoTestnet ] <> spoNodeCliArgs <> extraCliArgs nodeOptions - + <> ["--grpc-enable" | RpcEnabled <- [cardanoEnableRpc]] pure $ eRuntime <&> \rt -> rt{poolKeys=mKeys} let (failedNodes, testnetNodes') = partitionEithers (NEL.toList eTestnetNodes) diff --git a/cardano-testnet/src/Testnet/Start/Types.hs b/cardano-testnet/src/Testnet/Start/Types.hs index 90acfc8abbc..5f4f1792ba1 100644 --- a/cardano-testnet/src/Testnet/Start/Types.hs +++ b/cardano-testnet/src/Testnet/Start/Types.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingVia #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} @@ -15,6 +16,7 @@ module Testnet.Start.Types , NumPools(..) , NumRelays(..) , TxGeneratorSupport(..) + , RpcSupport(..) , cardanoNumPools , cardanoNumRelays @@ -174,6 +176,11 @@ data TxGeneratorSupport | GenerateTemplateConfigForTxGenerator deriving (Eq, Show) +data RpcSupport + = RpcDisabled + | RpcEnabled + deriving (Eq, Show) + -- | Options which, contrary to 'GenesisOptions' are not implemented -- by tuning the genesis files. data CardanoTestnetOptions = CardanoTestnetOptions @@ -187,6 +194,8 @@ data CardanoTestnetOptions = CardanoTestnetOptions , cardanoEnableNewEpochStateLogging :: Bool -- ^ if epoch state logging is enabled , cardanoEnableTxGenerator :: TxGeneratorSupport -- ^ Options regarding support for the tx-generator on the testnet (config generation, execution, etc.) , cardanoOutputDir :: UserProvidedEnv -- ^ The output directory where to store files, sockets, and so on. If unset, a temporary directory is used. + , cardanoEnableRpc :: RpcSupport + -- ^ Whether to enable gRPC endpoints in all testnet nodes } deriving (Eq, Show) -- | Path to the configuration file of the node, specified by the user @@ -223,6 +232,7 @@ instance Default CardanoTestnetOptions where , cardanoEnableNewEpochStateLogging = True , cardanoEnableTxGenerator = NoTxGeneratorSupport , cardanoOutputDir = def + , cardanoEnableRpc = RpcDisabled } -- | Options that are implemented by writing fields in the Shelley genesis file. @@ -272,7 +282,15 @@ cardanoDefaultTestnetNodeOptions = , RelayNodeOptions [] ] -data NodeLoggingFormat = NodeLoggingFormatAsJson | NodeLoggingFormatAsText deriving (Eq, Show) +data NodeLoggingFormat + = NodeLoggingFormatAsJson + | NodeLoggingFormatAsText + deriving (Eq, Show) + +instance Pretty NodeLoggingFormat where + pretty = \case + NodeLoggingFormatAsJson -> "json" + NodeLoggingFormatAsText -> "text" data NodeConfiguration diff --git a/cardano-testnet/src/Testnet/Types.hs b/cardano-testnet/src/Testnet/Types.hs index 07bfcf709fc..3b1737bfd94 100644 --- a/cardano-testnet/src/Testnet/Types.hs +++ b/cardano-testnet/src/Testnet/Types.hs @@ -21,6 +21,7 @@ module Testnet.Types , testnetSprockets , TestnetNode(..) , nodeSocketPath + , nodeRpcSocketPath , nodeConnectionInfo , isTestnetNodeSpo , SpoNodeKeys(..) @@ -52,6 +53,7 @@ import Cardano.Crypto.ProtocolMagic (RequiresNetworkMagic (..)) import Cardano.Node.Configuration.POM import qualified Cardano.Node.Protocol.Byron as Byron import Cardano.Node.Types +import Cardano.Rpc.Server.Config (nodeSocketPathToRpcSocketPath) import Prelude @@ -148,6 +150,10 @@ isTestnetNodeSpo = isJust . poolKeys nodeSocketPath :: TestnetNode -> SocketPath nodeSocketPath = File . H.sprocketSystemName . nodeSprocket +-- | Provide a default RPC socket path +nodeRpcSocketPath :: TestnetNode -> SocketPath +nodeRpcSocketPath = nodeSocketPathToRpcSocketPath . nodeSocketPath + -- | Connection data for a node in the testnet nodeConnectionInfo :: MonadTest m => TestnetRuntime From c83492f63d0852f4f75b63cb7c6c99119a4a2537 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Thu, 19 Feb 2026 17:21:43 +0100 Subject: [PATCH 6/8] Add cardano-rpc tests in cardano-testnet --- cardano-testnet/cardano-testnet.cabal | 1 - .../Cardano/Testnet/Test/Rpc/Query.hs | 193 ++++++++++++++++++ .../Cardano/Testnet/Test/Rpc/Transaction.hs | 160 +++++++++++++++ .../cardano-testnet-test.hs | 6 + 4 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs create mode 100644 cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Transaction.hs diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal index b1a8a8f8485..1ef32e1e5fe 100644 --- a/cardano-testnet/cardano-testnet.cabal +++ b/cardano-testnet/cardano-testnet.cabal @@ -284,7 +284,6 @@ test-suite cardano-testnet-test , mtl , process , resourcet - , rio , regex-compat , rio , tasty ^>= 1.5 diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs new file mode 100644 index 00000000000..e5625952c66 --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs @@ -0,0 +1,193 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Testnet.Test.Rpc.Query + ( hprop_rpc_query_pparams + ) +where + +import Cardano.Api +import qualified Cardano.Api.Ledger as L + +import Cardano.CLI.Type.Output (QueryTipLocalStateOutput (..)) +import qualified Cardano.Ledger.Api as L +import qualified Cardano.Ledger.Binary.Version as L +import qualified Cardano.Ledger.Conway.Core as L +import qualified Cardano.Ledger.Conway.PParams as L +import qualified Cardano.Ledger.Plutus as L +import qualified Cardano.Rpc.Client as Rpc +import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Query as U5c +import Cardano.Rpc.Server.Internal.UtxoRpc.Query () +import Cardano.Rpc.Server.Internal.UtxoRpc.Type (anyUtxoDataUtxoRpcToUtxo, + utxoRpcBigIntToInteger) +import Cardano.Testnet + +import Prelude + +import Control.Exception +import qualified Data.ByteString.Short as SBS +import Data.Default.Class +import qualified Data.Map.Strict as M +import Lens.Micro + +import Testnet.Components.Query +import Testnet.Process.Run +import Testnet.Property.Util (integrationRetryWorkspace) +import Testnet.Start.Types + +import Hedgehog +import qualified Hedgehog as H +import qualified Hedgehog.Extras.Test.Base as H +import qualified Hedgehog.Extras.Test.TestWatchdog as H + +-- | Run with: +-- @TASTY_PATTERN='/RPC Query Protocol Params/' cabal test cardano-testnet-test@ +hprop_rpc_query_pparams :: Property +hprop_rpc_query_pparams = integrationRetryWorkspace 2 "rpc-query-pparams" $ \tempAbsBasePath' -> H.runWithDefaultWatchdog_ $ do + conf@Conf{tempAbsPath} <- mkConf tempAbsBasePath' + let tempAbsPath' = unTmpAbsPath tempAbsPath + + let ceo = ConwayEraOnwardsConway + sbe = convert ceo + eraName = eraToString sbe + options = def{cardanoNodeEra = AnyShelleyBasedEra sbe, cardanoEnableRpc = RpcEnabled} + + TestnetRuntime + { testnetMagic + , configurationFile + , testnetNodes = node0@TestnetNode{nodeSprocket} : _ + } <- + createAndRunTestnet options def conf + + execConfig <- mkExecConfig tempAbsPath' nodeSprocket testnetMagic + epochStateView <- getEpochStateView configurationFile (nodeSocketPath node0) + pparams <- unLedgerProtocolParameters <$> getProtocolParams epochStateView ceo + utxos <- findAllUtxos epochStateView sbe + H.noteShowPretty_ utxos + rpcSocket <- H.note . unFile $ nodeRpcSocketPath node0 + + ---------- + -- Get tip + ---------- + QueryTipLocalStateOutput{localStateChainTip} <- + H.noteShowM $ execCliStdoutToJson execConfig [eraName, "query", "tip"] + (slot, blockHash, blockNo) <- case localStateChainTip of + ChainTipAtGenesis -> H.failure -- impossible + ChainTip (SlotNo slot) (HeaderHash hash) (BlockNo blockNo) -> pure (slot, SBS.fromShort hash, blockNo) + + -------------- + -- RPC queries + -------------- + let rpcServer = Rpc.ServerUnix rpcSocket + (pparamsResponse, utxosResponse) <- H.noteShowM . H.evalIO . Rpc.withConnection def rpcServer $ \conn -> do + pparams' <- do + let req = Rpc.defMessage + Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf U5c.QueryService "readParams")) req + + utxos' <- do + let req = Rpc.defMessage + Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf U5c.QueryService "readUtxos")) req + pure (pparams', utxos') + + --------------------------- + -- Test readParams response + --------------------------- + pparamsResponse ^. U5c.ledgerTip . U5c.slot === slot + pparamsResponse ^. U5c.ledgerTip . U5c.hash === blockHash + pparamsResponse ^. U5c.ledgerTip . U5c.height === blockNo + pparamsResponse ^. U5c.ledgerTip . U5c.timestamp === 0 -- not possible to implement at this moment + + -- https://docs.cardano.org/about-cardano/explore-more/parameter-guide + let chainParams = pparamsResponse ^. U5c.values . U5c.cardano + babbageEraOnwardsConstraints (convert ceo) $ do + pparams ^. L.ppCoinsPerUTxOByteL . to L.unCoinPerByte . to L.unCoin + ===^ chainParams ^. U5c.coinsPerUtxoByte . to utxoRpcBigIntToInteger + pparams ^. L.ppMaxTxSizeL === chainParams ^. U5c.maxTxSize . to fromIntegral + pparams ^. L.ppMinFeeBL ===^ chainParams ^. U5c.minFeeCoefficient . to (fmap L.Coin . utxoRpcBigIntToInteger) + pparams ^. L.ppMinFeeAL ===^ chainParams ^. U5c.minFeeConstant . to (fmap L.Coin . utxoRpcBigIntToInteger) + pparams ^. L.ppMaxBBSizeL === chainParams ^. U5c.maxBlockBodySize . to fromIntegral + pparams ^. L.ppMaxBHSizeL === chainParams ^. U5c.maxBlockHeaderSize . to fromIntegral + pparams ^. L.ppKeyDepositL ===^ chainParams ^. U5c.stakeKeyDeposit . to (fmap L.Coin . utxoRpcBigIntToInteger) + pparams ^. L.ppPoolDepositL ===^ chainParams ^. U5c.poolDeposit . to (fmap L.Coin . utxoRpcBigIntToInteger) + pparams ^. L.ppEMaxL . to L.unEpochInterval === chainParams ^. U5c.poolRetirementEpochBound . to fromIntegral + pparams ^. L.ppNOptL === chainParams ^. U5c.desiredNumberOfPools . to fromIntegral + pparams ^. L.ppA0L . to L.unboundRational === chainParams ^. U5c.poolInfluence . to inject + pparams ^. L.ppTauL . to L.unboundRational === chainParams ^. U5c.treasuryExpansion . to inject + pparams ^. L.ppRhoL . to L.unboundRational === chainParams ^. U5c.monetaryExpansion . to inject + pparams ^. L.ppMinPoolCostL ===^ chainParams ^. U5c.minPoolCost . to (fmap L.Coin . utxoRpcBigIntToInteger) + ( pparams ^. L.ppProtocolVersionL . to L.pvMajor . to L.getVersion + , pparams ^. L.ppProtocolVersionL . to L.pvMinor + ) + === ( chainParams ^. U5c.protocolVersion . U5c.major + , chainParams ^. U5c.protocolVersion . U5c.minor . to fromIntegral + ) + pparams ^. L.ppMaxValSizeL === chainParams ^. U5c.maxValueSize . to fromIntegral + pparams ^. L.ppCollateralPercentageL === chainParams ^. U5c.collateralPercentage . to fromIntegral + pparams ^. L.ppMaxCollateralInputsL === chainParams ^. U5c.maxCollateralInputs . to fromIntegral + let pparamsCostModels = L.getCostModelParams <$> pparams ^. L.ppCostModelsL . to L.costModelsValid + wrapInMaybe v = if v == mempty then Nothing else Just v + M.lookup L.PlutusV1 pparamsCostModels === chainParams ^. U5c.costModels . U5c.plutusV1 . U5c.values . to wrapInMaybe + M.lookup L.PlutusV2 pparamsCostModels === chainParams ^. U5c.costModels . U5c.plutusV2 . U5c.values . to wrapInMaybe + M.lookup L.PlutusV3 pparamsCostModels === chainParams ^. U5c.costModels . U5c.plutusV3 . U5c.values . to wrapInMaybe + M.lookup L.PlutusV4 pparamsCostModels === chainParams ^. U5c.costModels . U5c.plutusV4 . U5c.values . to wrapInMaybe + pparams ^. L.ppPricesL . to L.prSteps . to L.unboundRational === chainParams ^. U5c.prices . U5c.steps . to inject + pparams ^. L.ppPricesL . to L.prMem . to L.unboundRational === chainParams ^. U5c.prices . U5c.memory . to inject + pparams ^. L.ppMaxTxExUnitsL === chainParams ^. U5c.maxExecutionUnitsPerTransaction . to inject + pparams ^. L.ppMaxBlockExUnitsL === chainParams ^. U5c.maxExecutionUnitsPerBlock . to inject + pparams ^. L.ppMinFeeRefScriptCostPerByteL . to L.unboundRational + === chainParams ^. U5c.minFeeScriptRefCostPerByte . to inject + let poolVotingThresholds :: L.PoolVotingThresholds = + conwayEraOnwardsConstraints ceo $ + pparams ^. L.ppPoolVotingThresholdsL + ( L.unboundRational + <$> [ poolVotingThresholds ^. L.pvtMotionNoConfidenceL + , poolVotingThresholds ^. L.pvtCommitteeNormalL + , poolVotingThresholds ^. L.pvtCommitteeNoConfidenceL + , poolVotingThresholds ^. L.pvtHardForkInitiationL + , poolVotingThresholds ^. L.pvtPPSecurityGroupL + ] + ) + === chainParams ^. U5c.poolVotingThresholds . U5c.thresholds . to (map inject) + let drepVotingThresholds :: L.DRepVotingThresholds = + conwayEraOnwardsConstraints ceo $ + pparams ^. L.ppDRepVotingThresholdsL + ( L.unboundRational + <$> [ drepVotingThresholds ^. L.dvtMotionNoConfidenceL + , drepVotingThresholds ^. L.dvtCommitteeNormalL + , drepVotingThresholds ^. L.dvtCommitteeNoConfidenceL + , drepVotingThresholds ^. L.dvtUpdateToConstitutionL + , drepVotingThresholds ^. L.dvtHardForkInitiationL + , drepVotingThresholds ^. L.dvtPPNetworkGroupL + , drepVotingThresholds ^. L.dvtPPEconomicGroupL + , drepVotingThresholds ^. L.dvtPPTechnicalGroupL + , drepVotingThresholds ^. L.dvtPPGovGroupL + , drepVotingThresholds ^. L.dvtTreasuryWithdrawalL + ] + ) + === chainParams ^. U5c.drepVotingThresholds . U5c.thresholds . to (map inject) + pparams ^. L.ppCommitteeMinSizeL === chainParams ^. U5c.minCommitteeSize . to fromIntegral + pparams ^. L.ppCommitteeMaxTermLengthL . to L.unEpochInterval + === chainParams ^. U5c.committeeTermLimit . to fromIntegral + pparams ^. L.ppGovActionLifetimeL . to L.unEpochInterval + === chainParams ^. U5c.governanceActionValidityPeriod . to fromIntegral + pparams ^. L.ppGovActionDepositL ===^ chainParams ^. U5c.governanceActionDeposit . to (fmap L.Coin . utxoRpcBigIntToInteger) + pparams ^. L.ppDRepDepositL ===^ chainParams ^. U5c.drepDeposit . to (fmap L.Coin . utxoRpcBigIntToInteger) + pparams ^. L.ppDRepActivityL . to L.unEpochInterval === chainParams ^. U5c.drepInactivityPeriod . to fromIntegral + + -------------------------- + -- Test readUtxos response + -------------------------- + + utxoFromUtxoRpc <- H.leftFail $ utxosResponse ^. U5c.items . to (anyUtxoDataUtxoRpcToUtxo $ convert ceo) + utxos === utxoFromUtxoRpc + +(===^) :: (Eq a, Show a, H.MonadTest m) => a -> Either SomeException a -> m () +expected ===^ actual = do + v <- H.leftFail actual + expected === v + +infix 4 ===^ diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Transaction.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Transaction.hs new file mode 100644 index 00000000000..e15b697a408 --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Transaction.hs @@ -0,0 +1,160 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeOperators #-} + +module Cardano.Testnet.Test.Rpc.Transaction + ( hprop_rpc_transaction + ) +where + +import Cardano.Api +import qualified Cardano.Api.Ledger as L + +import Cardano.Rpc.Client (Proto) +import qualified Cardano.Rpc.Client as Rpc +import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Query as U5c hiding (cardano, items, tx) +import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Query as UtxoRpc +import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Submit as U5c +import qualified Cardano.Rpc.Proto.Api.UtxoRpc.Submit as UtxoRpc +import Cardano.Rpc.Server.Internal.UtxoRpc.Type +import Cardano.Testnet + +import Prelude + +import Control.Monad +import Control.Monad.Fix +import Data.Default.Class +import qualified Data.Text.Encoding as T +import GHC.Stack +import Lens.Micro + +import Testnet.Property.Util (integrationRetryWorkspace) +import Testnet.Types + +import Hedgehog +import qualified Hedgehog as H +import qualified Hedgehog.Extras.Test.Base as H +import qualified Hedgehog.Extras.Test.TestWatchdog as H + +import RIO (ByteString, threadDelay) + +-- | Run with: +-- @TASTY_PATTERN='/RPC Transaction Submit/' cabal test cardano-testnet-test@ +hprop_rpc_transaction :: Property +hprop_rpc_transaction = integrationRetryWorkspace 2 "rpc-tx" $ \tempAbsBasePath' -> H.runWithDefaultWatchdog_ $ do + conf <- mkConf tempAbsBasePath' + let (ceo, eraProxy) = + (conwayBasedEra, asType) :: era ~ ConwayEra => (ConwayEraOnwards era, AsType era) + sbe = convert ceo + options = def{cardanoNodeEra = AnyShelleyBasedEra sbe, cardanoEnableRpc = RpcEnabled} + addrInEra = AsAddressInEra eraProxy + + TestnetRuntime + { testnetNodes = node0 : _ + , wallets = wallet0@(PaymentKeyInfo _ addrTxt0) : (PaymentKeyInfo _ addrTxt1) : _ + } <- + createAndRunTestnet options def conf + + rpcSocket <- H.note . unFile $ nodeRpcSocketPath node0 + + -- prepare tx inputs and output address + H.noteShow_ addrTxt0 + addr0 <- H.nothingFail $ deserialiseAddress addrInEra addrTxt0 + + H.noteShow_ addrTxt1 + addr1 <- H.nothingFail $ deserialiseAddress addrInEra addrTxt1 + + -- read key witnesses + wit0 :: ShelleyWitnessSigningKey <- + H.leftFailM . H.evalIO $ + readFileTextEnvelopeAnyOf + [FromSomeType asType WitnessGenesisUTxOKey] + (signingKey $ paymentKeyInfoPair wallet0) + + -------------- + -- RPC queries + -------------- + let rpcServer = Rpc.ServerUnix rpcSocket + (pparamsResponse, utxosResponse) <- H.noteShowM . H.evalIO . Rpc.withConnection def rpcServer $ \conn -> do + pparams' <- do + let req = def + Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "readParams")) req + + utxos' <- do + let req = def -- & # U5c.keys .~ [T.encodeUtf8 addrTxt0] + Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "readUtxos")) req + pure (pparams', utxos') + + pparams <- H.leftFail $ utxoRpcPParamsToProtocolParams (convert ceo) $ pparamsResponse ^. U5c.values . U5c.cardano + + txOut0 : _ <- H.noteShowM . flip filterM (utxosResponse ^. U5c.items) $ \utxo -> do + utxoAddress <- deserialiseAddressBs addrInEra $ utxo ^. U5c.cardano . U5c.address + pure $ addr0 == utxoAddress + txIn0 <- txoRefToTxIn $ txOut0 ^. U5c.txoRef + + outputCoin <- H.leftFail $ txOut0 ^. U5c.cardano . U5c.coin . to utxoRpcBigIntToInteger + let amount = 200_000_000 + fee = 500 + change = outputCoin - amount - fee + txOut = TxOut addr1 (lovelaceToTxOutValue sbe $ L.Coin amount) TxOutDatumNone ReferenceScriptNone + changeTxOut = TxOut addr0 (lovelaceToTxOutValue sbe $ L.Coin change) TxOutDatumNone ReferenceScriptNone + content = + defaultTxBodyContent sbe + & setTxIns [(txIn0, pure $ KeyWitness KeyWitnessForSpending)] + & setTxFee (TxFeeExplicit sbe (L.Coin fee)) + & setTxOuts [txOut, changeTxOut] + & setTxProtocolParams (pure . pure $ LedgerProtocolParameters pparams) + + txBody <- H.leftFail $ createTransactionBody sbe content + + let signedTx = signShelleyTransaction sbe txBody [wit0] + txId' <- H.noteShow . getTxId $ getTxBody signedTx + + H.noteShowPretty_ utxosResponse + + (utxos, submitResponse) <- H.noteShowM . H.evalIO . Rpc.withConnection def rpcServer $ \conn -> do + submitResponse <- + Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.SubmitService "submitTx")) $ + def & U5c.tx .~ (def & U5c.raw .~ serialiseToCBOR signedTx) + + fix $ \loop -> do + resp <- Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "readParams")) def + + let previousBlockNo = pparamsResponse ^. U5c.ledgerTip . U5c.height + currentBlockNo = resp ^. U5c.ledgerTip . U5c.height + -- wait for 2 blocks + when (previousBlockNo + 1 >= currentBlockNo) $ do + threadDelay 500_000 + loop + + -- TODO use searchUtxos when available + utxos <- + Rpc.nonStreaming conn (Rpc.rpc @(Rpc.Protobuf UtxoRpc.QueryService "readUtxos")) def + pure (utxos, submitResponse) + + submittedTxId <- H.leftFail . deserialiseFromRawBytes AsTxId $ submitResponse ^. U5c.ref + + H.note_ "Ensure that submitted transaction ID is in the submitted transactions list" + txId' === submittedTxId + + H.note_ $ "Ensure that there are 2 UTXOs in the address " <> show addrTxt1 + utxosForAddress <- H.noteShowM . flip filterM (utxos ^. U5c.items) $ \utxo -> do + utxoAddress <- deserialiseAddressBs addrInEra $ utxo ^. U5c.cardano . U5c.address + pure $ addr1 == utxoAddress + 2 === length utxosForAddress + + let outputsAmounts = map (^. U5c.cardano . U5c.coin) utxosForAddress + H.note_ $ "Ensure that the output sent is one of the utxos for the address " <> show addrTxt1 + H.assertWith outputsAmounts $ elem (inject amount) + +txoRefToTxIn :: (HasCallStack, MonadTest m) => Proto UtxoRpc.TxoRef -> m TxIn +txoRefToTxIn r = withFrozenCallStack $ do + txId' <- H.leftFail $ deserialiseFromRawBytes AsTxId $ r ^. U5c.hash + pure $ TxIn txId' (TxIx . fromIntegral $ r ^. U5c.index) + +deserialiseAddressBs :: (MonadTest m, SerialiseAddress c) => AsType c -> ByteString -> m c +deserialiseAddressBs addrInEra = H.nothingFail . deserialiseAddress addrInEra <=< H.leftFail . T.decodeUtf8' diff --git a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs index 16ef12987ca..90080114c26 100644 --- a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs +++ b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs @@ -29,6 +29,8 @@ import qualified Cardano.Testnet.Test.Gov.TreasuryDonation as Gov import qualified Cardano.Testnet.Test.Gov.TreasuryWithdrawal as Gov import qualified Cardano.Testnet.Test.MainnetParams import qualified Cardano.Testnet.Test.Node.Shutdown +import qualified Cardano.Testnet.Test.Rpc.Query +import qualified Cardano.Testnet.Test.Rpc.Transaction import qualified Cardano.Testnet.Test.RunTestnet import qualified Cardano.Testnet.Test.SanityCheck import qualified Cardano.Testnet.Test.SanityCheck as LedgerEvents @@ -135,6 +137,10 @@ tests = do , T.testGroup "SubmitApi" [ ignoreOnMacAndWindows "transaction" Cardano.Testnet.Test.SubmitApi.Transaction.hprop_transaction ] + , T.testGroup "RPC" + [ ignoreOnWindows "RPC Query Protocol Params" Cardano.Testnet.Test.Rpc.Query.hprop_rpc_query_pparams + , ignoreOnWindows "RPC Transaction Submit" Cardano.Testnet.Test.Rpc.Transaction.hprop_rpc_transaction + ] ] main :: IO () From 54c0d9efb7a6dbedb3453219fc4bbd6b7353c946 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Fri, 20 Feb 2026 15:43:18 +0100 Subject: [PATCH 7/8] Add cardano-testnet changelog entry --- .../20260220_120000_mgalazyn_add_grpc_interface.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 cardano-testnet/changelog.d/20260220_120000_mgalazyn_add_grpc_interface.md diff --git a/cardano-testnet/changelog.d/20260220_120000_mgalazyn_add_grpc_interface.md b/cardano-testnet/changelog.d/20260220_120000_mgalazyn_add_grpc_interface.md new file mode 100644 index 00000000000..052ea623439 --- /dev/null +++ b/cardano-testnet/changelog.d/20260220_120000_mgalazyn_add_grpc_interface.md @@ -0,0 +1,10 @@ +### Added + +- Added `--enable-grpc` flag to `cardano-testnet` to enable the gRPC interface (via `cardano-rpc`) when starting a testnet. +- Added `cardanoEnableRpc` field to `CardanoTestnetOptions` (default `RpcDisabled`). +- Added `nodeRpcSocketPath` helper to `Testnet.Types` for deriving the gRPC socket path from a node's socket path. +- Renamed `cardano-testnet` CLI flag `--nodeLoggingFormat` to `--node-logging-format`. + +### Tests + +- Added integration tests for the gRPC interface: `hprop_rpc_query_pparams` verifies protocol parameters and UTxO queries over gRPC, and `hprop_rpc_transaction` verifies transaction submission over gRPC and confirms the transaction lands on-chain. From edc6cd220fc52b941dfae9165f8f5b154132ffdb Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Mon, 16 Mar 2026 22:59:50 +0100 Subject: [PATCH 8/8] Add chainpoint check to gRPC test --- .../Cardano/Testnet/Test/Rpc/Query.hs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs index e5625952c66..9a59fac8695 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Rpc/Query.hs @@ -29,15 +29,19 @@ import Cardano.Testnet import Prelude import Control.Exception +import Control.Monad import qualified Data.ByteString.Short as SBS import Data.Default.Class import qualified Data.Map.Strict as M +import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) +import Data.Word (Word64) import Lens.Micro import Testnet.Components.Query import Testnet.Process.Run import Testnet.Property.Util (integrationRetryWorkspace) import Testnet.Start.Types +import Testnet.Types (nodeConnectionInfo) import Hedgehog import qualified Hedgehog as H @@ -56,7 +60,7 @@ hprop_rpc_query_pparams = integrationRetryWorkspace 2 "rpc-query-pparams" $ \tem eraName = eraToString sbe options = def{cardanoNodeEra = AnyShelleyBasedEra sbe, cardanoEnableRpc = RpcEnabled} - TestnetRuntime + tr@TestnetRuntime { testnetMagic , configurationFile , testnetNodes = node0@TestnetNode{nodeSprocket} : _ @@ -79,6 +83,20 @@ hprop_rpc_query_pparams = integrationRetryWorkspace 2 "rpc-query-pparams" $ \tem ChainTipAtGenesis -> H.failure -- impossible ChainTip (SlotNo slot) (HeaderHash hash) (BlockNo blockNo) -> pure (slot, SBS.fromShort hash, blockNo) + ----------------------------------- + -- Compute expected tip timestamp + ----------------------------------- + connectionInfo <- nodeConnectionInfo tr 0 + (systemStart, eraHistory) <- + (H.leftFail <=< H.leftFailM) . H.evalIO $ + executeLocalStateQueryExpr connectionInfo VolatileTip $ do + ss <- querySystemStart + eh <- queryEraHistory + pure $ (,) <$> ss <*> eh + expectedTimestamp :: Word64 <- H.leftFail $ do + utcTime <- slotToUTCTime systemStart eraHistory (SlotNo slot) + pure (round $ utcTimeToPOSIXSeconds utcTime * 1000) + -------------- -- RPC queries -------------- @@ -99,7 +117,7 @@ hprop_rpc_query_pparams = integrationRetryWorkspace 2 "rpc-query-pparams" $ \tem pparamsResponse ^. U5c.ledgerTip . U5c.slot === slot pparamsResponse ^. U5c.ledgerTip . U5c.hash === blockHash pparamsResponse ^. U5c.ledgerTip . U5c.height === blockNo - pparamsResponse ^. U5c.ledgerTip . U5c.timestamp === 0 -- not possible to implement at this moment + pparamsResponse ^. U5c.ledgerTip . U5c.timestamp === expectedTimestamp -- https://docs.cardano.org/about-cardano/explore-more/parameter-guide let chainParams = pparamsResponse ^. U5c.values . U5c.cardano