From 363ab16cb3993e9b3011d1cfa082b60548636a5a Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Fri, 29 May 2026 01:31:23 +0200 Subject: [PATCH 01/18] Add support for sending tx through ogmios --- .../src/Cardano/Benchmarking/Compiler.hs | 27 ++-- .../src/Cardano/Benchmarking/Script/Core.hs | 2 + .../src/Cardano/Benchmarking/Script/Ogmios.hs | 123 ++++++++++++++++++ .../src/Cardano/Benchmarking/Script/Types.hs | 3 +- .../Cardano/TxGenerator/Setup/NixService.hs | 1 + bench/tx-generator/tx-generator.cabal | 3 + 6 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs index 719819250b1..f4fd0c7c34e 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,19 @@ benchmarkingPhase wallet collateralWallet = do inputs <- askNixOption _nix_inputs_per_tx outputs <- askNixOption _nix_outputs_per_tx txParams <- askNixOption txGenTxParams + ogmiosUrl <- askNixOption _nix_ogmiosUrl doneWallet <- newWallet "done_wallet" let payMode = PayToAddr keyNameBenchmarkDone doneWallet - submitMode = if debugMode - then LocalSocket - else Benchmark targetNodes tps txCount + submitMode + | Just url <- ogmiosUrl = Ogmios url + | debugMode = LocalSocket + | otherwise = 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 +252,11 @@ cmd1 cmd arg = emit . cmd =<< askNixOption arg askNixOption :: (NixServiceOptions -> v) -> Compiler v askNixOption = asks +getSetupSubmitMode :: Compiler SubmitMode +getSetupSubmitMode = do + ogmiosUrl <- askNixOption _nix_ogmiosUrl + return $ maybe LocalSocket Ogmios ogmiosUrl + delay :: Compiler () delay = cmd1 Delay _nix_init_cooldown diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs index 1b345952511..6026fbaeb98 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs @@ -30,6 +30,7 @@ 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 Ogmios 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 +245,7 @@ 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 + Ogmios url -> Ogmios.submitAllOgmios url 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..230011ffa0f --- /dev/null +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -0,0 +1,123 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE RankNTypes #-} + +module Cardano.Benchmarking.Script.Ogmios + ( submitAllOgmios + ) where + +import Cardano.Api (IsShelleyBasedEra, Tx, serialiseToCBOR) + +import Cardano.Benchmarking.Script.Env (ActionM, liftTxGenError, traceDebug) +import Cardano.Benchmarking.Wallet (TxStream) +import Cardano.TxGenerator.Types (TxGenError (..)) + +import Prelude + +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.Text (Text) +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text +import Network.URI (parseURI, uriAuthority, uriPath, uriPort, uriRegName) +import qualified Network.WebSockets as WS + +import Streaming + + +submitAllOgmios :: forall era. IsShelleyBasedEra era + => String -> TxStream IO era -> ActionM () +submitAllOgmios url txStream = + case parseOgmiosUrl url of + Left err -> liftTxGenError $ TxGenError err + Right (host, port, path) -> do + traceDebug $ "Ogmios: connecting to " ++ url + result <- liftIO $ WS.runClient host port path $ \conn -> + submitLoop conn txStream 0 0 0 + case result of + Left err -> liftTxGenError err + Right (sent, failed) -> + traceDebug $ "Ogmios: done, " ++ show sent ++ " sent, " ++ show failed ++ " failed" + +submitLoop :: IsShelleyBasedEra era + => WS.Connection -> TxStream IO era -> Int -> Int -> Int -> IO (Either TxGenError (Int, Int)) +submitLoop conn stream reqId sent failed = 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 + let msg = Aeson.encode (mkSubmitRequest tx reqId) + WS.sendTextData conn msg + resp <- WS.receiveData conn + case parseOgmiosResponse resp of + Left parseErr -> + return $ Left $ TxGenError $ "Ogmios response parse error: " ++ parseErr + Right (OgmiosError _code errMsg _) -> do + putStrLn $ "Ogmios submit failed: " ++ Text.unpack errMsg + submitLoop conn rest (reqId + 1) sent (failed + 1) + Right (OgmiosSuccess _txId) -> + submitLoop conn rest (reqId + 1) (sent + 1) failed + + +parseOgmiosUrl :: String -> Either String (String, Int, String) +parseOgmiosUrl urlStr = + case parseURI urlStr of + Nothing -> Left $ "Invalid Ogmios URL: " ++ urlStr + Just uri -> case uriAuthority uri of + Nothing -> Left $ "No authority in Ogmios URL: " ++ urlStr + Just auth -> + let host = uriRegName auth + port = case uriPort auth of + "" -> 1337 + ':':p -> read p + p -> read p + path = case uriPath uri of + "" -> "/" + p -> p + in Right (host, port, path) + + +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 + ] + + +data OgmiosResult + = OgmiosSuccess Text + | OgmiosError Int Text Value + +parseOgmiosResponse :: LBS.ByteString -> Either String OgmiosResult +parseOgmiosResponse bs = + case Aeson.eitherDecode bs of + Left err -> Left err + Right val -> Aeson.parseEither parseResult val + where + parseResult = Aeson.withObject "OgmiosResponse" $ \obj -> do + mResult <- obj .:? "result" + case mResult of + Just resultVal -> do + txId <- Aeson.withObject "result" (\r -> do + txObj <- r .: "transaction" + Aeson.withObject "transaction" (\t -> t .: "id") txObj + ) resultVal + return $ OgmiosSuccess txId + Nothing -> do + errVal <- obj .: "error" + Aeson.withObject "error" (\errObj -> do + code <- errObj .: "code" + msg <- errObj .: "message" + dat <- errObj .:? "data" + return $ OgmiosError code msg (maybe Null id dat) + ) errVal diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs index 08a2f63c8c8..1df6590c8f8 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs @@ -33,7 +33,7 @@ module Cardano.Benchmarking.Script.Types ( , ScriptBudget(AutoScript, StaticScriptBudget) , ScriptSpec(..) , SubmitMode(Benchmark, DiscardTX, DumpToFile, LocalSocket, - NodeToNode) + NodeToNode, Ogmios) , TargetNodes , TxList(..) ) where @@ -185,6 +185,7 @@ data SubmitMode where DumpToFile :: !FilePath -> SubmitMode DiscardTX :: SubmitMode NodeToNode :: NonEmpty NodeIPv4Address -> SubmitMode --deprecated + Ogmios :: !String -> 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..20962e26e6c 100644 --- a/bench/tx-generator/src/Cardano/TxGenerator/Setup/NixService.hs +++ b/bench/tx-generator/src/Cardano/TxGenerator/Setup/NixService.hs @@ -59,6 +59,7 @@ data NixServiceOptions = NixServiceOptions { , _nix_sigKey :: SigningKeyFile In , _nix_localNodeSocketPath :: String , _nix_targetNodes :: NonEmpty NodeDescription + , _nix_ogmiosUrl :: Maybe String } deriving (Show, Eq) deriving instance Generic NixServiceOptions diff --git a/bench/tx-generator/tx-generator.cabal b/bench/tx-generator/tx-generator.cabal index ef230e3e003..7572a21fde1 100644 --- a/bench/tx-generator/tx-generator.cabal +++ b/bench/tx-generator/tx-generator.cabal @@ -70,6 +70,7 @@ library Cardano.Benchmarking.Script.Aeson Cardano.Benchmarking.Script.Core Cardano.Benchmarking.Script.Env + Cardano.Benchmarking.Script.Ogmios Cardano.Benchmarking.Script.Selftest Cardano.Benchmarking.Script.Types Cardano.Benchmarking.TpsThrottle @@ -140,6 +141,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 +161,7 @@ library , transformers , transformers-except , unordered-containers + , websockets , yaml -- Needed by "Cardano.Api.Internal.ProtocolParameters" port. , either From 68dbfff91496635a223f970ab1fc7c571f689c8b Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Fri, 29 May 2026 03:51:00 +0200 Subject: [PATCH 02/18] Patch AsyncBenchmarkControl --- .../src/Cardano/Benchmarking/Script.hs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script.hs index 9b7537bc250..4a586195afe 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script.hs @@ -14,19 +14,18 @@ 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 import Control.Concurrent (threadDelay) +import qualified Control.Concurrent.Async as Async 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) @@ -61,11 +60,22 @@ runScript env script constants@EnvConsts { .. } = do 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 + Nothing -> liftIO noopBenchmarkControl + +noopBenchmarkControl :: IO AsyncBenchmarkControl +noopBenchmarkControl = do + feeder <- Async.async (return ()) + pure AsyncBenchmarkControl + { abcFeeder = feeder + , abcWorkers = [] + , abcSummary = pure SubmissionSummary + { ssTxSent = 0, ssTxUnavailable = 0 + , ssElapsed = 0, ssEffectiveTps = 0 + , ssThreadwiseTps = [], ssFailures = [] + } + , abcShutdown = return () + } shutDownLogging :: Env.ActionM () shutDownLogging = do From 1407c32071febc2853422033aca7dd2347565ea0 Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Fri, 29 May 2026 03:51:37 +0200 Subject: [PATCH 03/18] Add script for testing ogmios with tx-generator --- bench/tx-generator/test-ogmios.sh | 221 ++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100755 bench/tx-generator/test-ogmios.sh diff --git a/bench/tx-generator/test-ogmios.sh b/bench/tx-generator/test-ogmios.sh new file mode 100755 index 00000000000..8ed32864716 --- /dev/null +++ b/bench/tx-generator/test-ogmios.sh @@ -0,0 +1,221 @@ +#!/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) +# +set -euo pipefail + +OGMIOS_FLAKE="${1:-github:IntersectMBO/ogmios/jmiller/dijkstra-integration}" +TESTNET_MAGIC=42 +OGMIOS_PORT=11337 + +# --- 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) + +# cardano-node tools: input ref from the ogmios flake (uses tag, not hash) +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" + +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) +TX_GENERATOR=$(cabal list-bin tx-generator 2>/dev/null \ + || find "$(pwd)/dist-newstyle" -name tx-generator -type f -perm +111 | 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" + +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" +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", + "ogmiosUrl": "ws://127.0.0.1:$OGMIOS_PORT" +} +EOF + +BENCH_ADDR="addr_test1vz4qz2ayucp7xvnthrx93uhha7e04gvxttpnuq4e6mx2n5gzfw23z" + +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 ---" +( cd "$WORKDIR" && "$TX_GENERATOR" json_highlevel "$CONFIG_FILE" \ + --testnet-config-dir "$TESTNET_DIR" \ + > "$LOGS_DIR/tx-generator.stdout" 2> "$LOGS_DIR/tx-generator.stderr" ) +TX_EXIT=$? + +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 ---" +for i in $(seq 1 30); 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))" + +exit "$TX_EXIT" From ccfb1359a628ab4c81fc963d7c1b7960838cd1c4 Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Fri, 12 Jun 2026 21:55:09 +0200 Subject: [PATCH 04/18] Strengthen and polish rough edges of the Ogmios implementation Strenghten and polish rough edges of implementation: * `runScript` now returns `Maybe AsyncBenchmarkControl` instead of fabricating a no-op control: submit modes that never start the benchmark machinery (LocalSocket, Ogmios) yield Nothing, and a failing run no longer dies with the misleading "AsyncBenchmarkControl uninitialized" error that masked the real one. noopBenchmarkControl is gone; both call sites in Command.hs only ever consumed fst, so they are unaffected. * WebSocket failures (DNS, refused connection, handshake rejection, mid-stream drops, close frames) are caught around WS.runClient and converted into TxGenError instead of escaping as raw exceptions past the error machinery and logging shutdown. * `parseOgmiosUrl` validates the scheme (plain `ws://` only; `wss://` was silently degrading to a plaintext connection), parses the port via readMaybe with a 1-65535 range check instead of a partial read (`ws://host:/` no longer crashes), and rejects empty hosts. * Submission responses are subject to a 90s timeout (generous, since the node may hold submissions back under mempool pressure) and their JSON-RPC id is verified against the request id; a mismatched or null id is treated as a protocol fault and aborts the run with the offending response described. * json_highlevel configs that set ogmiosUrl without debugMode: true are rejected at compile time with an explanatory error: Ogmios mode ignores tps/targetNodes and produces no benchmark metrics, so a config asking for a real benchmark must fail fast rather than run unpaced and unmeasured. Low-level json scripts are unaffected, and compileOptions fails before any node interaction. * Polish: parseOgmiosUrl/parseOgmiosResponse/OgmiosResult exported to make them unit-testable, `fromMaybe Null` instead of `maybe Null id`, unused `RankNTypes` pragma dropped, haddock module header added, import list put in stylish-haskell order. --- .../src/Cardano/Benchmarking/Compiler.hs | 16 ++- .../src/Cardano/Benchmarking/Script.hs | 41 ++---- .../src/Cardano/Benchmarking/Script/Ogmios.hs | 136 ++++++++++++------ 3 files changed, 119 insertions(+), 74 deletions(-) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs index f4fd0c7c34e..6b4c0bf80f8 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs @@ -200,12 +200,20 @@ benchmarkingPhase wallet collateralWallet = do txParams <- askNixOption txGenTxParams ogmiosUrl <- askNixOption _nix_ogmiosUrl doneWallet <- newWallet "done_wallet" + -- Ogmios 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 (ogmiosUrl, debugMode) of + (Just url, True) -> pure $ Ogmios url + (Just _, False) -> throwCompileError $ SomeCompilerError + "ogmiosUrl is a functional submission transport: it ignores tps and \ + \targetNodes and produces no benchmark metrics. Set debugMode: true \ + \to acknowledge this, or remove ogmiosUrl to run a real benchmark." + (Nothing, True) -> pure LocalSocket + (Nothing, False) -> pure $ Benchmark targetNodes tps txCount let payMode = PayToAddr keyNameBenchmarkDone doneWallet - submitMode - | Just url <- ogmiosUrl = Ogmios url - | debugMode = LocalSocket - | otherwise = Benchmark targetNodes tps txCount generator = Take txCount $ Cycle $ NtoM wallet payMode inputs outputs (Just $ txParamAddTxSize txParams) collateralWallet emit $ Submit era submitMode txParams generator case submitMode of diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script.hs index 4a586195afe..21cdff42fd3 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script.hs @@ -21,61 +21,42 @@ import Cardano.Benchmarking.Script.Types import Prelude import Control.Concurrent (threadDelay) -import qualified Control.Concurrent.Async as Async import Control.Concurrent.STM.TVar as STM (readTVar) import Control.Monad import Control.Monad.IO.Class import Control.Monad.STM as STM (atomically) -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 - Just abc -> pure abc - Nothing -> liftIO noopBenchmarkControl - -noopBenchmarkControl :: IO AsyncBenchmarkControl -noopBenchmarkControl = do - feeder <- Async.async (return ()) - pure AsyncBenchmarkControl - { abcFeeder = feeder - , abcWorkers = [] - , abcSummary = pure SubmissionSummary - { ssTxSent = 0, ssTxUnavailable = 0 - , ssElapsed = 0, ssEffectiveTps = 0 - , ssThreadwiseTps = [], ssFailures = [] - } - , abcShutdown = return () - } + Env.getEnvThreads shutDownLogging :: Env.ActionM () shutDownLogging = do diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs index 230011ffa0f..37e65c94768 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -1,9 +1,22 @@ +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE RankNTypes #-} +{-| +Module : Cardano.Benchmarking.Script.Ogmios +Description : Submit transactions through an Ogmios endpoint. + +This is a functional submission transport, not a benchmarking one: it +submits strictly one transaction per round trip over a single WebSocket +connection, ignores any TPS pacing, and reports no submission metrics +beyond a sent/failed count traced at debug level. The high-level config +compiler therefore only selects it together with @debugMode: true@. +-} module Cardano.Benchmarking.Script.Ogmios - ( submitAllOgmios + ( OgmiosResult (..) + , parseOgmiosResponse + , parseOgmiosUrl + , submitAllOgmios ) where import Cardano.Api (IsShelleyBasedEra, Tx, serialiseToCBOR) @@ -14,19 +27,32 @@ import Cardano.TxGenerator.Types (TxGenError (..)) import Prelude -import Data.Aeson (Value (..), object, (.=), (.:), (.:?)) +import Control.Exception (Handler (..), IOException, catches) +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.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text -import Network.URI (parseURI, uriAuthority, uriPath, uriPort, uriRegName) +import Network.URI (parseURI, uriAuthority, uriPath, uriPort, uriRegName, uriScheme) import qualified Network.WebSockets as WS +import System.Timeout (timeout) +import Text.Read (readMaybe) import Streaming +-- | The port Ogmios listens on by default. +defaultOgmiosPort :: Int +defaultOgmiosPort = 1337 + +-- | Per-request response timeout in microseconds. Generous, because the +-- node may hold a submission back while its mempool is saturated. +responseTimeout :: Int +responseTimeout = 90_000_000 submitAllOgmios :: forall era. IsShelleyBasedEra era => String -> TxStream IO era -> ActionM () @@ -35,12 +61,20 @@ submitAllOgmios url txStream = Left err -> liftTxGenError $ TxGenError err Right (host, port, path) -> do traceDebug $ "Ogmios: connecting to " ++ url - result <- liftIO $ WS.runClient host port path $ \conn -> - submitLoop conn txStream 0 0 0 + result <- liftIO $ + WS.runClient host port path (\conn -> submitLoop conn txStream 0 0 0) + `catches` + [ Handler $ \(e :: WS.HandshakeException) -> connectionFailure e + , Handler $ \(e :: WS.ConnectionException) -> connectionFailure e + , Handler $ \(e :: IOException) -> connectionFailure e + ] case result of Left err -> liftTxGenError err Right (sent, failed) -> traceDebug $ "Ogmios: done, " ++ show sent ++ " sent, " ++ show failed ++ " failed" + where + connectionFailure :: Show e => e -> IO (Either TxGenError (Int, Int)) + connectionFailure e = return $ Left $ TxGenError $ "Ogmios connection failure: " ++ show e submitLoop :: IsShelleyBasedEra era => WS.Connection -> TxStream IO era -> Int -> Int -> Int -> IO (Either TxGenError (Int, Int)) @@ -50,36 +84,54 @@ submitLoop conn stream reqId sent failed = do Left () -> return $ Right (sent, failed) Right (Left err :> _rest) -> return $ Left err Right (Right tx :> rest) -> do - let msg = Aeson.encode (mkSubmitRequest tx reqId) - WS.sendTextData conn msg - resp <- WS.receiveData conn - case parseOgmiosResponse resp of - Left parseErr -> - return $ Left $ TxGenError $ "Ogmios response parse error: " ++ parseErr - Right (OgmiosError _code errMsg _) -> do - putStrLn $ "Ogmios submit failed: " ++ Text.unpack errMsg - submitLoop conn rest (reqId + 1) sent (failed + 1) - Right (OgmiosSuccess _txId) -> - submitLoop conn rest (reqId + 1) (sent + 1) failed - + WS.sendTextData conn $ Aeson.encode (mkSubmitRequest tx reqId) + mResp <- timeout responseTimeout $ WS.receiveData conn + case mResp of + Nothing -> return $ Left $ TxGenError $ + "Ogmios: no response to request " ++ show reqId + ++ " within " ++ show (responseTimeout `div` 1_000_000) ++ "s" + Just resp -> case parseOgmiosResponse resp of + Left parseErr -> + return $ Left $ TxGenError $ "Ogmios response parse error: " ++ parseErr + Right (respId, result) + -- a mismatched (or null) id is a protocol-level fault, not a + -- transaction rejection: the connection is out of sync, so stop + | respId /= Just reqId -> return $ Left $ TxGenError $ + "Ogmios: response id mismatch: expected " ++ show reqId + ++ ", got " ++ show respId ++ " (" ++ describe result ++ ")" + | OgmiosError _code errMsg _errData <- result -> do + putStrLn $ "Ogmios submit failed: " ++ Text.unpack errMsg + submitLoop conn rest (reqId + 1) sent (failed + 1) + | otherwise -> + submitLoop conn rest (reqId + 1) (sent + 1) failed + where + describe (OgmiosSuccess txId) = "success, tx " ++ Text.unpack txId + describe (OgmiosError _ msg _) = "error: " ++ Text.unpack msg parseOgmiosUrl :: String -> Either String (String, Int, String) -parseOgmiosUrl urlStr = - case parseURI urlStr of - Nothing -> Left $ "Invalid Ogmios URL: " ++ urlStr - Just uri -> case uriAuthority uri of - Nothing -> Left $ "No authority in Ogmios URL: " ++ urlStr - Just auth -> - let host = uriRegName auth - port = case uriPort auth of - "" -> 1337 - ':':p -> read p - p -> read p - path = case uriPath uri of - "" -> "/" - p -> p - in Right (host, port, path) - +parseOgmiosUrl urlStr = do + uri <- note ("Invalid Ogmios URL: " ++ urlStr) $ parseURI urlStr + -- 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 <- note ("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 + note msg = maybe (Left msg) Right + parsePort p = case readMaybe p of + Just n | n >= 1 && n <= 65535 -> Right n + _ -> Left $ "Invalid port in Ogmios URL: " ++ urlStr mkSubmitRequest :: IsShelleyBasedEra era => Tx era -> Int -> Value mkSubmitRequest tx reqId = object @@ -93,20 +145,23 @@ mkSubmitRequest tx reqId = object , "id" .= reqId ] - data OgmiosResult = OgmiosSuccess Text | OgmiosError Int Text Value + deriving (Eq, Show) -parseOgmiosResponse :: LBS.ByteString -> Either String OgmiosResult +-- | 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 = case Aeson.eitherDecode bs of Left err -> Left err - Right val -> Aeson.parseEither parseResult val + Right val -> Aeson.parseEither parseResponse val where - parseResult = Aeson.withObject "OgmiosResponse" $ \obj -> do + parseResponse = Aeson.withObject "OgmiosResponse" $ \obj -> do + respId <- obj .:? "id" mResult <- obj .:? "result" - case mResult of + result <- case mResult of Just resultVal -> do txId <- Aeson.withObject "result" (\r -> do txObj <- r .: "transaction" @@ -119,5 +174,6 @@ parseOgmiosResponse bs = code <- errObj .: "code" msg <- errObj .: "message" dat <- errObj .:? "data" - return $ OgmiosError code msg (maybe Null id dat) + return $ OgmiosError code msg (fromMaybe Null dat) ) errVal + return (respId, result) From 00f9dfaffff4dfe9c213fb0dfbfe361852aae3c8 Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Sat, 13 Jun 2026 00:17:19 +0200 Subject: [PATCH 05/18] Add documentation, improve errors, and add changelog entry * Document the limitation on the implementation of the support of Ogmios as a target that limits the throughput to one request in flight per round trip (Ogmios supports pipelining by JSON-RPC id, but we are not supporting it for now. * Document that only submission goes through Ogmios: protocol-parameter and era queries as well as protocol startup still require the local node socket and config file. * Route per-transaction rejections through the benchmark tracer instead of putStrLn, so they reach the trace stream like every other submission event instead of interleaving arbitrarily with it. The failure detail payload reported by Ogmios ('error.data'), which carries the actual ledger failure and was previously discarded, is included in the message along with the error code. * Add `ogmiosUrl` to the README's connection-settings table plus a 'Submitting through Ogmios' section, add a changelog entry for the feature (including the clean-exit behavior change for scripts that never start the benchmark machinery), and bump the package version to 2.17. --- bench/tx-generator/CHANGELOG.md | 5 ++ bench/tx-generator/README.md | 13 +++++ .../src/Cardano/Benchmarking/Script/Ogmios.hs | 48 +++++++++++++++---- bench/tx-generator/tx-generator.cabal | 2 +- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/bench/tx-generator/CHANGELOG.md b/bench/tx-generator/CHANGELOG.md index 869512bad16..018e00ba51f 100644 --- a/bench/tx-generator/CHANGELOG.md +++ b/bench/tx-generator/CHANGELOG.md @@ -1,5 +1,10 @@ # ChangeLog +## 2.17 -- Jun 2026 + +* New `Ogmios` submit mode, selected by the optional `ogmiosUrl` config key: when set, all transaction submission (genesis fund import, UTxO splitting, and the benchmarking phase) goes through an Ogmios WebSocket endpoint as JSON-RPC 2.0 `submitTransaction` calls instead of the local socket / Node-to-Node protocols. This is a functional submission transport without TPS pacing or benchmark metrics, so the high-level config compiler only accepts `ogmiosUrl` together with `debugMode: true`. +* Fix: Scripts that never start the node-to-node benchmark machinery (such as `debugMode: true` runs and low-level `json` scripts without a `Benchmark` submit phase) now exit cleanly instead of crashing at shutdown with "AsyncBenchmarkControl absent". + ## 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..e1e82291fd1 100644 --- a/bench/tx-generator/README.md +++ b/bench/tx-generator/README.md @@ -58,6 +58,7 @@ 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 | +| `ogmiosUrl` | Optional ogmios endpoint (`ws://host:port`) for tx submission | No | ### Transaction Settings @@ -111,6 +112,18 @@ 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 Ogmios + +Setting `ogmiosUrl` (e.g. `"ogmiosUrl": "ws://127.0.0.1:1337"`) reroutes the submission of **every** phase — genesis fund import, UTxO splitting, and the final phase — through that [Ogmios](https://ogmios.dev) endpoint, as JSON-RPC 2.0 `submitTransaction` calls over a WebSocket (only plain `ws://` is supported). + +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 Ogmios) and a final sent/failed count go to the trace output. +- Because no real benchmark runs, the config compiler requires `debugMode: true` alongside `ogmiosUrl` 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 Ogmios. + ## 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/Script/Ogmios.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs index 37e65c94768..4587ea51934 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -11,6 +11,20 @@ submits strictly one transaction per round trip over a single WebSocket connection, ignores any TPS pacing, and reports no submission metrics beyond a sent/failed count traced at debug level. The high-level config compiler therefore only selects it together with @debugMode: true@. + +Throughput is bounded by the round-trip time to the endpoint, since at +most one request is in flight at a time. Ogmios itself supports +pipelining 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. + +Note that only transaction submission goes through Ogmios. Everything +else still talks to the local node directly: protocol-parameter and era +queries as well as protocol startup use the node socket and config +file, so tx-generator keeps requiring local node access even when +submitting through a remote endpoint. -} module Cardano.Benchmarking.Script.Ogmios ( OgmiosResult (..) @@ -21,8 +35,11 @@ module Cardano.Benchmarking.Script.Ogmios import Cardano.Api (IsShelleyBasedEra, Tx, serialiseToCBOR) -import Cardano.Benchmarking.Script.Env (ActionM, liftTxGenError, traceDebug) +import Cardano.Benchmarking.LogTypes (BenchTracers (..), TraceBenchTxSubmit (..)) +import Cardano.Benchmarking.Script.Env (ActionM, getBenchTracers, liftTxGenError, + traceDebug) import Cardano.Benchmarking.Wallet (TxStream) +import Cardano.Logging (traceWith) import Cardano.TxGenerator.Types (TxGenError (..)) import Prelude @@ -60,9 +77,23 @@ submitAllOgmios url txStream = case parseOgmiosUrl url of Left err -> liftTxGenError $ TxGenError err Right (host, port, path) -> do + tracers <- getBenchTracers + -- per-transaction rejections must reach the trace stream like every + -- other submission event, not stdout; the loop itself runs in plain + -- IO under the WebSocket client, so hand it a prebuilt trace action + let traceSubmitFail :: Int -> Text -> Value -> IO () + traceSubmitFail code msg errData = + traceWith (btTxSubmit_ tracers) $ TraceBenchTxSubError $ Text.concat + [ "Ogmios submit failed: ", msg + , " (code ", Text.pack (show code), ")" + , case errData of + Null -> "" + dat -> ", details: " + <> Text.decodeUtf8 (LBS.toStrict (Aeson.encode dat)) + ] traceDebug $ "Ogmios: connecting to " ++ url result <- liftIO $ - WS.runClient host port path (\conn -> submitLoop conn txStream 0 0 0) + WS.runClient host port path (\conn -> submitLoop traceSubmitFail conn txStream 0 0 0) `catches` [ Handler $ \(e :: WS.HandshakeException) -> connectionFailure e , Handler $ \(e :: WS.ConnectionException) -> connectionFailure e @@ -77,8 +108,9 @@ submitAllOgmios url txStream = connectionFailure e = return $ Left $ TxGenError $ "Ogmios connection failure: " ++ show e submitLoop :: IsShelleyBasedEra era - => WS.Connection -> TxStream IO era -> Int -> Int -> Int -> IO (Either TxGenError (Int, Int)) -submitLoop conn stream reqId sent failed = do + => (Int -> Text -> Value -> IO ()) + -> WS.Connection -> TxStream IO era -> Int -> Int -> Int -> IO (Either TxGenError (Int, Int)) +submitLoop traceSubmitFail conn stream reqId sent failed = do step <- Streaming.inspect stream case step of Left () -> return $ Right (sent, failed) @@ -99,11 +131,11 @@ submitLoop conn stream reqId sent failed = do | respId /= Just reqId -> return $ Left $ TxGenError $ "Ogmios: response id mismatch: expected " ++ show reqId ++ ", got " ++ show respId ++ " (" ++ describe result ++ ")" - | OgmiosError _code errMsg _errData <- result -> do - putStrLn $ "Ogmios submit failed: " ++ Text.unpack errMsg - submitLoop conn rest (reqId + 1) sent (failed + 1) + | OgmiosError code errMsg errData <- result -> do + traceSubmitFail code errMsg errData + submitLoop traceSubmitFail conn rest (reqId + 1) sent (failed + 1) | otherwise -> - submitLoop conn rest (reqId + 1) (sent + 1) failed + submitLoop traceSubmitFail conn rest (reqId + 1) (sent + 1) failed where describe (OgmiosSuccess txId) = "success, tx " ++ Text.unpack txId describe (OgmiosError _ msg _) = "error: " ++ Text.unpack msg diff --git a/bench/tx-generator/tx-generator.cabal b/bench/tx-generator/tx-generator.cabal index 7572a21fde1..e5ab7d93660 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, From 77f84d53f7346a66a860648405effbf2cf05ba98 Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Sat, 13 Jun 2026 00:47:11 +0200 Subject: [PATCH 06/18] Address issues with `test-ogmios.sh` script * Capture the tx-generator exit code with an if/else instead of a bare $? after the subshell: under 'set -e' a failure used to abort the script on the spot, so the log tails and the exit-code report - the part that matters exactly when the run fails - were dead code. (Note the 'if ! (...); then TX_EXIT=$?' form would not work either: the negation resets $? to 0.) * Actually fail on functional failure: if no new UTxOs appear at the benchmark address the script now exits 1 instead of falling through and reporting success. The confirmation window is doubled to 120s - in practice inclusion took ~50s, which the previous 60s window only just covered. * Derive the benchmark address from the hardcoded "BenchmarkingDone" signing key (cf. keyBenchmarkDone in Compiler.hs) at run time instead of hardcoding the bech32 - if the key or the derivation ever changes, the test follows instead of silently counting a stale address. * Default the ogmios flake ref to the repository's master branch instead of a personal work-in-progress branch that will rot away; a different ref can still be passed as the first argument. * Use 'find -perm -u+x' in the cabal fallback - the previous '+111' is BSD-only syntax that GNU find rejects; '-u+x' works in both. * Document that debugMode is mandatory alongside ogmiosUrl, silence the SC2329 false positive for the trap-only cleanup function on the CI-pinned shellcheck, and check for required host commands (nix, jq, nc) up front. --- bench/tx-generator/test-ogmios.sh | 61 +++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/bench/tx-generator/test-ogmios.sh b/bench/tx-generator/test-ogmios.sh index 8ed32864716..cdb62358e13 100755 --- a/bench/tx-generator/test-ogmios.sh +++ b/bench/tx-generator/test-ogmios.sh @@ -12,10 +12,18 @@ # set -euo pipefail -OGMIOS_FLAKE="${1:-github:IntersectMBO/ogmios/jmiller/dijkstra-integration}" +OGMIOS_FLAKE="${1:-github:IntersectMBO/ogmios/master}" 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" @@ -34,8 +42,9 @@ CARDANO_CLI=$(nix build "${CN_FLAKE}#cardano-cli" --no-link --print-out-paths)/b 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 +111 | head -1) + || 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}" @@ -56,6 +65,7 @@ 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 ===" @@ -141,6 +151,9 @@ if [ -z "$SIG_KEY" ]; then fi CONFIG_FILE="$WORKDIR/tx-generator-config.json" +# debugMode is mandatory alongside ogmiosUrl: the Ogmios submit mode is a +# functional transport without pacing or metrics, and the config compiler +# rejects it for benchmark (non-debug) runs. cat > "$CONFIG_FILE" << EOF { "tx_count": 10, @@ -160,7 +173,23 @@ cat > "$CONFIG_FILE" << EOF } EOF -BENCH_ADDR="addr_test1vz4qz2ayucp7xvnthrx93uhha7e04gvxttpnuq4e6mx2n5gzfw23z" +# 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 \ @@ -179,10 +208,15 @@ echo " $BEFORE UTxOs at $BENCH_ADDR" # --- 7. Run tx-generator via Ogmios --- echo "" echo "--- Running tx-generator with Ogmios submission ---" -( cd "$WORKDIR" && "$TX_GENERATOR" json_highlevel "$CONFIG_FILE" \ +# 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" ) -TX_EXIT=$? + > "$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) ---" @@ -200,7 +234,8 @@ fi # --- 8. Wait for txs to land in blocks, then count UTxOs --- echo "" echo "--- Waiting for transactions to be included in blocks ---" -for i in $(seq 1 30); do +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 @@ -218,4 +253,14 @@ echo " UTxOs before: $BEFORE" echo " UTxOs after: $AFTER" echo " New UTxOs: $((AFTER - BEFORE))" -exit "$TX_EXIT" +# 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 From c6c6be0cc1c58670aa69ed9a12d4afb5e9fc2a64 Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Sat, 13 Jun 2026 01:12:05 +0200 Subject: [PATCH 07/18] Fail on rejected transactions in Ogmios submit mode A rejected transaction used to be counted and traced but otherwise ignored: the run carried on and exited 0 even if every submission was rejected, so a script could mistake a completely failed run for a successful one. Now a rejection makes the run fail, with the strategy depending on the shape of the stream being submitted. Streams of chained setup transactions (genesis import, splitting) abort at the first rejection - everything after it spends outputs that will never exist, so carrying on would only produce a cascade of confusing follow-up rejections. The benchmarking phase's NtoM stream consists of mutually independent transactions, so it is submitted to the end and the action fails afterwards when the tally shows rejections. Either way the process exits non-zero, making exit codes trustworthy in scripts. The strategy is picked in submitInEra from the generator: NtoM (looked up through Take/Cycle/Sequence wrappers) submits to the end, everything else aborts at the first rejection. --- bench/tx-generator/CHANGELOG.md | 4 +- bench/tx-generator/README.md | 1 + .../src/Cardano/Benchmarking/Script/Core.hs | 2 +- .../src/Cardano/Benchmarking/Script/Ogmios.hs | 57 ++++++++++++++++--- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/bench/tx-generator/CHANGELOG.md b/bench/tx-generator/CHANGELOG.md index 018e00ba51f..efac6707e95 100644 --- a/bench/tx-generator/CHANGELOG.md +++ b/bench/tx-generator/CHANGELOG.md @@ -2,8 +2,8 @@ ## 2.17 -- Jun 2026 -* New `Ogmios` submit mode, selected by the optional `ogmiosUrl` config key: when set, all transaction submission (genesis fund import, UTxO splitting, and the benchmarking phase) goes through an Ogmios WebSocket endpoint as JSON-RPC 2.0 `submitTransaction` calls instead of the local socket / Node-to-Node protocols. This is a functional submission transport without TPS pacing or benchmark metrics, so the high-level config compiler only accepts `ogmiosUrl` together with `debugMode: true`. -* Fix: Scripts that never start the node-to-node benchmark machinery (such as `debugMode: true` runs and low-level `json` scripts without a `Benchmark` submit phase) now exit cleanly instead of crashing at shutdown with "AsyncBenchmarkControl absent". +* New `Ogmios` submit mode, selected by the optional `ogmiosUrl` config key: when set, all transaction submission (genesis fund import, UTxO splitting, and the benchmarking phase) goes through an Ogmios WebSocket endpoint as JSON-RPC 2.0 `submitTransaction` calls instead of the local socket / Node-to-Node protocols. This is a functional submission transport without TPS pacing or benchmark metrics, so the high-level config compiler only accepts `ogmiosUrl` together with `debugMode: true`. Rejected transaction submitted through Ogmios are traced through the regular tracing pipeline, including the failure detail reported by Ogmios, and make the run fail: setup phases abort at the first rejected transaction, and the benchmarking phase exits non-zero if any transaction was rejected. +* Scripts that never start the node-to-node benchmark machinery (such as `debugMode: true` runs and low-level `json` scripts without a `Benchmark` submit phase) now exit cleanly instead of crashing at shutdown with "AsyncBenchmarkControl absent". ## 2.16 -- Apr 2026 diff --git a/bench/tx-generator/README.md b/bench/tx-generator/README.md index e1e82291fd1..7be7bb17204 100644 --- a/bench/tx-generator/README.md +++ b/bench/tx-generator/README.md @@ -120,6 +120,7 @@ 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 Ogmios) 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 `ogmiosUrl` 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 Ogmios. diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs index 6026fbaeb98..4f1c832c9a2 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs @@ -245,7 +245,7 @@ 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 - Ogmios url -> Ogmios.submitAllOgmios url txStream + Ogmios url -> Ogmios.submitAllOgmios (Ogmios.onRejectionFor generator) url 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 index 4587ea51934..b86123c7883 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -25,9 +25,18 @@ else still talks to the local node directly: protocol-parameter and era queries as well as protocol startup use the node socket and config file, so tx-generator keeps requiring local node access even when submitting through a remote endpoint. + +Rejected transactions make the run fail. Streams of chained setup +transactions (genesis import, splitting) abort at the first rejection — +everything after it would be doomed anyway — while the benchmarking +phase's stream of independent transactions is submitted to the end and +the action fails afterwards if anything was rejected. Either way the +process exits non-zero, so exit codes can be trusted in scripts. -} module Cardano.Benchmarking.Script.Ogmios ( OgmiosResult (..) + , OnRejection (..) + , onRejectionFor , parseOgmiosResponse , parseOgmiosUrl , submitAllOgmios @@ -38,6 +47,7 @@ import Cardano.Api (IsShelleyBasedEra, Tx, serialiseToCBOR) 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 (..)) @@ -71,9 +81,29 @@ defaultOgmiosPort = 1337 responseTimeout :: Int responseTimeout = 90_000_000 +-- | How to proceed when Ogmios 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. The benchmarking phase's 'NtoM' stream +-- consists of mutually independent transactions, so submit it to the end +-- and let the final tally decide. +onRejectionFor :: Generator -> OnRejection +onRejectionFor generator = case generator of + NtoM {} -> ContinueOnRejection + Take _ g -> onRejectionFor g + Cycle g -> onRejectionFor g + Sequence gs + | any ((ContinueOnRejection ==) . onRejectionFor) gs -> ContinueOnRejection + _ -> AbortOnRejection + submitAllOgmios :: forall era. IsShelleyBasedEra era - => String -> TxStream IO era -> ActionM () -submitAllOgmios url txStream = + => OnRejection -> String -> TxStream IO era -> ActionM () +submitAllOgmios onRejection url txStream = case parseOgmiosUrl url of Left err -> liftTxGenError $ TxGenError err Right (host, port, path) -> do @@ -93,7 +123,7 @@ submitAllOgmios url txStream = ] traceDebug $ "Ogmios: connecting to " ++ url result <- liftIO $ - WS.runClient host port path (\conn -> submitLoop traceSubmitFail conn txStream 0 0 0) + WS.runClient host port path (\conn -> submitLoop onRejection traceSubmitFail conn txStream 0 0 0) `catches` [ Handler $ \(e :: WS.HandshakeException) -> connectionFailure e , Handler $ \(e :: WS.ConnectionException) -> connectionFailure e @@ -101,16 +131,22 @@ submitAllOgmios url txStream = ] case result of Left err -> liftTxGenError err - Right (sent, failed) -> + Right (sent, failed) -> do traceDebug $ "Ogmios: done, " ++ show sent ++ " sent, " ++ show failed ++ " failed" + -- a rejected transaction is a functional failure; report it as + -- such so the run's exit code can be trusted + when (failed > 0) $ liftTxGenError $ TxGenError $ + "Ogmios: " ++ show failed ++ " of " ++ show (sent + failed) + ++ " transactions were rejected" where connectionFailure :: Show e => e -> IO (Either TxGenError (Int, Int)) connectionFailure e = return $ Left $ TxGenError $ "Ogmios connection failure: " ++ show e submitLoop :: IsShelleyBasedEra era - => (Int -> Text -> Value -> IO ()) + => OnRejection + -> (Int -> Text -> Value -> IO ()) -> WS.Connection -> TxStream IO era -> Int -> Int -> Int -> IO (Either TxGenError (Int, Int)) -submitLoop traceSubmitFail conn stream reqId sent failed = do +submitLoop onRejection traceSubmitFail conn stream reqId sent failed = do step <- Streaming.inspect stream case step of Left () -> return $ Right (sent, failed) @@ -133,9 +169,14 @@ submitLoop traceSubmitFail conn stream reqId sent failed = do ++ ", got " ++ show respId ++ " (" ++ describe result ++ ")" | OgmiosError code errMsg errData <- result -> do traceSubmitFail code errMsg errData - submitLoop traceSubmitFail conn rest (reqId + 1) sent (failed + 1) + case onRejection of + AbortOnRejection -> return $ Left $ TxGenError $ + "Ogmios: transaction rejected: " ++ Text.unpack errMsg + ++ " (code " ++ show code ++ ")" + ContinueOnRejection -> + submitLoop onRejection traceSubmitFail conn rest (reqId + 1) sent (failed + 1) | otherwise -> - submitLoop traceSubmitFail conn rest (reqId + 1) (sent + 1) failed + submitLoop onRejection traceSubmitFail conn rest (reqId + 1) (sent + 1) failed where describe (OgmiosSuccess txId) = "success, tx " ++ Text.unpack txId describe (OgmiosError _ msg _) = "error: " ++ Text.unpack msg From eec146cda828f3e38b8c8dddafc19310a172edf8 Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Tue, 23 Jun 2026 13:38:15 +0200 Subject: [PATCH 08/18] Re-structure changelog entries --- bench/tx-generator/CHANGELOG.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/bench/tx-generator/CHANGELOG.md b/bench/tx-generator/CHANGELOG.md index efac6707e95..be4b78c6610 100644 --- a/bench/tx-generator/CHANGELOG.md +++ b/bench/tx-generator/CHANGELOG.md @@ -2,8 +2,24 @@ ## 2.17 -- Jun 2026 -* New `Ogmios` submit mode, selected by the optional `ogmiosUrl` config key: when set, all transaction submission (genesis fund import, UTxO splitting, and the benchmarking phase) goes through an Ogmios WebSocket endpoint as JSON-RPC 2.0 `submitTransaction` calls instead of the local socket / Node-to-Node protocols. This is a functional submission transport without TPS pacing or benchmark metrics, so the high-level config compiler only accepts `ogmiosUrl` together with `debugMode: true`. Rejected transaction submitted through Ogmios are traced through the regular tracing pipeline, including the failure detail reported by Ogmios, and make the run fail: setup phases abort at the first rejected transaction, and the benchmarking phase exits non-zero if any transaction was rejected. -* Scripts that never start the node-to-node benchmark machinery (such as `debugMode: true` runs and low-level `json` scripts without a `Benchmark` submit phase) now exit cleanly instead of crashing at shutdown with "AsyncBenchmarkControl absent". +* **New `Ogmios` submit mode** — send transactions over an Ogmios WebSocket + instead of the local socket / Node-to-Node protocols. + * Turn it on with the optional `ogmiosUrl` key, e.g. + `"ogmiosUrl": "ws://127.0.0.1:1337"`. + * It reroutes **every** phase, but only tx submission: genesis fund import, UTxO splitting, and benchmarking. + * Each transaction is sent as a JSON-RPC 2.0 `submitTransaction` call. + * It is a **functional transport, not a benchmark**: no TPS pacing, no metrics. + * So it requires `debugMode: true`. The compiler rejects `ogmiosUrl` 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 Ogmios 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 From 636343601fc4807142977c7b01e3b6ce794e004a Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Tue, 23 Jun 2026 17:52:47 +0200 Subject: [PATCH 09/18] Improve haddock structure for Ogmios module --- .../src/Cardano/Benchmarking/Script/Ogmios.hs | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs index b86123c7883..a23730cae42 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -6,32 +6,51 @@ Module : Cardano.Benchmarking.Script.Ogmios Description : Submit transactions through an Ogmios endpoint. -This is a functional submission transport, not a benchmarking one: it -submits strictly one transaction per round trip over a single WebSocket -connection, ignores any TPS pacing, and reports no submission metrics -beyond a sent/failed count traced at debug level. The high-level config -compiler therefore only selects it together with @debugMode: true@. - -Throughput is bounded by the round-trip time to the endpoint, since at -most one request is in flight at a time. Ogmios itself supports -pipelining 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. - -Note that only transaction submission goes through Ogmios. Everything -else still talks to the local node directly: protocol-parameter and era -queries as well as protocol startup use the node socket and config -file, so tx-generator keeps requiring local node access even when -submitting through a remote endpoint. - -Rejected transactions make the run fail. Streams of chained setup -transactions (genesis import, splitting) abort at the first rejection — -everything after it would be doomed anyway — while the benchmarking -phase's stream of independent transactions is submitted to the end and -the action fails afterwards if anything was rejected. Either way the -process exits non-zero, so exit codes can be trusted in scripts. +Submit transactions through an WebSocket +endpoint, 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 Ogmios requires @debugMode: true@__ — the high-level config +compiler rejects an @ogmiosUrl@ 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 ( OgmiosResult (..) From 904a16443e2510f11a651481f01d27329d9f7f8b Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Tue, 23 Jun 2026 18:50:28 +0200 Subject: [PATCH 10/18] Small lints --- .../src/Cardano/Benchmarking/Script/Ogmios.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs index a23730cae42..4e39ee6f445 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -80,6 +80,7 @@ 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.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as Text @@ -202,12 +203,12 @@ submitLoop onRejection traceSubmitFail conn stream reqId sent failed = do parseOgmiosUrl :: String -> Either String (String, Int, String) parseOgmiosUrl urlStr = do - uri <- note ("Invalid Ogmios URL: " ++ urlStr) $ parseURI urlStr + uri <- maybeToEither ("Invalid Ogmios URL: " ++ urlStr) $ parseURI urlStr -- 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 <- note ("No authority in Ogmios URL: " ++ urlStr) $ uriAuthority uri + 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 @@ -220,9 +221,8 @@ parseOgmiosUrl urlStr = do p -> p return (uriRegName auth, port, path) where - note msg = maybe (Left msg) Right parsePort p = case readMaybe p of - Just n | n >= 1 && n <= 65535 -> Right n + Just n | n >= 1 && n <= 65_535 -> Right n _ -> Left $ "Invalid port in Ogmios URL: " ++ urlStr mkSubmitRequest :: IsShelleyBasedEra era => Tx era -> Int -> Value @@ -257,7 +257,7 @@ parseOgmiosResponse bs = Just resultVal -> do txId <- Aeson.withObject "result" (\r -> do txObj <- r .: "transaction" - Aeson.withObject "transaction" (\t -> t .: "id") txObj + Aeson.withObject "transaction" (.: "id") txObj ) resultVal return $ OgmiosSuccess txId Nothing -> do From 1f74ba9ea170450386cdfb03497c4cfefe14c2f9 Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Tue, 23 Jun 2026 21:30:19 +0200 Subject: [PATCH 11/18] Fix ogmios resolution in test-ogmios.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues kept the script from resolving and launching ogmios: 1. Submodules. Resolving ogmios as `github:IntersectMBO/ogmios/` downloads a GitHub tarball, which omits the hjsonpointer, hjsonschema and wai-routes git submodules that ogmios' cabal.project depends on. haskell.nix then fails plan-to-nix with "modules/hjsonpointer does not contain any .cabal file". This is platform-independent (it fails on x86_64 too). Use the `git+https://…?submodules=1` fetcher, which actually pulls the submodules. Note that `github:…?submodules=1` is silently ignored and the flake's own `self.submodules = true` does not rescue the tarball path either. 2. Default ref. The cardano-node tools are derived from a cardano-node input of the ogmios flake, which only the testnet-tx-gen-tests branch declares; ogmios master has no such input, so the old default produced 'github:null/null/null' and a 404. Default to the branch that actually provides it. Also guard the lookup so a missing input fails with a clear message instead of a cryptic fetch error. 3. Binary path. The ogmios line captured the nix output *directory* and ran it directly ("Is a directory"); the cardano-* lines already append /bin/. Append /bin/ogmios to match. --- bench/tx-generator/test-ogmios.sh | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/bench/tx-generator/test-ogmios.sh b/bench/tx-generator/test-ogmios.sh index cdb62358e13..40e02a54e60 100755 --- a/bench/tx-generator/test-ogmios.sh +++ b/bench/tx-generator/test-ogmios.sh @@ -10,9 +10,18 @@ # 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:-github:IntersectMBO/ogmios/master}" +OGMIOS_FLAKE="${1:-git+https://github.com/IntersectMBO/ogmios?submodules=1&ref=testnet-tx-gen-tests}" TESTNET_MAGIC=42 OGMIOS_PORT=11337 @@ -29,13 +38,23 @@ echo "=== Resolving nix packages ===" echo " ogmios flake: $OGMIOS_FLAKE" # ogmios: packages.ogmios-exe -OGMIOS=$(nix build "${OGMIOS_FLAKE}#ogmios" --no-link --print-out-paths) +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) +# 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 From 1c2ad4bcdca481a0716fb0d17d25f6671d0d7b5b Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Sat, 27 Jun 2026 00:39:10 +0200 Subject: [PATCH 12/18] Generalise tx-generator submission over the endpoint type Submission to a remote endpoint is no longer Ogmios-specific. A new backend-agnostic transport interface sits behind the submission path, with Ogmios as the first (and currently only) backend. * Add Cardano.Benchmarking.Script.Submission: a SubmitTransport record (one submitOne action), the transport-agnostic submitLoop (rejection policy, tally, tracing, exit code) and the rejection-policy helpers. The backend's rejection type is kept abstract and only rendered for tracing (via Pretty), so no transport detail leaks into the loop. * Cardano.Benchmarking.Script.Ogmios becomes that interface's Ogmios backend: withOgmiosTransport builds a SubmitTransport over a WebSocket connection; its rejection type and protocol-fault handling stay internal to the module. * Config: replace the ogmiosUrl key with submissionEndpointType and submissionEndpointURI, which must be set together; SubmitMode's Ogmios constructor becomes SubmitToEndpoint SubmissionEndpointType. * Update README, CHANGELOG and test-ogmios.sh accordingly. --- bench/tx-generator/CHANGELOG.md | 17 +- bench/tx-generator/README.md | 24 ++- .../src/Cardano/Benchmarking/Compiler.hs | 44 ++-- .../src/Cardano/Benchmarking/Script/Core.hs | 9 +- .../src/Cardano/Benchmarking/Script/Ogmios.hs | 194 ++++++++---------- .../Cardano/Benchmarking/Script/Submission.hs | 125 +++++++++++ .../src/Cardano/Benchmarking/Script/Types.hs | 8 +- .../Cardano/TxGenerator/Setup/NixService.hs | 20 +- bench/tx-generator/test-ogmios.sh | 10 +- bench/tx-generator/tx-generator.cabal | 1 + 10 files changed, 301 insertions(+), 151 deletions(-) create mode 100644 bench/tx-generator/src/Cardano/Benchmarking/Script/Submission.hs diff --git a/bench/tx-generator/CHANGELOG.md b/bench/tx-generator/CHANGELOG.md index be4b78c6610..91ceabe2622 100644 --- a/bench/tx-generator/CHANGELOG.md +++ b/bench/tx-generator/CHANGELOG.md @@ -2,18 +2,21 @@ ## 2.17 -- Jun 2026 -* **New `Ogmios` submit mode** — send transactions over an Ogmios WebSocket - instead of the local socket / Node-to-Node protocols. - * Turn it on with the optional `ogmiosUrl` key, e.g. - `"ogmiosUrl": "ws://127.0.0.1:1337"`. +* **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 `submissionEndpointType` and + `submissionEndpointURI` keys, which must be set together, e.g. + `"submissionEndpointType": "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. - * Each transaction is sent as a JSON-RPC 2.0 `submitTransaction` call. * It is a **functional transport, not a benchmark**: no TPS pacing, no metrics. - * So it requires `debugMode: true`. The compiler rejects `ogmiosUrl` on its own. + * 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 Ogmios reports. + * 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 diff --git a/bench/tx-generator/README.md b/bench/tx-generator/README.md index 7be7bb17204..11644781075 100644 --- a/bench/tx-generator/README.md +++ b/bench/tx-generator/README.md @@ -58,7 +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 | -| `ogmiosUrl` | Optional ogmios endpoint (`ws://host:port`) for tx submission | No | +| `submissionEndpointType` | Optional submission endpoint kind (`Ogmios`); set together with `submissionEndpointURI` | No | +| `submissionEndpointURI` | Optional submission endpoint URI (e.g. `ws://host:port`); set together with `submissionEndpointType` | No | ### Transaction Settings @@ -112,18 +113,29 @@ 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 Ogmios +### Submitting through a remote endpoint -Setting `ogmiosUrl` (e.g. `"ogmiosUrl": "ws://127.0.0.1:1337"`) reroutes the submission of **every** phase — genesis fund import, UTxO splitting, and the final phase — through that [Ogmios](https://ogmios.dev) endpoint, as JSON-RPC 2.0 `submitTransaction` calls over a WebSocket (only plain `ws://` is supported). +Setting a submission endpoint (`submissionEndpointType` 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 Ogmios) and a final sent/failed count go to the trace output. +- 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 `ogmiosUrl` and rejects the config otherwise. +- 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 Ogmios. +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 +"submissionEndpointType": "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) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs index 6b4c0bf80f8..06617969925 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs @@ -198,20 +198,20 @@ benchmarkingPhase wallet collateralWallet = do inputs <- askNixOption _nix_inputs_per_tx outputs <- askNixOption _nix_outputs_per_tx txParams <- askNixOption txGenTxParams - ogmiosUrl <- askNixOption _nix_ogmiosUrl + endpoint <- resolveSubmissionEndpoint doneWallet <- newWallet "done_wallet" - -- Ogmios 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 (ogmiosUrl, debugMode) of - (Just url, True) -> pure $ Ogmios url - (Just _, False) -> throwCompileError $ SomeCompilerError - "ogmiosUrl is a functional submission transport: it ignores tps and \ - \targetNodes and produces no benchmark metrics. Set debugMode: true \ - \to acknowledge this, or remove ogmiosUrl to run a real benchmark." - (Nothing, True) -> pure LocalSocket - (Nothing, False) -> pure $ Benchmark targetNodes tps txCount + -- 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 generator = Take txCount $ Cycle $ NtoM wallet payMode inputs outputs (Just $ txParamAddTxSize txParams) collateralWallet @@ -261,9 +261,21 @@ askNixOption :: (NixServiceOptions -> v) -> Compiler v askNixOption = asks getSetupSubmitMode :: Compiler SubmitMode -getSetupSubmitMode = do - ogmiosUrl <- askNixOption _nix_ogmiosUrl - return $ maybe LocalSocket Ogmios ogmiosUrl +getSetupSubmitMode = + maybe LocalSocket (uncurry SubmitToEndpoint) <$> resolveSubmissionEndpoint + +-- | Resolve the configured submission endpoint, requiring its type and URI to +-- be set together (or both omitted). +resolveSubmissionEndpoint :: Compiler (Maybe (SubmissionEndpointType, String)) +resolveSubmissionEndpoint = do + mType <- askNixOption _nix_submissionEndpointType + mUri <- askNixOption _nix_submissionEndpointURI + case (mType, mUri) of + (Nothing, Nothing) -> pure Nothing + (Just t, Just u) -> pure $ Just (t, u) + _ -> throwCompileError $ SomeCompilerError + "submissionEndpointType 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/Core.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs index 4f1c832c9a2..d3040b316d5 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs @@ -30,7 +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 Ogmios +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 @@ -245,7 +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 - Ogmios url -> Ogmios.submitAllOgmios (Ogmios.onRejectionFor generator) url txStream + SubmitToEndpoint endpointType 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 index 4e39ee6f445..828149b4d29 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -4,10 +4,11 @@ {-| Module : Cardano.Benchmarking.Script.Ogmios -Description : Submit transactions through an Ogmios endpoint. +Description : Ogmios backend for the transaction submission transport. -Submit transactions through an WebSocket -endpoint, as JSON-RPC 2.0 @submitTransaction@ calls. +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) @@ -17,8 +18,9 @@ This is a __functional submission transport, not a benchmarking one__: * TPS pacing is ignored; * no submission metrics — only a sent/failed count, traced at debug level. -__Using Ogmios requires @debugMode: true@__ — the high-level config -compiler rejects an @ogmiosUrl@ config that does not also set it. +__Using a submission endpoint requires @debugMode: true@__ — the high-level +config compiler rejects a @submissionEndpointURI@ config that does not also +set it. == Throughput @@ -53,27 +55,18 @@ exit codes can be trusted in scripts. /How/ it fails depends on the phase: then the action fails afterwards if anything was rejected. -} module Cardano.Benchmarking.Script.Ogmios - ( OgmiosResult (..) - , OnRejection (..) - , onRejectionFor - , parseOgmiosResponse - , parseOgmiosUrl - , submitAllOgmios + ( parseOgmiosUrl + , withOgmiosTransport ) where import Cardano.Api (IsShelleyBasedEra, Tx, serialiseToCBOR) -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.Benchmarking.Script.Submission (SubmitTransport (..)) import Cardano.TxGenerator.Types (TxGenError (..)) import Prelude -import Control.Exception (Handler (..), IOException, catches) +import Control.Exception (Exception, Handler (..), IOException, catches, throwIO) import Control.Monad (unless, when) import Data.Aeson (Value (..), object, (.:), (.:?), (.=)) import qualified Data.Aeson as Aeson @@ -81,17 +74,17 @@ 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 (parseURI, uriAuthority, uriPath, uriPort, uriRegName, uriScheme) import qualified Network.WebSockets as WS +import Prettyprinter (Pretty (..), parens, (<+>)) import System.Timeout (timeout) import Text.Read (readMaybe) -import Streaming - -- | The port Ogmios listens on by default. defaultOgmiosPort :: Int defaultOgmiosPort = 1337 @@ -101,102 +94,79 @@ defaultOgmiosPort = 1337 responseTimeout :: Int responseTimeout = 90_000_000 --- | How to proceed when Ogmios 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. The benchmarking phase's 'NtoM' stream --- consists of mutually independent transactions, so submit it to the end --- and let the final tally decide. -onRejectionFor :: Generator -> OnRejection -onRejectionFor generator = case generator of - NtoM {} -> ContinueOnRejection - Take _ g -> onRejectionFor g - Cycle g -> onRejectionFor g - Sequence gs - | any ((ContinueOnRejection ==) . onRejectionFor) gs -> ContinueOnRejection - _ -> AbortOnRejection - -submitAllOgmios :: forall era. IsShelleyBasedEra era - => OnRejection -> String -> TxStream IO era -> ActionM () -submitAllOgmios onRejection url txStream = +-- | 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 !Text !Value + +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 + => String + -> (SubmitTransport era OgmiosRejection -> IO (Either TxGenError a)) + -> IO (Either TxGenError a) +withOgmiosTransport url use = case parseOgmiosUrl url of - Left err -> liftTxGenError $ TxGenError err - Right (host, port, path) -> do - tracers <- getBenchTracers - -- per-transaction rejections must reach the trace stream like every - -- other submission event, not stdout; the loop itself runs in plain - -- IO under the WebSocket client, so hand it a prebuilt trace action - let traceSubmitFail :: Int -> Text -> Value -> IO () - traceSubmitFail code msg errData = - traceWith (btTxSubmit_ tracers) $ TraceBenchTxSubError $ Text.concat - [ "Ogmios submit failed: ", msg - , " (code ", Text.pack (show code), ")" - , case errData of - Null -> "" - dat -> ", details: " - <> Text.decodeUtf8 (LBS.toStrict (Aeson.encode dat)) - ] - traceDebug $ "Ogmios: connecting to " ++ url - result <- liftIO $ - WS.runClient host port path (\conn -> submitLoop onRejection traceSubmitFail conn txStream 0 0 0) - `catches` - [ Handler $ \(e :: WS.HandshakeException) -> connectionFailure e - , Handler $ \(e :: WS.ConnectionException) -> connectionFailure e - , Handler $ \(e :: IOException) -> connectionFailure e - ] - case result of - Left err -> liftTxGenError err - Right (sent, failed) -> do - traceDebug $ "Ogmios: done, " ++ show sent ++ " sent, " ++ show failed ++ " failed" - -- a rejected transaction is a functional failure; report it as - -- such so the run's exit code can be trusted - when (failed > 0) $ liftTxGenError $ TxGenError $ - "Ogmios: " ++ show failed ++ " of " ++ show (sent + failed) - ++ " transactions were rejected" + 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 - connectionFailure :: Show e => e -> IO (Either TxGenError (Int, Int)) + 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 -submitLoop :: IsShelleyBasedEra era - => OnRejection - -> (Int -> Text -> Value -> IO ()) - -> WS.Connection -> TxStream IO era -> Int -> Int -> Int -> IO (Either TxGenError (Int, Int)) -submitLoop onRejection traceSubmitFail conn stream reqId sent failed = 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 - WS.sendTextData conn $ Aeson.encode (mkSubmitRequest tx reqId) - mResp <- timeout responseTimeout $ WS.receiveData conn - case mResp of - Nothing -> return $ Left $ TxGenError $ - "Ogmios: no response to request " ++ show reqId - ++ " within " ++ show (responseTimeout `div` 1_000_000) ++ "s" - Just resp -> case parseOgmiosResponse resp of - Left parseErr -> - return $ Left $ TxGenError $ "Ogmios response parse error: " ++ parseErr - Right (respId, result) - -- a mismatched (or null) id is a protocol-level fault, not a - -- transaction rejection: the connection is out of sync, so stop - | respId /= Just reqId -> return $ Left $ TxGenError $ - "Ogmios: response id mismatch: expected " ++ show reqId - ++ ", got " ++ show respId ++ " (" ++ describe result ++ ")" - | OgmiosError code errMsg errData <- result -> do - traceSubmitFail code errMsg errData - case onRejection of - AbortOnRejection -> return $ Left $ TxGenError $ - "Ogmios: transaction rejected: " ++ Text.unpack errMsg - ++ " (code " ++ show code ++ ")" - ContinueOnRejection -> - submitLoop onRejection traceSubmitFail conn rest (reqId + 1) sent (failed + 1) - | otherwise -> - submitLoop onRejection traceSubmitFail conn rest (reqId + 1) (sent + 1) failed +-- | 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) + WS.sendTextData conn $ Aeson.encode (mkSubmitRequest tx reqId) + mResp <- timeout responseTimeout $ WS.receiveData conn + case mResp of + Nothing -> throwIO $ OgmiosProtocolError $ + "no response to request " ++ show reqId + ++ " 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 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..f6f401b51ee --- /dev/null +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Submission.hs @@ -0,0 +1,125 @@ +{-# 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. The benchmarking phase's 'NtoM' stream consists +-- of mutually independent transactions, so submit it to the end and let the +-- final tally decide. +onRejectionFor :: Generator -> OnRejection +onRejectionFor generator = case generator of + NtoM {} -> ContinueOnRejection + Take _ g -> onRejectionFor g + Cycle g -> onRejectionFor g + Sequence gs + | any ((ContinueOnRejection ==) . onRejectionFor) gs -> ContinueOnRejection + _ -> 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 1df6590c8f8..c0c4d74abc8 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs @@ -32,8 +32,9 @@ module Cardano.Benchmarking.Script.Types ( , ProtocolParametersSource(QueryLocalNode, UseLocalProtocolFile) , ScriptBudget(AutoScript, StaticScriptBudget) , ScriptSpec(..) + , SubmissionEndpointType(..) , SubmitMode(Benchmark, DiscardTX, DumpToFile, LocalSocket, - NodeToNode, Ogmios) + NodeToNode, SubmitToEndpoint) , TargetNodes , TxList(..) ) where @@ -44,7 +45,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 (NodeDescription, + SubmissionEndpointType (..)) import Cardano.TxGenerator.Types import Prelude @@ -185,7 +187,7 @@ data SubmitMode where DumpToFile :: !FilePath -> SubmitMode DiscardTX :: SubmitMode NodeToNode :: NonEmpty NodeIPv4Address -> SubmitMode --deprecated - Ogmios :: !String -> SubmitMode + SubmitToEndpoint :: !SubmissionEndpointType -> !String -> 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 20962e26e6c..dddd361a762 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,7 @@ module Cardano.TxGenerator.Setup.NixService ( NixServiceOptions (..) , NodeDescription (..) + , SubmissionEndpointType (..) , defaultKeepaliveTimeout , getKeepaliveTimeout , getNodeAlias @@ -59,11 +61,27 @@ data NixServiceOptions = NixServiceOptions { , _nix_sigKey :: SigningKeyFile In , _nix_localNodeSocketPath :: String , _nix_targetNodes :: NonEmpty NodeDescription - , _nix_ogmiosUrl :: Maybe String + , _nix_submissionEndpointType :: Maybe SubmissionEndpointType + , _nix_submissionEndpointURI :: Maybe String } deriving (Show, Eq) deriving instance Generic NixServiceOptions +-- | Which kind of endpoint 'submissionEndpointURI' addresses. Currently only +-- Ogmios is supported; this is the extension point for further submission +-- backends. +data SubmissionEndpointType + = Ogmios + deriving (Show, Eq, Generic) + +instance FromJSON SubmissionEndpointType where + parseJSON = withText "SubmissionEndpointType" $ \t -> case t of + "Ogmios" -> pure Ogmios + _ -> fail $ "unknown submissionEndpointType: " ++ show t + +instance ToJSON SubmissionEndpointType where + toJSON Ogmios = String "Ogmios" + -- 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 index 40e02a54e60..d9bff8e35cc 100755 --- a/bench/tx-generator/test-ogmios.sh +++ b/bench/tx-generator/test-ogmios.sh @@ -170,9 +170,10 @@ if [ -z "$SIG_KEY" ]; then fi CONFIG_FILE="$WORKDIR/tx-generator-config.json" -# debugMode is mandatory alongside ogmiosUrl: the Ogmios submit mode is a -# functional transport without pacing or metrics, and the config compiler -# rejects it for benchmark (non-debug) runs. +# 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. submissionEndpointType and submissionEndpointURI +# must be set together. cat > "$CONFIG_FILE" << EOF { "tx_count": 10, @@ -188,7 +189,8 @@ cat > "$CONFIG_FILE" << EOF "debugMode": true, "plutus": null, "sigKey": "$SIG_KEY", - "ogmiosUrl": "ws://127.0.0.1:$OGMIOS_PORT" + "submissionEndpointType": "Ogmios", + "submissionEndpointURI": "ws://127.0.0.1:$OGMIOS_PORT" } EOF diff --git a/bench/tx-generator/tx-generator.cabal b/bench/tx-generator/tx-generator.cabal index e5ab7d93660..48b25d2e0ca 100644 --- a/bench/tx-generator/tx-generator.cabal +++ b/bench/tx-generator/tx-generator.cabal @@ -72,6 +72,7 @@ library 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 From 7716d4b7a954eddcaf06c4e8682453397b7c11df Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Sat, 4 Jul 2026 02:26:35 +0200 Subject: [PATCH 13/18] Put the Ogmios send under the round-trip timeout WS.sendTextData could block indefinitely if the peer stalls and TCP buffers fill up; only the receive was guarded. Send and receive now share the single responseTimeout deadline, and the timeout error message covers both stall modes. --- .../src/Cardano/Benchmarking/Script/Ogmios.hs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs index 828149b4d29..8c61f464c4f 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -89,8 +89,9 @@ import Text.Read (readMaybe) defaultOgmiosPort :: Int defaultOgmiosPort = 1337 --- | Per-request response timeout in microseconds. Generous, because the --- node may hold a submission back while its mempool is saturated. +-- | 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 @@ -151,12 +152,15 @@ ogmiosSubmitOne => WS.Connection -> IORef Int -> Tx era -> IO (Either OgmiosRejection ()) ogmiosSubmitOne conn reqIdRef tx = do reqId <- atomicModifyIORef' reqIdRef $ \n -> (n + 1, n) - WS.sendTextData conn $ Aeson.encode (mkSubmitRequest tx reqId) - mResp <- timeout responseTimeout $ WS.receiveData conn + -- 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 $ - "no response to request " ++ show reqId - ++ " within " ++ show (responseTimeout `div` 1_000_000) ++ "s" + "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) From cf9b22943d1fb35def8a8fafacdea7d9b926703c Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Sat, 4 Jul 2026 02:26:36 +0200 Subject: [PATCH 14/18] Collapse parseOgmiosResponse's decode case into bind The Left/Right case analysis is just the Either monad's bind. Note the suggested 'fmap' variant would not typecheck (it nests the Eithers); (>>=) is the correct collapse. --- bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs index 8c61f464c4f..8057f19afc6 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -220,9 +220,7 @@ data OgmiosResult -- mirrored request id alongside the result. parseOgmiosResponse :: LBS.ByteString -> Either String (Maybe Int, OgmiosResult) parseOgmiosResponse bs = - case Aeson.eitherDecode bs of - Left err -> Left err - Right val -> Aeson.parseEither parseResponse val + Aeson.eitherDecode bs >>= Aeson.parseEither parseResponse where parseResponse = Aeson.withObject "OgmiosResponse" $ \obj -> do respId <- obj .:? "id" From 6ab928516fe8ec51ee4bcd8bb794a308b784b654 Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Sat, 4 Jul 2026 02:26:37 +0200 Subject: [PATCH 15/18] Extract parseSuccess/parseError from parseOgmiosResponse The response parser had four levels of rightward drift (case on the result, then nested withObject calls, then the error branch). The success and error branches are now top-level helpers, as suggested in review. --- .../src/Cardano/Benchmarking/Script/Ogmios.hs | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs index 8057f19afc6..d9307d1d2e2 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -225,19 +225,21 @@ parseOgmiosResponse bs = parseResponse = Aeson.withObject "OgmiosResponse" $ \obj -> do respId <- obj .:? "id" mResult <- obj .:? "result" - result <- case mResult of - Just resultVal -> do - txId <- Aeson.withObject "result" (\r -> do - txObj <- r .: "transaction" - Aeson.withObject "transaction" (.: "id") txObj - ) resultVal - return $ OgmiosSuccess txId - Nothing -> do - errVal <- obj .: "error" - Aeson.withObject "error" (\errObj -> do - code <- errObj .: "code" - msg <- errObj .: "message" - dat <- errObj .:? "data" - return $ OgmiosError code msg (fromMaybe Null dat) - ) errVal + 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 From 0a2d98abf0cb4d23d25d513ed4e4dbee8c7f23be Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Sat, 4 Jul 2026 02:26:38 +0200 Subject: [PATCH 16/18] Always abort on rejection for Sequence generators A Sequence can mix chained and independent sub-generators; deciding the policy once for the whole stream meant a raw script's Sequence [Split .., NtoM ..] became ContinueOnRejection, so a rejected Split did not abort and its doomed descendants were submitted anyway. Abort is the safe unconditional choice: either policy fails the run on any rejection, the policy only decides whether to keep submitting after the first one. The high-level compiler never emits Sequence for the benchmarking phase, so this changes nothing for high-level configs. --- .../Cardano/Benchmarking/Script/Submission.hs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Submission.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Submission.hs index f6f401b51ee..a2ca726950c 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Submission.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Submission.hs @@ -56,17 +56,21 @@ data OnRejection -- | Setup-phase generators (genesis import, splitting) emit chains of -- interdependent transactions: once one is rejected, everything after it is --- doomed, so abort right away. The benchmarking phase's 'NtoM' stream consists --- of mutually independent transactions, so submit it to the end and let the --- final tally decide. +-- 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 - Sequence gs - | any ((ContinueOnRejection ==) . onRejectionFor) gs -> ContinueOnRejection - _ -> AbortOnRejection + 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 From cf2c1a6357b22c0c1822e533009a67eedc1bc190 Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Sat, 4 Jul 2026 02:26:39 +0200 Subject: [PATCH 17/18] Parse the submission endpoint URI at the config boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The endpoint URI travelled as a raw String all the way to the Ogmios backend, which parsed it at connection time — so a malformed URI only surfaced mid-run. It is now decoded into an EndpointUri (a newtype over Network.URI.URI whose FromJSON only accepts absolute URIs), giving both the nix service config and raw JSON scripts decode-time validation. Backend-specific validation stays in the backend: parseOgmiosUrl takes the well-formed URI and still checks the ws:// scheme, host presence and port range, and applies Ogmios defaults. The JSON wire format is unchanged (a plain string). --- .../src/Cardano/Benchmarking/Compiler.hs | 5 +++-- .../src/Cardano/Benchmarking/Script/Core.hs | 2 +- .../src/Cardano/Benchmarking/Script/Ogmios.hs | 19 +++++++++++------- .../src/Cardano/Benchmarking/Script/Types.hs | 5 +++-- .../Cardano/TxGenerator/Setup/NixService.hs | 20 ++++++++++++++++++- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs index 06617969925..72461a18268 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs @@ -265,8 +265,9 @@ getSetupSubmitMode = maybe LocalSocket (uncurry SubmitToEndpoint) <$> resolveSubmissionEndpoint -- | Resolve the configured submission endpoint, requiring its type and URI to --- be set together (or both omitted). -resolveSubmissionEndpoint :: Compiler (Maybe (SubmissionEndpointType, String)) +-- 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 (SubmissionEndpointType, EndpointUri)) resolveSubmissionEndpoint = do mType <- askNixOption _nix_submissionEndpointType mUri <- askNixOption _nix_submissionEndpointURI diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs index d3040b316d5..90b1f357c28 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Core.hs @@ -246,7 +246,7 @@ 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 uri -> case endpointType of + SubmitToEndpoint endpointType (EndpointUri uri) -> case endpointType of Ogmios -> Submission.runSubmitTransport (Submission.onRejectionFor generator) (OgmiosBackend.withOgmiosTransport uri) diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs index d9307d1d2e2..4a920665977 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -79,7 +79,8 @@ import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text -import Network.URI (parseURI, uriAuthority, uriPath, uriPort, uriRegName, uriScheme) +import Network.URI (URI, uriAuthority, uriPath, uriPort, uriRegName, uriScheme, + uriToString) import qualified Network.WebSockets as WS import Prettyprinter (Pretty (..), parens, (<+>)) import System.Timeout (timeout) @@ -121,11 +122,11 @@ instance Exception OgmiosProtocolError -- surfaced as a 'Left' 'TxGenError'. withOgmiosTransport :: forall era a. IsShelleyBasedEra era - => String + => URI -> (SubmitTransport era OgmiosRejection -> IO (Either TxGenError a)) -> IO (Either TxGenError a) -withOgmiosTransport url use = - case parseOgmiosUrl url of +withOgmiosTransport uri use = + case parseOgmiosUrl uri of Left err -> return $ Left $ TxGenError err Right (host, port, path) -> WS.runClient host port path runWithConn @@ -175,9 +176,12 @@ ogmiosSubmitOne conn reqIdRef tx = do describe (OgmiosSuccess txId) = "success, tx " ++ Text.unpack txId describe (OgmiosError _ msg _) = "error: " ++ Text.unpack msg -parseOgmiosUrl :: String -> Either String (String, Int, String) -parseOgmiosUrl urlStr = do - uri <- maybeToEither ("Invalid Ogmios URL: " ++ urlStr) $ parseURI urlStr +-- | 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:") $ @@ -195,6 +199,7 @@ parseOgmiosUrl urlStr = do 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 diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs index c0c4d74abc8..94fd1f55a06 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) @@ -45,7 +46,7 @@ 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, SubmissionEndpointType (..)) import Cardano.TxGenerator.Types @@ -187,7 +188,7 @@ data SubmitMode where DumpToFile :: !FilePath -> SubmitMode DiscardTX :: SubmitMode NodeToNode :: NonEmpty NodeIPv4Address -> SubmitMode --deprecated - SubmitToEndpoint :: !SubmissionEndpointType -> !String -> SubmitMode + SubmitToEndpoint :: !SubmissionEndpointType -> !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 dddd361a762..33c10705c48 100644 --- a/bench/tx-generator/src/Cardano/TxGenerator/Setup/NixService.hs +++ b/bench/tx-generator/src/Cardano/TxGenerator/Setup/NixService.hs @@ -13,6 +13,7 @@ module Cardano.TxGenerator.Setup.NixService ( NixServiceOptions (..) , NodeDescription (..) , SubmissionEndpointType (..) + , EndpointUri (..) , defaultKeepaliveTimeout , getKeepaliveTimeout , getNodeAlias @@ -39,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 { @@ -62,7 +65,7 @@ data NixServiceOptions = NixServiceOptions { , _nix_localNodeSocketPath :: String , _nix_targetNodes :: NonEmpty NodeDescription , _nix_submissionEndpointType :: Maybe SubmissionEndpointType - , _nix_submissionEndpointURI :: Maybe String + , _nix_submissionEndpointURI :: Maybe EndpointUri } deriving (Show, Eq) deriving instance Generic NixServiceOptions @@ -82,6 +85,21 @@ instance FromJSON SubmissionEndpointType where instance ToJSON SubmissionEndpointType 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 { From 8c635b0bb1ff5f0632fc60f84be37c89b559a20d Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Sat, 4 Jul 2026 02:26:39 +0200 Subject: [PATCH 18/18] Address doc review: field haddocks, rename SubmissionEndpointType Add per-field haddocks to OgmiosRejection and OgmiosResult (the bare Int/Text/Value fields were undocumented), document the SubmitToEndpoint constructor, and rename SubmissionEndpointType to SubmissionEndpointProtocol - the values name the protocol spoken to the endpoint, not the endpoint itself. The rename includes the JSON config key (submissionEndpointType -> submissionEndpointProtocol) and the mentions in README, CHANGELOG and test-ogmios.sh. --- bench/tx-generator/CHANGELOG.md | 4 ++-- bench/tx-generator/README.md | 8 ++++---- .../src/Cardano/Benchmarking/Compiler.hs | 6 +++--- .../src/Cardano/Benchmarking/Script/Ogmios.hs | 17 +++++++++++++--- .../src/Cardano/Benchmarking/Script/Types.hs | 8 +++++--- .../Cardano/TxGenerator/Setup/NixService.hs | 20 +++++++++---------- bench/tx-generator/test-ogmios.sh | 4 ++-- 7 files changed, 40 insertions(+), 27 deletions(-) diff --git a/bench/tx-generator/CHANGELOG.md b/bench/tx-generator/CHANGELOG.md index 91ceabe2622..f3594f28b16 100644 --- a/bench/tx-generator/CHANGELOG.md +++ b/bench/tx-generator/CHANGELOG.md @@ -5,9 +5,9 @@ * **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 `submissionEndpointType` and + * Turn it on with the optional `submissionEndpointProtocol` and `submissionEndpointURI` keys, which must be set together, e.g. - `"submissionEndpointType": "Ogmios", "submissionEndpointURI": "ws://127.0.0.1:1337"`. + `"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. diff --git a/bench/tx-generator/README.md b/bench/tx-generator/README.md index 11644781075..e3c9bf9038b 100644 --- a/bench/tx-generator/README.md +++ b/bench/tx-generator/README.md @@ -58,8 +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 | -| `submissionEndpointType` | Optional submission endpoint kind (`Ogmios`); set together with `submissionEndpointURI` | No | -| `submissionEndpointURI` | Optional submission endpoint URI (e.g. `ws://host:port`); set together with `submissionEndpointType` | No | +| `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 @@ -115,7 +115,7 @@ The high-level JSON configuration is automatically compiled into a multi-phase s ### Submitting through a remote endpoint -Setting a submission endpoint (`submissionEndpointType` 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. +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**: @@ -131,7 +131,7 @@ Note that the **local node socket and config file are still required**: protocol The only endpoint type currently supported is `Ogmios`. Set: ```json -"submissionEndpointType": "Ogmios", +"submissionEndpointProtocol": "Ogmios", "submissionEndpointURI": "ws://127.0.0.1:1337" ``` diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs index 72461a18268..bf0d95aa2a5 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Compiler.hs @@ -267,15 +267,15 @@ getSetupSubmitMode = -- | 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 (SubmissionEndpointType, EndpointUri)) +resolveSubmissionEndpoint :: Compiler (Maybe (SubmissionEndpointProtocol, EndpointUri)) resolveSubmissionEndpoint = do - mType <- askNixOption _nix_submissionEndpointType + 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 - "submissionEndpointType and submissionEndpointURI must be set together \ + "submissionEndpointProtocol and submissionEndpointURI must be set together \ \(or both omitted)." delay :: Compiler () diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs index 4a920665977..2459aeb137c 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Ogmios.hs @@ -100,7 +100,11 @@ responseTimeout = 90_000_000 -- 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 !Text !Value +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) = @@ -216,9 +220,16 @@ mkSubmitRequest tx reqId = object , "id" .= reqId ] +-- | Outcome of a single @submitTransaction@ call, as decoded from the +-- JSON-RPC response. data OgmiosResult - = OgmiosSuccess Text - | OgmiosError Int Text Value + = 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 diff --git a/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs b/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs index 94fd1f55a06..dec3d4c4023 100644 --- a/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs +++ b/bench/tx-generator/src/Cardano/Benchmarking/Script/Types.hs @@ -33,7 +33,7 @@ module Cardano.Benchmarking.Script.Types ( , ProtocolParametersSource(QueryLocalNode, UseLocalProtocolFile) , ScriptBudget(AutoScript, StaticScriptBudget) , ScriptSpec(..) - , SubmissionEndpointType(..) + , SubmissionEndpointProtocol(..) , SubmitMode(Benchmark, DiscardTX, DumpToFile, LocalSocket, NodeToNode, SubmitToEndpoint) , TargetNodes @@ -47,7 +47,7 @@ import Cardano.Benchmarking.OuroborosImports (SigningKeyFile) import Cardano.Node.Configuration.NodeAddress (NodeIPv4Address) import Cardano.TxGenerator.ProtocolParameters (ProtocolParameters) import Cardano.TxGenerator.Setup.NixService (EndpointUri (..), NodeDescription, - SubmissionEndpointType (..)) + SubmissionEndpointProtocol (..)) import Cardano.TxGenerator.Types import Prelude @@ -188,7 +188,9 @@ data SubmitMode where DumpToFile :: !FilePath -> SubmitMode DiscardTX :: SubmitMode NodeToNode :: NonEmpty NodeIPv4Address -> SubmitMode --deprecated - SubmitToEndpoint :: !SubmissionEndpointType -> !EndpointUri -> SubmitMode + -- | 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 33c10705c48..a8f2131b3fe 100644 --- a/bench/tx-generator/src/Cardano/TxGenerator/Setup/NixService.hs +++ b/bench/tx-generator/src/Cardano/TxGenerator/Setup/NixService.hs @@ -12,7 +12,7 @@ module Cardano.TxGenerator.Setup.NixService ( NixServiceOptions (..) , NodeDescription (..) - , SubmissionEndpointType (..) + , SubmissionEndpointProtocol (..) , EndpointUri (..) , defaultKeepaliveTimeout , getKeepaliveTimeout @@ -64,25 +64,25 @@ data NixServiceOptions = NixServiceOptions { , _nix_sigKey :: SigningKeyFile In , _nix_localNodeSocketPath :: String , _nix_targetNodes :: NonEmpty NodeDescription - , _nix_submissionEndpointType :: Maybe SubmissionEndpointType + , _nix_submissionEndpointProtocol :: Maybe SubmissionEndpointProtocol , _nix_submissionEndpointURI :: Maybe EndpointUri } deriving (Show, Eq) deriving instance Generic NixServiceOptions --- | Which kind of endpoint 'submissionEndpointURI' addresses. Currently only --- Ogmios is supported; this is the extension point for further submission --- backends. -data SubmissionEndpointType +-- | 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 SubmissionEndpointType where - parseJSON = withText "SubmissionEndpointType" $ \t -> case t of +instance FromJSON SubmissionEndpointProtocol where + parseJSON = withText "SubmissionEndpointProtocol" $ \t -> case t of "Ogmios" -> pure Ogmios - _ -> fail $ "unknown submissionEndpointType: " ++ show t + _ -> fail $ "unknown submissionEndpointProtocol: " ++ show t -instance ToJSON SubmissionEndpointType where +instance ToJSON SubmissionEndpointProtocol where toJSON Ogmios = String "Ogmios" -- | A submission endpoint address, well-formed by construction: decoding diff --git a/bench/tx-generator/test-ogmios.sh b/bench/tx-generator/test-ogmios.sh index d9bff8e35cc..61508e9f948 100755 --- a/bench/tx-generator/test-ogmios.sh +++ b/bench/tx-generator/test-ogmios.sh @@ -172,7 +172,7 @@ 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. submissionEndpointType and submissionEndpointURI +# benchmark (non-debug) runs. submissionEndpointProtocol and submissionEndpointURI # must be set together. cat > "$CONFIG_FILE" << EOF { @@ -189,7 +189,7 @@ cat > "$CONFIG_FILE" << EOF "debugMode": true, "plutus": null, "sigKey": "$SIG_KEY", - "submissionEndpointType": "Ogmios", + "submissionEndpointProtocol": "Ogmios", "submissionEndpointURI": "ws://127.0.0.1:$OGMIOS_PORT" } EOF