diff --git a/bench/tx-generator/CHANGELOG.md b/bench/tx-generator/CHANGELOG.md index 869512bad16..f3594f28b16 100644 --- a/bench/tx-generator/CHANGELOG.md +++ b/bench/tx-generator/CHANGELOG.md @@ -1,5 +1,29 @@ # ChangeLog +## 2.17 -- Jun 2026 + +* **New remote submission endpoint** — send transactions to a remote endpoint + instead of the local socket / Node-to-Node protocols, behind a generic, + backend-agnostic transport interface. + * Turn it on with the optional `submissionEndpointProtocol` and + `submissionEndpointURI` keys, which must be set together, e.g. + `"submissionEndpointProtocol": "Ogmios", "submissionEndpointURI": "ws://127.0.0.1:1337"`. + * The only endpoint type currently supported is `Ogmios`: each transaction is + sent over a WebSocket as a JSON-RPC 2.0 `submitTransaction` call. + * It reroutes **every** phase, but only tx submission: genesis fund import, UTxO splitting, and benchmarking. + * It is a **functional transport, not a benchmark**: no TPS pacing, no metrics. + * So it requires `debugMode: true`. The compiler rejects an endpoint config on its own. + * **A rejected transaction fails the whole run** (the process exits non-zero): + * Setup phases stop at the first rejection. + * The benchmark phase finishes the stream, then fails if anything was rejected. + * Rejections are traced through the normal pipeline, including the detail the endpoint reports. + +* **Fix: clean exit for non-benchmark runs.** Scripts that never start the + Node-to-Node benchmark machinery now exit cleanly instead of crashing at + shutdown with "AsyncBenchmarkControl absent". + * Affects `debugMode: true` runs and low-level `json` scripts with no + `Benchmark` submit phase. + ## 2.16 -- Apr 2026 * Added a `--testnet-config-dir` flag to `tx-generator json_highlevel` that auto-discovers connection settings config (socket path, signing key, node config, target nodes) from a `cardano-testnet` output directory. diff --git a/bench/tx-generator/README.md b/bench/tx-generator/README.md index 319ceee0b01..e3c9bf9038b 100644 --- a/bench/tx-generator/README.md +++ b/bench/tx-generator/README.md @@ -58,6 +58,8 @@ cabal run tx-generator -- json_highlevel config.json \ | `targetNodes` | List of nodes to submit transactions to (Node-to-Node protocol) | Yes | | `nodeConfigFile` | Path to node configuration file | Yes | | `sigKey` | Path to signing key with sufficient funds (genesis key) | Yes | +| `submissionEndpointProtocol` | Optional submission endpoint kind (`Ogmios`); set together with `submissionEndpointURI` | No | +| `submissionEndpointURI` | Optional submission endpoint URI (e.g. `ws://host:port`); set together with `submissionEndpointProtocol` | No | ### Transaction Settings @@ -111,6 +113,30 @@ The high-level JSON configuration is automatically compiled into a multi-phase s **Important**: All phases use the **local node socket** for setup (phases 1-3), and only phase 4 connects to **target nodes** via Node-to-Node protocol for actual benchmarking. +### Submitting through a remote endpoint + +Setting a submission endpoint (`submissionEndpointProtocol` together with `submissionEndpointURI`) reroutes the submission of **every** phase — genesis fund import, UTxO splitting, and the final phase — through that endpoint instead of the local socket / Node-to-Node protocols. + +This is a **functional submission transport, not a benchmarking one**: + +- Transactions are submitted strictly one per round trip, so throughput is bounded by the latency to the endpoint; `tps` pacing and `targetNodes` are not used. +- No benchmark metrics or submission summary are produced; per-transaction rejections (including the failure detail reported by the endpoint) and a final sent/failed count go to the trace output. +- Rejected transactions make the run fail: setup phases (genesis import, splitting) abort at the first rejection, and the final phase exits non-zero if any transaction was rejected. Exit codes can be trusted in scripts. +- Because no real benchmark runs, the config compiler requires `debugMode: true` alongside the endpoint and rejects the config otherwise. + +Note that the **local node socket and config file are still required**: protocol-parameter and era queries as well as protocol startup keep using the local node; only transaction submission goes through the endpoint. + +#### Ogmios + +The only endpoint type currently supported is `Ogmios`. Set: + +```json +"submissionEndpointProtocol": "Ogmios", +"submissionEndpointURI": "ws://127.0.0.1:1337" +``` + +Transactions are sent to the [Ogmios](https://ogmios.dev) endpoint as JSON-RPC 2.0 `submitTransaction` calls over a WebSocket (only plain `ws://` is supported). + ## Low-Level JSON Script (Advanced) For fine-grained control, use low-level JSON scripts. Example in `test/script.json`: diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs index 719819250b1..bf0d95aa2a5 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs @@ -82,8 +82,9 @@ importGenesisFunds = do wallet <- newWallet "genesis_wallet" era <- askNixOption _nix_era txParams <- askNixOption txGenTxParams + setupMode <- getSetupSubmitMode cmd1 (ReadSigningKey keyNameGenesisInputFund) _nix_sigKey - emit $ Submit era LocalSocket txParams $ SecureGenesis wallet keyNameGenesisInputFund keyNameTxGenFunds + emit $ Submit era setupMode txParams $ SecureGenesis wallet keyNameGenesisInputFund keyNameTxGenFunds delay logMsg "Importing Genesis Fund. Done." return wallet @@ -102,7 +103,8 @@ addCollaterals src = do (PayToAddr keyNameCollaterals collateralWallet) (PayToAddr keyNameTxGenFunds src) [ safeCollateral ] - emit $ Submit era LocalSocket txParams generator + setupMode <- getSetupSubmitMode + emit $ Submit era setupMode txParams generator logMsg "Create collaterals. Done." return $ Just collateralWallet @@ -129,7 +131,8 @@ splittingPhase srcWallet = do let generator = case split of SplitWithChange lovelace count -> Split src payMode (PayToAddr keyNameTxGenFunds src) $ replicate count lovelace FullSplits txCount -> Take txCount $ Cycle $ SplitN src payMode maxOutputsPerTx - emit $ Submit era LocalSocket txParams generator + setupMode <- getSetupSubmitMode + emit $ Submit era setupMode txParams generator delay logMsg "Splitting step: Done" @@ -195,16 +198,27 @@ benchmarkingPhase wallet collateralWallet = do inputs <- askNixOption _nix_inputs_per_tx outputs <- askNixOption _nix_outputs_per_tx txParams <- askNixOption txGenTxParams + endpoint <- resolveSubmissionEndpoint doneWallet <- newWallet "done_wallet" + -- A submission endpoint is a functional submission transport, not a + -- benchmarking one: it ignores tps and targetNodes and produces no + -- submission metrics, so a config that asks for a real benchmark must fail + -- fast here instead of running unpaced and unmeasured. + submitMode <- case (endpoint, debugMode) of + (Just (eType, uri), True) -> pure $ SubmitToEndpoint eType uri + (Just _, False) -> throwCompileError $ SomeCompilerError + "submissionEndpointURI is a functional submission transport: it ignores \ + \tps and targetNodes and produces no benchmark metrics. Set debugMode: \ + \true to acknowledge this, or remove it to run a real benchmark." + (Nothing, True) -> pure LocalSocket + (Nothing, False) -> pure $ Benchmark targetNodes tps txCount let payMode = PayToAddr keyNameBenchmarkDone doneWallet - submitMode = if debugMode - then LocalSocket - else Benchmark targetNodes tps txCount generator = Take txCount $ Cycle $ NtoM wallet payMode inputs outputs (Just $ txParamAddTxSize txParams) collateralWallet emit $ Submit era submitMode txParams generator - unless debugMode $ do - emit WaitBenchmark + case submitMode of + Benchmark {} -> emit WaitBenchmark + _ -> return () return doneWallet data Fees = Fees { @@ -246,6 +260,24 @@ cmd1 cmd arg = emit . cmd =<< askNixOption arg askNixOption :: (NixServiceOptions -> v) -> Compiler v askNixOption = asks +getSetupSubmitMode :: Compiler SubmitMode +getSetupSubmitMode = + maybe LocalSocket (uncurry SubmitToEndpoint) <$> resolveSubmissionEndpoint + +-- | Resolve the configured submission endpoint, requiring its type and URI to +-- be set together (or both omitted). The URI itself is already parsed: decoding +-- the config only accepts a well-formed absolute URI. +resolveSubmissionEndpoint :: Compiler (Maybe (SubmissionEndpointProtocol, EndpointUri)) +resolveSubmissionEndpoint = do + mType <- askNixOption _nix_submissionEndpointProtocol + mUri <- askNixOption _nix_submissionEndpointURI + case (mType, mUri) of + (Nothing, Nothing) -> pure Nothing + (Just t, Just u) -> pure $ Just (t, u) + _ -> throwCompileError $ SomeCompilerError + "submissionEndpointProtocol and submissionEndpointURI must be set together \ + \(or both omitted)." + delay :: Compiler () delay = cmd1 Delay _nix_init_cooldown diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script.hs index 9b7537bc250..21cdff42fd3 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script.hs @@ -14,10 +14,9 @@ import Cardano.Benchmarking.LogTypes import Cardano.Benchmarking.Script.Action import Cardano.Benchmarking.Script.Aeson (parseScriptFileAeson) import Cardano.Benchmarking.Script.Core (setProtocolParameters) -import qualified Cardano.Benchmarking.Script.Env as Env (ActionM, Env (..), Error (TxGenError), +import qualified Cardano.Benchmarking.Script.Env as Env (ActionM, Env (..), Error, getEnvThreads, runActionMEnv, traceError) import Cardano.Benchmarking.Script.Types -import qualified Cardano.TxGenerator.Types as Types (TxGenError (..)) import Prelude @@ -26,46 +25,38 @@ import Control.Concurrent.STM.TVar as STM (readTVar) import Control.Monad import Control.Monad.IO.Class import Control.Monad.STM as STM (atomically) -import Control.Monad.Trans.Except as Except (throwE) -import qualified Data.List as List (unwords) import System.Mem (performGC) type Script = [Action] -runScript :: Env.Env -> Script -> EnvConsts -> IO (Either Env.Error (), AsyncBenchmarkControl) +-- | Run a benchmarking script. The second component of the result carries +-- the 'AsyncBenchmarkControl' of the submission threads when the script +-- started any: submit modes other than 'Benchmark' (e.g. 'LocalSocket', +-- 'Ogmios') never create one, in which case it is 'Nothing'. +runScript :: Env.Env -> Script -> EnvConsts -> IO (Either Env.Error (), Maybe AsyncBenchmarkControl) runScript env script constants@EnvConsts { .. } = do result <- go performGC threadDelay $ 150 * 1_000 return result where - go :: IO (Either Env.Error (), AsyncBenchmarkControl) + go :: IO (Either Env.Error (), Maybe AsyncBenchmarkControl) go = Env.runActionMEnv env execScript constants >>= \case - (Right abc, env', ()) -> do + (Right abcMaybe, env', ()) -> do cleanup env' shutDownLogging - pure (Right (), abc) + pure (Right (), abcMaybe) (Left err, env', ()) -> do cleanup env' (Env.traceError (show err) >> shutDownLogging) abcMaybe <- STM.atomically $ STM.readTVar envThreads - case abcMaybe of - Just abc -> pure (Left err, abc) - Nothing -> error $ List.unwords - [ "Cardano.Benchmarking.Script.runScript:" - , "AsyncBenchmarkControl uninitialized" ] + pure (Left err, abcMaybe) where cleanup :: Env.Env -> Env.ActionM () -> IO () cleanup env' acts = void $ Env.runActionMEnv env' acts constants - execScript :: Env.ActionM AsyncBenchmarkControl + execScript :: Env.ActionM (Maybe AsyncBenchmarkControl) execScript = do setProtocolParameters QueryLocalNode forM_ script action - abcMaybe <- Env.getEnvThreads - case abcMaybe of - Nothing -> throwE $ Env.TxGenError $ Types.TxGenError $ - List.unwords - [ "Cardano.Benchmarking.Script.runScript:" - , "AsyncBenchmarkControl absent from map in execScript" ] - Just abc -> pure abc + Env.getEnvThreads shutDownLogging :: Env.ActionM () shutDownLogging = do diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs index 1b345952511..90b1f357c28 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs @@ -30,6 +30,8 @@ import Cardano.Benchmarking.LogTypes as Core (AsyncBenchmarkControl (. import Cardano.Benchmarking.OuroborosImports as Core (LocalSubmitTx, SigningKeyFile, makeLocalConnectInfo, protocolToCodecConfig) import Cardano.Benchmarking.Script.Aeson (prettyPrintOrdered, readProtocolParametersFile) +import qualified Cardano.Benchmarking.Script.Ogmios as OgmiosBackend +import qualified Cardano.Benchmarking.Script.Submission as Submission import Cardano.Benchmarking.Script.Env hiding (Error (TxGenError)) import qualified Cardano.Benchmarking.Script.Env as Env (Error (TxGenError)) import Cardano.Benchmarking.Script.Types @@ -244,6 +246,11 @@ submitInEra submitMode generator txParams era = do NodeToNode _ -> error "NodeToNode deprecated: ToDo: remove" Benchmark nodes tpsRate txCount -> benchmarkTxStream txStream nodes tpsRate txCount era LocalSocket -> submitAll (void . localSubmitTx . Utils.mkTxInModeCardano) txStream + SubmitToEndpoint endpointType (EndpointUri uri) -> case endpointType of + Ogmios -> Submission.runSubmitTransport + (Submission.onRejectionFor generator) + (OgmiosBackend.withOgmiosTransport uri) + txStream DumpToFile filePath -> liftIO $ Streaming.writeFile filePath $ Streaming.map showTx txStream DiscardTX -> liftIO $ Streaming.mapM_ forceTx txStream where diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs new file mode 100644 index 00000000000..2459aeb137c --- /dev/null +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -0,0 +1,261 @@ +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +{-| +Module : Cardano.Benchmarking.Script.Ogmios +Description : Ogmios backend for the transaction submission transport. + +An WebSocket backend for the generic +'Cardano.Benchmarking.Script.Submission.SubmitTransport': transactions are +submitted as JSON-RPC 2.0 @submitTransaction@ calls. + +== What this is (and is not) + +This is a __functional submission transport, not a benchmarking one__: + +* one transaction per round trip, over a single WebSocket connection; +* TPS pacing is ignored; +* no submission metrics — only a sent/failed count, traced at debug level. + +__Using a submission endpoint requires @debugMode: true@__ — the high-level +config compiler rejects a @submissionEndpointURI@ config that does not also +set it. + +== Throughput + +Throughput is bounded by the round-trip time to the endpoint: at most one +request is in flight at a time. + +Ogmios /can/ pipeline many requests per connection, correlated by the +JSON-RPC @id@. A paced sender/receiver pair with an in-flight window is the +natural next step, should this transport ever need to carry benchmark-grade +load. Until then, __do not draw throughput conclusions from runs submitted +this way__. + +== What still uses the local node + +Only transaction /submission/ goes through Ogmios. Everything else talks to +the local node directly: + +* protocol-parameter and era queries; +* protocol startup. + +So tx-generator still needs local node access (socket + config file) even +when submitting to a remote endpoint. + +== Rejected transactions + +A rejected transaction __fails the run__: the process exits non-zero, so +exit codes can be trusted in scripts. /How/ it fails depends on the phase: + +* __Setup__ (genesis import, splitting) — chained transactions, so it aborts + at the first rejection; everything after it would be doomed anyway. +* __Benchmark__ — independent transactions, so the whole stream is submitted, + then the action fails afterwards if anything was rejected. +-} +module Cardano.Benchmarking.Script.Ogmios + ( parseOgmiosUrl + , withOgmiosTransport + ) where + +import Cardano.Api (IsShelleyBasedEra, Tx, serialiseToCBOR) + +import Cardano.Benchmarking.Script.Submission (SubmitTransport (..)) +import Cardano.TxGenerator.Types (TxGenError (..)) + +import Prelude + +import Control.Exception (Exception, Handler (..), IOException, catches, throwIO) +import Control.Monad (unless, when) +import Data.Aeson (Value (..), object, (.:), (.:?), (.=)) +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Types as Aeson +import qualified Data.ByteString.Base16 as Base16 +import qualified Data.ByteString.Lazy as LBS +import Data.Either.Extra (maybeToEither) +import Data.IORef (IORef, atomicModifyIORef', newIORef) +import Data.Maybe (fromMaybe) +import Data.Text (Text) +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text +import Network.URI (URI, uriAuthority, uriPath, uriPort, uriRegName, uriScheme, + uriToString) +import qualified Network.WebSockets as WS +import Prettyprinter (Pretty (..), parens, (<+>)) +import System.Timeout (timeout) +import Text.Read (readMaybe) + +-- | The port Ogmios listens on by default. +defaultOgmiosPort :: Int +defaultOgmiosPort = 1337 + +-- | Per-request round-trip (send + response) timeout in microseconds. +-- Generous, because the node may hold a submission back while its mempool +-- is saturated. +responseTimeout :: Int +responseTimeout = 90_000_000 + +-- | A transaction rejection reported by Ogmios, carried as the error type of +-- the 'SubmitTransport' and rendered for tracing via 'Pretty'. Deliberately +-- not exported: the submission loop only needs to render it, so no other +-- module should depend on its shape. +data OgmiosRejection = OgmiosRejection + !Int -- ^ JSON-RPC 2.0 error code + !Text -- ^ human-readable error message + !Value -- ^ structured details from the response's optional @data@ field; + -- 'Null' when absent + +instance Pretty OgmiosRejection where + pretty (OgmiosRejection code msg dat) = + "Ogmios submit failed:" <+> pretty msg <+> parens ("code" <+> pretty code) + <> case dat of + Null -> mempty + _ -> ", details:" <+> pretty (Text.decodeUtf8 (LBS.toStrict (Aeson.encode dat))) + +-- | A protocol-level fault: no or late response, an unparseable response, or a +-- mismatched JSON-RPC id. The connection is out of sync and unusable, so this +-- aborts the whole run rather than counting as a transaction rejection. +newtype OgmiosProtocolError = OgmiosProtocolError String + deriving Show + +instance Exception OgmiosProtocolError + +-- | Open a WebSocket connection to an Ogmios endpoint and run an action with a +-- 'SubmitTransport' backed by it. Connection-level and protocol faults are +-- surfaced as a 'Left' 'TxGenError'. +withOgmiosTransport + :: forall era a. IsShelleyBasedEra era + => URI + -> (SubmitTransport era OgmiosRejection -> IO (Either TxGenError a)) + -> IO (Either TxGenError a) +withOgmiosTransport uri use = + case parseOgmiosUrl uri of + Left err -> return $ Left $ TxGenError err + Right (host, port, path) -> + WS.runClient host port path runWithConn + `catches` + [ Handler $ \(e :: WS.HandshakeException) -> connectionFailure e + , Handler $ \(e :: WS.ConnectionException) -> connectionFailure e + , Handler $ \(e :: IOException) -> connectionFailure e + , Handler $ \(OgmiosProtocolError m) -> return $ Left $ TxGenError $ "Ogmios: " ++ m + ] + where + runWithConn conn = do + -- a per-connection counter mirrors each request's JSON-RPC id, so a + -- response can be matched back to the request that produced it + reqIdRef <- newIORef 0 + use SubmitTransport { submitOne = ogmiosSubmitOne conn reqIdRef } + connectionFailure :: Show e => e -> IO (Either TxGenError a) + connectionFailure e = return $ Left $ TxGenError $ "Ogmios connection failure: " ++ show e + +-- | Submit a single transaction and await its response on the same connection. +-- A transaction rejection is returned as 'Left'; a protocol-level fault throws +-- 'OgmiosProtocolError' (caught by 'withOgmiosTransport'). +ogmiosSubmitOne + :: IsShelleyBasedEra era + => WS.Connection -> IORef Int -> Tx era -> IO (Either OgmiosRejection ()) +ogmiosSubmitOne conn reqIdRef tx = do + reqId <- atomicModifyIORef' reqIdRef $ \n -> (n + 1, n) + -- the send can stall too (a wedged peer with full TCP buffers), so it + -- shares the round-trip deadline with the receive + mResp <- timeout responseTimeout $ do + WS.sendTextData conn $ Aeson.encode (mkSubmitRequest tx reqId) + WS.receiveData conn + case mResp of + Nothing -> throwIO $ OgmiosProtocolError $ + "request " ++ show reqId ++ " did not complete within " + ++ show (responseTimeout `div` 1_000_000) ++ "s" + Just resp -> case parseOgmiosResponse resp of + Left parseErr -> throwIO $ OgmiosProtocolError $ "response parse error: " ++ parseErr + Right (respId, result) + -- a mismatched (or null) id means the connection is out of sync + | respId /= Just reqId -> throwIO $ OgmiosProtocolError $ + "response id mismatch: expected " ++ show reqId + ++ ", got " ++ show respId ++ " (" ++ describe result ++ ")" + | OgmiosError code errMsg errData <- result -> + return $ Left $ OgmiosRejection code errMsg errData + | otherwise -> return $ Right () + where + describe (OgmiosSuccess txId) = "success, tx " ++ Text.unpack txId + describe (OgmiosError _ msg _) = "error: " ++ Text.unpack msg + +-- | Extract WebSocket connection parameters @(host, port, path)@ from an +-- endpoint URI (already well-formed by construction, see +-- 'Cardano.TxGenerator.Setup.NixService.EndpointUri'), validating the +-- Ogmios-specific parts. +parseOgmiosUrl :: URI -> Either String (String, Int, String) +parseOgmiosUrl uri = do + -- WS.runClient speaks plaintext TCP only, so accepting wss:// (or any + -- other scheme) here would silently drop the security the URL asks for. + unless (uriScheme uri == "ws:") $ + Left $ "Unsupported scheme in Ogmios URL (only plain ws:// is supported): " ++ urlStr + auth <- maybeToEither ("No authority in Ogmios URL: " ++ urlStr) $ uriAuthority uri + when (null $ uriRegName auth) $ + Left $ "No host in Ogmios URL: " ++ urlStr + port <- case uriPort auth of + "" -> Right defaultOgmiosPort + ":" -> Right defaultOgmiosPort + ':':p -> parsePort p + p -> parsePort p + let path = case uriPath uri of + "" -> "/" + p -> p + return (uriRegName auth, port, path) + where + urlStr = uriToString id uri "" + parsePort p = case readMaybe p of + Just n | n >= 1 && n <= 65_535 -> Right n + _ -> Left $ "Invalid port in Ogmios URL: " ++ urlStr + +mkSubmitRequest :: IsShelleyBasedEra era => Tx era -> Int -> Value +mkSubmitRequest tx reqId = object + [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= ("submitTransaction" :: Text) + , "params" .= object + [ "transaction" .= object + [ "cbor" .= Text.decodeUtf8 (Base16.encode (serialiseToCBOR tx)) + ] + ] + , "id" .= reqId + ] + +-- | Outcome of a single @submitTransaction@ call, as decoded from the +-- JSON-RPC response. +data OgmiosResult + = OgmiosSuccess + Text -- ^ id of the accepted transaction + | OgmiosError + Int -- ^ JSON-RPC 2.0 error code + Text -- ^ human-readable error message + Value -- ^ structured details from the optional @data@ field; + -- 'Null' when absent + deriving (Eq, Show) + +-- | Parse a JSON-RPC 2.0 response to @submitTransaction@, returning the +-- mirrored request id alongside the result. +parseOgmiosResponse :: LBS.ByteString -> Either String (Maybe Int, OgmiosResult) +parseOgmiosResponse bs = + Aeson.eitherDecode bs >>= Aeson.parseEither parseResponse + where + parseResponse = Aeson.withObject "OgmiosResponse" $ \obj -> do + respId <- obj .:? "id" + mResult <- obj .:? "result" + result <- maybe (parseError obj) parseSuccess mResult + return (respId, result) + +parseSuccess :: Value -> Aeson.Parser OgmiosResult +parseSuccess = Aeson.withObject "result" $ \r -> do + txObj <- r .: "transaction" + txId <- Aeson.withObject "transaction" (.: "id") txObj + return $ OgmiosSuccess txId + +parseError :: Aeson.Object -> Aeson.Parser OgmiosResult +parseError obj = do + errVal <- obj .: "error" + Aeson.withObject "error" (\errObj -> + OgmiosError + <$> errObj .: "code" + <*> errObj .: "message" + <*> (fromMaybe Null <$> errObj .:? "data") + ) errVal diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Submission.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Submission.hs new file mode 100644 index 00000000000..a2ca726950c --- /dev/null +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Submission.hs @@ -0,0 +1,129 @@ +{-# LANGUAGE ScopedTypeVariables #-} + +{-| +Module : Cardano.Benchmarking.Script.Submission +Description : Backend-agnostic transaction submission transport. + +A 'SubmitTransport' is the minimal interface for submitting transactions to +some endpoint, one at a time. Concrete backends (e.g. +"Cardano.Benchmarking.Script.Ogmios") construct one; 'submitLoop' drives a +transaction stream through it, and 'runSubmitTransport' wires that into the +script monad — all independent of how submission actually happens on the wire. + +The backend's rejection type is kept abstract (the @e@ parameter): the loop +only ever needs to render a rejection for tracing, expressed as a 'Pretty' +constraint, so no transport-specific detail leaks into this module. +-} +module Cardano.Benchmarking.Script.Submission + ( SubmitTransport (..) + , OnRejection (..) + , onRejectionFor + , submitLoop + , runSubmitTransport + ) where + +import Cardano.Api (Tx) + +import Cardano.Benchmarking.LogTypes (BenchTracers (..), TraceBenchTxSubmit (..)) +import Cardano.Benchmarking.Script.Env (ActionM, getBenchTracers, liftTxGenError, + traceDebug) +import Cardano.Benchmarking.Script.Types (Generator (..)) +import Cardano.Benchmarking.Wallet (TxStream) +import Cardano.Logging (traceWith) +import Cardano.TxGenerator.Types (TxGenError (..)) + +import Prelude + +import Control.Monad (when) +import Data.Text (Text) +import qualified Data.Text as Text +import Prettyprinter (Pretty (..), defaultLayoutOptions, layoutPretty) +import Prettyprinter.Render.Text (renderStrict) + +import Streaming + +-- | A backend that can submit a single transaction and report, synchronously, +-- whether the endpoint accepted it or rejected it. The rejection type @e@ is +-- the backend's own; this module never inspects it, only renders it. +newtype SubmitTransport era e = SubmitTransport + { submitOne :: Tx era -> IO (Either e ()) } + +-- | How to proceed when the endpoint rejects a transaction. +data OnRejection + = AbortOnRejection -- ^ stop the stream at the first rejection + | ContinueOnRejection -- ^ submit the whole stream, then fail if anything was rejected + deriving (Eq, Show) + +-- | Setup-phase generators (genesis import, splitting) emit chains of +-- interdependent transactions: once one is rejected, everything after it is +-- doomed, so abort right away. Only a plain 'NtoM' stream (the benchmarking +-- phase) is known to consist of mutually independent transactions, so it is +-- submitted to the end and the final tally decides. +-- +-- 'Sequence' may mix chained and independent sub-generators, so it aborts +-- unconditionally. Either policy fails the run on any rejection (see +-- 'runSubmitTransport'); the policy only decides whether to keep submitting +-- after the first one, and aborting is always safe — merely conservative +-- for an all-independent sequence. +onRejectionFor :: Generator -> OnRejection +onRejectionFor generator = case generator of + NtoM {} -> ContinueOnRejection + Take _ g -> onRejectionFor g + Cycle g -> onRejectionFor g + _ -> AbortOnRejection + +-- | Drive a transaction stream through a transport, returning a @(sent, failed)@ +-- tally. Rejections are traced (rendered via 'Pretty') and counted; how the loop +-- reacts depends on the 'OnRejection' policy. +submitLoop + :: forall era e. Pretty e + => BenchTracers + -> OnRejection + -> SubmitTransport era e + -> TxStream IO era + -> IO (Either TxGenError (Int, Int)) +submitLoop tracers onRejection transport = go 0 0 + where + go :: Int -> Int -> TxStream IO era -> IO (Either TxGenError (Int, Int)) + go sent failed stream = do + step <- Streaming.inspect stream + case step of + Left () -> return $ Right (sent, failed) + Right (Left err :> _rest) -> return $ Left err + Right (Right tx :> rest) -> do + outcome <- submitOne transport tx + case outcome of + Right () -> go (sent + 1) failed rest + Left e -> do + let rendered = renderRejection e + traceWith (btTxSubmit_ tracers) $ TraceBenchTxSubError rendered + case onRejection of + AbortOnRejection -> + return $ Left $ TxGenError $ "transaction rejected: " ++ Text.unpack rendered + ContinueOnRejection -> go sent (failed + 1) rest + +renderRejection :: Pretty e => e -> Text +renderRejection = renderStrict . layoutPretty defaultLayoutOptions . pretty + +-- | Run a transaction stream through a transport within the script monad: open +-- the transport, drive the loop, and turn the outcome into the run's result — +-- a rejected transaction is a functional failure, so the action fails (and the +-- process exits non-zero) if anything was rejected. +runSubmitTransport + :: Pretty e + => OnRejection + -> ((SubmitTransport era e -> IO (Either TxGenError (Int, Int))) + -> IO (Either TxGenError (Int, Int))) + -> TxStream IO era + -> ActionM () +runSubmitTransport onRejection withTransport txStream = do + tracers <- getBenchTracers + result <- liftIO $ withTransport $ \transport -> + submitLoop tracers onRejection transport txStream + case result of + Left err -> liftTxGenError err + Right (sent, failed) -> do + traceDebug $ "submission done, " ++ show sent ++ " sent, " ++ show failed ++ " failed" + when (failed > 0) $ liftTxGenError $ TxGenError $ + show failed ++ " of " ++ show (sent + failed) + ++ " transactions were rejected" diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs index 08a2f63c8c8..dec3d4c4023 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs @@ -25,6 +25,7 @@ things one might do with the connexion. -} module Cardano.Benchmarking.Script.Types ( Action(..) + , EndpointUri(..) , Generator(Cycle, NtoM, OneOf, RoundRobin, SecureGenesis, Sequence, Split, SplitN, Take) , PayMode(PayToAddr, PayToScript) @@ -32,8 +33,9 @@ module Cardano.Benchmarking.Script.Types ( , ProtocolParametersSource(QueryLocalNode, UseLocalProtocolFile) , ScriptBudget(AutoScript, StaticScriptBudget) , ScriptSpec(..) + , SubmissionEndpointProtocol(..) , SubmitMode(Benchmark, DiscardTX, DumpToFile, LocalSocket, - NodeToNode) + NodeToNode, SubmitToEndpoint) , TargetNodes , TxList(..) ) where @@ -44,7 +46,8 @@ import qualified Cardano.Api.Ledger as L import Cardano.Benchmarking.OuroborosImports (SigningKeyFile) import Cardano.Node.Configuration.NodeAddress (NodeIPv4Address) import Cardano.TxGenerator.ProtocolParameters (ProtocolParameters) -import Cardano.TxGenerator.Setup.NixService (NodeDescription) +import Cardano.TxGenerator.Setup.NixService (EndpointUri (..), NodeDescription, + SubmissionEndpointProtocol (..)) import Cardano.TxGenerator.Types import Prelude @@ -185,6 +188,9 @@ data SubmitMode where DumpToFile :: !FilePath -> SubmitMode DiscardTX :: SubmitMode NodeToNode :: NonEmpty NodeIPv4Address -> SubmitMode --deprecated + -- | Submit through an external service at the given URI, with the + -- 'SubmissionEndpointProtocol' selecting which backend protocol to speak. + SubmitToEndpoint :: !SubmissionEndpointProtocol -> !EndpointUri -> SubmitMode deriving (Show, Eq) deriving instance Generic SubmitMode diff --git a/bench/tx-generator/src/Cardano/TxGenerator/Setup/NixService.hs b/bench/tx-generator/src/Cardano/TxGenerator/Setup/NixService.hs index 8b83528f49a..a8f2131b3fe 100644 --- a/bench/tx-generator/src/Cardano/TxGenerator/Setup/NixService.hs +++ b/bench/tx-generator/src/Cardano/TxGenerator/Setup/NixService.hs @@ -1,6 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} @@ -11,6 +12,8 @@ module Cardano.TxGenerator.Setup.NixService ( NixServiceOptions (..) , NodeDescription (..) + , SubmissionEndpointProtocol (..) + , EndpointUri (..) , defaultKeepaliveTimeout , getKeepaliveTimeout , getNodeAlias @@ -37,8 +40,10 @@ import Data.Foldable (find) import Data.Function (on) import Data.List.NonEmpty (NonEmpty (..)) import Data.Maybe (fromMaybe) +import qualified Data.Text as Text import qualified Data.Time.Clock as Clock (DiffTime, secondsToDiffTime) import GHC.Generics (Generic) +import Network.URI (URI, parseURI, uriToString) data NixServiceOptions = NixServiceOptions { @@ -59,10 +64,42 @@ data NixServiceOptions = NixServiceOptions { , _nix_sigKey :: SigningKeyFile In , _nix_localNodeSocketPath :: String , _nix_targetNodes :: NonEmpty NodeDescription + , _nix_submissionEndpointProtocol :: Maybe SubmissionEndpointProtocol + , _nix_submissionEndpointURI :: Maybe EndpointUri } deriving (Show, Eq) deriving instance Generic NixServiceOptions +-- | Which protocol to speak with the endpoint 'submissionEndpointURI' +-- addresses. Currently only Ogmios is supported; this is the extension +-- point for further submission backends. +data SubmissionEndpointProtocol + = Ogmios + deriving (Show, Eq, Generic) + +instance FromJSON SubmissionEndpointProtocol where + parseJSON = withText "SubmissionEndpointProtocol" $ \t -> case t of + "Ogmios" -> pure Ogmios + _ -> fail $ "unknown submissionEndpointProtocol: " ++ show t + +instance ToJSON SubmissionEndpointProtocol where + toJSON Ogmios = String "Ogmios" + +-- | A submission endpoint address, well-formed by construction: decoding +-- only accepts absolute URIs (a scheme is required, e.g. @ws://host:1337@). +-- Which schemes are meaningful is for each backend to decide. +newtype EndpointUri = EndpointUri URI + deriving (Show, Eq) + +instance FromJSON EndpointUri where + parseJSON = withText "EndpointUri" $ \t -> case parseURI (Text.unpack t) of + Just uri -> pure $ EndpointUri uri + Nothing -> fail $ + "invalid endpoint URI (must be absolute, e.g. ws://host:1337): " ++ show t + +instance ToJSON EndpointUri where + toJSON (EndpointUri uri) = String $ Text.pack $ uriToString id uri "" + -- only works on JSON Object types data NodeDescription = NodeDescription { diff --git a/bench/tx-generator/test-ogmios.sh b/bench/tx-generator/test-ogmios.sh new file mode 100755 index 00000000000..61508e9f948 --- /dev/null +++ b/bench/tx-generator/test-ogmios.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash +# +# Integration test: submit transactions via Ogmios. +# +# Usage: +# bash bench/tx-generator/test-ogmios.sh [ogmios-flake-ref] +# +# Binaries are resolved from the ogmios flake: +# ogmios ← packages.ogmios-exe (hsPkgs.ogmios.components.exes.ogmios) +# cardano-* ← inputs.cardano-node.packages.{cardano-node,cardano-cli,cardano-testnet} +# tx-generator ← locally-built (cabal) +# +# The ogmios ref MUST be fetched with submodules: ogmios pulls hjsonpointer, +# hjsonschema and wai-routes from git submodules that its cabal.project needs, +# and a plain `github:` ref downloads a tarball that omits them, so haskell.nix +# then fails with "modules/hjsonpointer does not contain any .cabal file". The +# `git+https://…?submodules=1` fetcher pulls them; `github:…?submodules=1` does +# not (it is silently ignored), and the flake's own `self.submodules = true` is +# not enough either. Override with a like-for-like ref, e.g.: +# bash bench/tx-generator/test-ogmios.sh 'git+https://github.com/IntersectMBO/ogmios?submodules=1&ref=' +# +set -euo pipefail + +OGMIOS_FLAKE="${1:-git+https://github.com/IntersectMBO/ogmios?submodules=1&ref=testnet-tx-gen-tests}" +TESTNET_MAGIC=42 +OGMIOS_PORT=11337 + +# --- Check host prerequisites --- +for cmd in nix jq nc; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "ERROR: required command not found: $cmd" + exit 1 + fi +done + +# --- Resolve binaries from the ogmios flake --- +echo "=== Resolving nix packages ===" +echo " ogmios flake: $OGMIOS_FLAKE" + +# ogmios: packages.ogmios-exe +OGMIOS=$(nix build "${OGMIOS_FLAKE}#ogmios" --no-link --print-out-paths)/bin/ogmios + +# cardano-node tools: input ref from the ogmios flake (uses tag, not hash). +# Requires the ogmios flake to declare a cardano-node input — the test +# branch does, but e.g. ogmios master does not, in which case the jq below +# yields "github:null/null/null". Guard against that with a clear error. +CN_FLAKE=$(nix flake metadata "${OGMIOS_FLAKE}" --json \ + | jq -r '.locks.nodes["cardano-node"].original + | "github:\(.owner)/\(.repo)/\(.ref)"') +echo " cardano-node flake: $CN_FLAKE" +case "$CN_FLAKE" in + *null*) + echo "ERROR: ogmios flake '$OGMIOS_FLAKE' has no usable cardano-node input." + echo " Use an ogmios ref that declares one (e.g. ...&ref=testnet-tx-gen-tests)." + exit 1 + ;; +esac + +CARDANO_NODE=$(nix build "${CN_FLAKE}#cardano-node" --no-link --print-out-paths)/bin/cardano-node +CARDANO_CLI=$(nix build "${CN_FLAKE}#cardano-cli" --no-link --print-out-paths)/bin/cardano-cli +CARDANO_TESTNET=$(nix build "${CN_FLAKE}#cardano-testnet" --no-link --print-out-paths)/bin/cardano-testnet + +# tx-generator: locally-built (resolve to absolute path) +# -perm -u+x is understood by both GNU and BSD find +TX_GENERATOR=$(cabal list-bin tx-generator 2>/dev/null \ + || find "$(pwd)/dist-newstyle" -name tx-generator -type f -perm -u+x | head -1) + +for bin in OGMIOS CARDANO_TESTNET CARDANO_NODE CARDANO_CLI TX_GENERATOR; do + path="${!bin}" + if [ ! -x "$path" ]; then + echo "ERROR: $bin not found or not executable at: $path" + exit 1 + fi + echo " $bin: $path" +done + +# --- Set up work directory --- +WORKDIR=$(mktemp -d /tmp/ogmios-test.XXXXXX) +LOGS_DIR="$WORKDIR/logs" +TESTNET_DIR="$WORKDIR/testnet" +mkdir -p "$LOGS_DIR" + +echo "" +echo "=== Ogmios tx-generator integration test ===" +echo "Work directory: $WORKDIR" + +# shellcheck disable=SC2329 # invoked via the EXIT trap only +cleanup() { + echo "" + echo "=== Cleaning up ===" + [ -n "${OGMIOS_PID:-}" ] && kill "$OGMIOS_PID" 2>/dev/null && echo "Stopped ogmios (PID $OGMIOS_PID)" + [ -n "${TESTNET_PID:-}" ] && kill "$TESTNET_PID" 2>/dev/null && echo "Stopped cardano-testnet (PID $TESTNET_PID)" + echo "Logs available at: $LOGS_DIR" +} +trap cleanup EXIT + +# --- 1. Start cardano-testnet --- +echo "" +echo "--- Starting cardano-testnet ---" +CARDANO_CLI="$CARDANO_CLI" CARDANO_NODE="$CARDANO_NODE" \ + "$CARDANO_TESTNET" cardano \ + --testnet-magic "$TESTNET_MAGIC" \ + --output-dir "$TESTNET_DIR" \ + > "$LOGS_DIR/cardano-testnet.stdout" 2> "$LOGS_DIR/cardano-testnet.stderr" & +TESTNET_PID=$! +echo "cardano-testnet PID: $TESTNET_PID" + +# --- 2. Wait for node socket --- +echo "Waiting for node socket..." +SOCKET_PATH="" +for i in $(seq 1 120); do + SOCKET_PATH=$(find "$TESTNET_DIR" -name "sock" 2>/dev/null | head -1 || true) + if [ -n "$SOCKET_PATH" ] && [ -S "$SOCKET_PATH" ]; then + break + fi + SOCKET_PATH="" + sleep 1 +done + +if [ -z "$SOCKET_PATH" ]; then + echo "FAILED: node socket not found after 120s" + tail -20 "$LOGS_DIR/cardano-testnet.stderr" + exit 1 +fi +echo "Found node socket: $SOCKET_PATH" + +# --- 3. Find node config --- +CONFIG_PATH="" +for name in configuration.yaml configuration.json config.json; do + CONFIG_PATH=$(find "$TESTNET_DIR" -name "$name" 2>/dev/null | head -1 || true) + [ -n "$CONFIG_PATH" ] && break +done +if [ -z "$CONFIG_PATH" ]; then + echo "FAILED: node config not found" + exit 1 +fi +echo "Found node config: $CONFIG_PATH" + +# --- 4. Start ogmios --- +echo "" +echo "--- Starting ogmios ---" +"$OGMIOS" \ + --node-socket "$SOCKET_PATH" \ + --node-config "$CONFIG_PATH" \ + --port "$OGMIOS_PORT" \ + --log-level error \ + > "$LOGS_DIR/ogmios.stdout" 2> "$LOGS_DIR/ogmios.stderr" & +OGMIOS_PID=$! +echo "ogmios PID: $OGMIOS_PID" + +echo "Waiting for ogmios on port $OGMIOS_PORT..." +for i in $(seq 1 60); do + if nc -z 127.0.0.1 "$OGMIOS_PORT" 2>/dev/null; then break; fi + sleep 1 +done +if ! nc -z 127.0.0.1 "$OGMIOS_PORT" 2>/dev/null; then + echo "FAILED: ogmios not accepting connections after 60s" + tail -20 "$LOGS_DIR/ogmios.stderr" + exit 1 +fi +echo "ogmios is ready on port $OGMIOS_PORT" + +# --- 5. Create tx-generator config --- +echo "" +echo "--- Creating tx-generator config ---" +SIG_KEY=$(find "$TESTNET_DIR" -name "utxo.skey" -path "*/utxo1/*" 2>/dev/null | head -1 || true) +if [ -z "$SIG_KEY" ]; then + echo "FAILED: signing key not found" + exit 1 +fi + +CONFIG_FILE="$WORKDIR/tx-generator-config.json" +# debugMode is mandatory alongside a submission endpoint: it is a functional +# transport without pacing or metrics, and the config compiler rejects it for +# benchmark (non-debug) runs. submissionEndpointProtocol and submissionEndpointURI +# must be set together. +cat > "$CONFIG_FILE" << EOF +{ + "tx_count": 10, + "tps": 2, + "inputs_per_tx": 2, + "outputs_per_tx": 2, + "tx_fee": 212345, + "min_utxo_value": 1000000, + "add_tx_size": 39, + "init_cooldown": 5, + "era": "Conway", + "keepalive": 30, + "debugMode": true, + "plutus": null, + "sigKey": "$SIG_KEY", + "submissionEndpointProtocol": "Ogmios", + "submissionEndpointURI": "ws://127.0.0.1:$OGMIOS_PORT" +} +EOF + +# The benchmarking phase pays to the compiler's hardcoded "BenchmarkingDone" +# key; derive its address instead of hardcoding it. The cborHex must match +# keyBenchmarkDone in src/Cardano/Benchmarking/Compiler.hs. +# (addr_test1vz4qz2ayucp7xvnthrx93uhha7e04gvxttpnuq4e6mx2n5gzfw23z @ magic 42) +cat > "$WORKDIR/benchmark-done.skey" << EOF +{ + "type": "PaymentSigningKeyShelley_ed25519", + "description": "", + "cborHex": "582016ca4f13fa17557e56a7d0dd3397d747db8e1e22fdb5b9df638abdb680650d50" +} +EOF +"$CARDANO_CLI" key verification-key \ + --signing-key-file "$WORKDIR/benchmark-done.skey" \ + --verification-key-file "$WORKDIR/benchmark-done.vkey" +BENCH_ADDR=$("$CARDANO_CLI" address build \ + --payment-verification-key-file "$WORKDIR/benchmark-done.vkey" \ + --testnet-magic "$TESTNET_MAGIC") + +query_utxo_count() { + "$CARDANO_CLI" query utxo \ + --address "$BENCH_ADDR" \ + --testnet-magic "$TESTNET_MAGIC" \ + --socket-path "$SOCKET_PATH" \ + --out-file /dev/stdout | jq 'length' +} + +# --- 6. UTxO count before --- +echo "" +echo "--- UTxOs at benchmark address before tx-generator ---" +BEFORE=$(query_utxo_count) +echo " $BEFORE UTxOs at $BENCH_ADDR" + +# --- 7. Run tx-generator via Ogmios --- +echo "" +echo "--- Running tx-generator with Ogmios submission ---" +# the if/else keeps a failure from tripping `set -e` before the log tails +# and the exit-code report below get a chance to run +if ( cd "$WORKDIR" && "$TX_GENERATOR" json_highlevel "$CONFIG_FILE" \ + --testnet-config-dir "$TESTNET_DIR" \ + > "$LOGS_DIR/tx-generator.stdout" 2> "$LOGS_DIR/tx-generator.stderr" ); then + TX_EXIT=0 +else + TX_EXIT=$? +fi + +echo "" +echo "--- tx-generator stdout (last 30 lines) ---" +tail -30 "$LOGS_DIR/tx-generator.stdout" +echo "" +echo "--- tx-generator stderr (last 20 lines) ---" +tail -20 "$LOGS_DIR/tx-generator.stderr" + +if [ "$TX_EXIT" -ne 0 ]; then + echo "" + echo "=== FAILED: tx-generator exit code $TX_EXIT ===" + exit "$TX_EXIT" +fi + +# --- 8. Wait for txs to land in blocks, then count UTxOs --- +echo "" +echo "--- Waiting for transactions to be included in blocks ---" +AFTER="$BEFORE" +for i in $(seq 1 60); do + AFTER=$(query_utxo_count) + echo " [$i] $AFTER UTxOs at benchmark address" + if [ "$AFTER" -gt "$BEFORE" ]; then + sleep 5 + AFTER=$(query_utxo_count) + echo " [final] $AFTER UTxOs at benchmark address" + break + fi + sleep 2 +done + +echo "" +echo "=== Results ===" +echo " UTxOs before: $BEFORE" +echo " UTxOs after: $AFTER" +echo " New UTxOs: $((AFTER - BEFORE))" + +# submissions were all accepted (or we exited above), so new UTxOs must +# have appeared by now for the run to count as a pass +if [ "$AFTER" -le "$BEFORE" ]; then + echo "" + echo "=== FAILED: no new UTxOs appeared at the benchmark address ===" + exit 1 +fi + +echo "" +echo "=== PASSED ===" +exit 0 diff --git a/bench/tx-generator/tx-generator.cabal b/bench/tx-generator/tx-generator.cabal index ef230e3e003..48b25d2e0ca 100644 --- a/bench/tx-generator/tx-generator.cabal +++ b/bench/tx-generator/tx-generator.cabal @@ -1,7 +1,7 @@ cabal-version: 3.0 name: tx-generator -version: 2.16 +version: 2.17 synopsis: A transaction workload generator for Cardano clusters description: A transaction workload generator for Cardano clusters. category: Cardano, @@ -70,7 +70,9 @@ library Cardano.Benchmarking.Script.Aeson Cardano.Benchmarking.Script.Core Cardano.Benchmarking.Script.Env + Cardano.Benchmarking.Script.Ogmios Cardano.Benchmarking.Script.Selftest + Cardano.Benchmarking.Script.Submission Cardano.Benchmarking.Script.Types Cardano.Benchmarking.TpsThrottle Cardano.Benchmarking.Tracer @@ -140,6 +142,7 @@ library , mtl , network , network-mux + , network-uri , optparse-applicative , ouroboros-consensus:{ouroboros-consensus, cardano, diffusion} >= 3.0.1 , ouroboros-network:{api, framework, framework-tracing, ouroboros-network, protocols} >= 1.1 @@ -159,6 +162,7 @@ library , transformers , transformers-except , unordered-containers + , websockets , yaml -- Needed by "Cardano.Api.Internal.ProtocolParameters" port. , either