Skip to content

Tests comparing against cardano-cli, on top of cardano-testnet and tx-generator - #12

Open
palas wants to merge 27 commits into
jmiller/dijkstra-integrationfrom
testnet-tx-gen-tests
Open

Tests comparing against cardano-cli, on top of cardano-testnet and tx-generator#12
palas wants to merge 27 commits into
jmiller/dijkstra-integrationfrom
testnet-tx-gen-tests

Conversation

@palas

@palas palas commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

To run:

nix develop .#integration
cd server
cabal run ogmios-integration-tests

It includes a retry mechanism to avoid intermittent node connection issues.

palas added 13 commits May 29, 2026 04:50
Compares queryLedgerState/protocolParameters (ogmios) against
cardano-cli conway query protocol-parameters, checking key fields:
minFeeCoefficient, minFeeConstant, maxBlockBodySize, maxTxSize,
stakeCredentialDeposit, and stakePoolDeposit.
Compares queryLedgerState/tip slot against cardano-cli query tip.
Compares queryNetwork/tip slot against cardano-cli query tip.
Compares queryLedgerState/epoch (ogmios) against the epoch field
from cardano-cli query tip.
Compares pool IDs from queryLedgerState/stakePools (ogmios) against
cardano-cli conway query stake-pools.
Compares reward balances from queryLedgerState/rewardAccountSummaries
(ogmios) against cardano-cli conway query stake-address-info.
Compares pool IDs in queryLedgerState/liveStakeDistribution (ogmios)
against cardano-cli conway query stake-distribution.
Compares constitution anchor hash from queryLedgerState/constitution
(ogmios) against cardano-cli conway query constitution.
Compares DRep IDs from queryLedgerState/delegateRepresentatives (ogmios)
against cardano-cli conway query drep-state --all-dreps.
Redirect tx-generator stdout/stderr to logs/tx-generator.{stdout,stderr}
in the work directory, consistent with cardano-testnet and ogmios.
Comment thread server/test/integration/src/Test/Integration/Utxo.hs Outdated
Comment thread server/test/integration/src/Test/Integration/Env.hs
Comment thread server/test/integration/src/Test/Integration/Env.hs Outdated

@Jimbo4350 Jimbo4350 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments

palas added 3 commits July 16, 2026 04:57
Moved from cardano-node PR #6609 (bench/tx-generator/test-ogmios.sh),
where it sat unreferenced by the cabal file, CI or nix; it exercises
Ogmios's submitTransaction contract end to end and resolves its
binaries from this repository's flake, so this is where it belongs.

The script spins up a local cardano-testnet and an Ogmios instance,
runs cardano-node's tx-generator with a submission endpoint config
(every phase's transactions go through Ogmios as JSON-RPC 2.0
submitTransaction calls), and passes when new UTxOs appear at
tx-generator's benchmark address.

Adaptations from the original: ogmios now comes from this repository's
own flake; tx-generator comes from a cardano-node flake ref given as
$1, defaulting to github:IntersectMBO/cardano-node/master (until
cardano-node#6609 is merged, pass its branch instead); and every
binary can be supplied through the environment to skip its nix build.

Kept as a plain script for now; integrating it with the
server/test/integration suite is a follow-up.
Each failed readiness probe (connection refused while ogmios is still
starting up) threw out of connect before close ran, leaking one socket
fd per attempt. Wrap the socket in bracket so it is closed either way.
withResource only runs teardown when setup returns, so a throw after
the processes had spawned (waitForFile/waitForTcpPort timeout, ogmios
createProcess failure, non-zero tx-generator exit) orphaned
cardano-testnet and ogmios and leaked the log handles. A leaked ogmios
also keeps holding port 11337, which can make the next run's readiness
probe pass against a stale daemon.

Register each process and log handle in an IORef as it is created and
wrap setup in bracketOnError, so a failing setup tears down whatever it
had created (newest first) and re-raises. The success path is unchanged:
teardown still owns cleanup once setup returns.

runTxGenerator's resources, unlike setup's, do not outlive the call, so
plain scoped cleanup fits there: the log handles move to withFile and
the process itself to withCreateProcess, covering its failure paths
(including cancellation mid-run) without a stack.
@palas
palas changed the base branch from jmiller/dijkstra-integration to master July 29, 2026 17:33
@palas
palas changed the base branch from master to jmiller/dijkstra-integration July 29, 2026 17:33
@palas
palas changed the base branch from jmiller/dijkstra-integration to master July 29, 2026 17:34
@palas
palas force-pushed the testnet-tx-gen-tests branch 2 times, most recently from 7234aa9 to 89c3deb Compare July 29, 2026 17:35
Every query module carried its own copy of the WebSocket
send/receive/decode function (differing only in the method string and
optional params), the {result|error} unwrap case, a cardano-cli runner
and a parseEither-to-assertFailure wrapper. Move those into a shared
Test.Integration.Query:

  queryOgmios    - JSON-RPC request with the connection retry and the
                   result/error unwrap folded in
  queryCli       - cardano-cli query via --out-file; the socket, magic
                   and a fresh per-call out-file are appended, so
                   parallel tests cannot clobber each other's output
  queryCliStdout - for commands without --out-file support
  assertSameSet  - set comparison printing each side's extra elements
  parseIO        - aeson parser failure to test failure

The per-module parsers and normalizers stay where they were; the query
modules keep only their method, cli arguments and comparisons.
@palas
palas force-pushed the testnet-tx-gen-tests branch from 7234aa9 to e730a2c Compare July 29, 2026 17:37
@palas
palas force-pushed the testnet-tx-gen-tests branch from e53b6b2 to e730a2c Compare July 29, 2026 17:38
@palas
palas changed the base branch from master to jmiller/dijkstra-integration July 29, 2026 17:38
palas added 2 commits July 29, 2026 19:58
With `"aarch64-darwin"` enabled in `systems`, `packages.ogmios-musl`
resolves to `project.projectCross.aarch64-multiplatform-musl` on a macOS
host, i.e. a darwin -> aarch64-unknown-linux-musl cross build.  No cache
has a substitute for the cross toolchain that needs, so Hydra compiles
`aarch64-unknown-linux-musl-ghc-9.8.4` from source on the darwin builders;
on evaluation 116643 it spent 1h32m on `oak-m2pro-2`, died with
`error: unexpected end-of-file`, was retried twice more (10m25s, 7m13s),
and took `aarch64-darwin.required` down with it.

A static Linux binary is only meaningful from a Linux host anyway, so gate
the job on the host platform.  `x86_64-linux` and `aarch64-linux` keep
`ogmios-musl`; `aarch64-darwin` no longer offers it.

Verified with `nix eval .?submodules=1#hydraJobs.<system>.packages`:

  aarch64-darwin  [ default ogmios ogmios-integration-tests ogmios-lib ]
  x86_64-linux    [ default ogmios ogmios-integration-tests ogmios-lib
                    ogmios-musl ]
scripts/test-txgen-submission.sh checked that transactions submitted
through ogmios (cardano-node's tx-generator speaking its
`submissionEndpointProtocol: "Ogmios"` transport) are accepted and end up
in blocks. Everything it did by hand — start cardano-testnet, wait for the
node socket, start ogmios, wait for its port, run tx-generator, clean up —
the integration suite already does in Test.Integration.Env, and the five
binaries it resolved with nix are what the `integration` devshell provides.
So the script's own content boils down to one test case, which is what this
commit adds; the script goes away with it.

Test.Integration.TxSubmission reuses the suite's testnet and ogmios
instance, and asserts what the script asserted: tx-generator exits
cleanly, and new UTxOs appear at the address its last phase pays to (the
"BenchmarkingDone" key, whose address is derived here rather than
hardcoded, as in the script). Counting those UTxOs through both ogmios and
cardano-cli also covers an address-filtered ledger-state query, which the
whole-UTxO comparison in Test.Integration.Utxo does not.

Two details are worth pointing out, both marked in the code:

  - The submission run does not pass --testnet-config-dir. Discovery always
    picks utxo1 and overrides the config's own sigKey, and the funding run
    in Env has already spent that key's genesis fund — spending it a second
    time would be rejected for a missing input. The four connection
    settings discovery would supply are given explicitly instead, with
    utxo2 as the genesis key (cardano-testnet seeds three).

  - targetNodes is spelled out as empty. An endpoint replaces the target
    nodes as the submission target, so this is what the config compiler
    wants, and it doubles as a guard: a tx-generator predating
    IntersectMBO/cardano-node#6609 ignores the two submissionEndpoint* keys
    and would otherwise quietly benchmark node-to-node, passing this test
    without ogmios ever seeing a transaction. Such a version now fails on
    the empty targetNodes, and the test checks tx-generator's own report of
    its submission target as well.

Because the test mutates the UTxO set, it has to run after the comparison
tests, which assume nothing is moving underneath them: Main now sequences
the query groups ahead of it with AllFinish, so a failing comparison does
not skip submission coverage.

The transport landed in cardano-node after 11.0.1, so tx-generator comes
from a second flake input pinned past the merge, leaving the node, cli and
testnet binaries on the released tag the server is built against. That
input can be folded back into `cardano-node` once a release carries the
transport.
@palas
palas changed the base branch from jmiller/dijkstra-integration to master July 30, 2026 20:26
@palas
palas force-pushed the testnet-tx-gen-tests branch from cac3e57 to a9b40e5 Compare July 30, 2026 20:27
@palas
palas changed the base branch from master to jmiller/dijkstra-integration July 30, 2026 20:27
palas added 2 commits August 1, 2026 04:51
The unit test suite bakes the absolute path of the package root into
the test binary at compile time (Test.Path.Util.getProjectRoot, via
file-embed's `makeRelativeToProject ""`) and uses it to locate the
golden files, the network configurations and the test-vector output
directory. Nix builds and runs the suite in two separate derivations,
so the check recreated that path before running the tests:

    mkdir -p /build/ogmios-src-test-unit-root/server/test
    ...

That only works on Linux, where every sandboxed build runs in /build.
On darwin build directories are unique per derivation (under
/private/tmp) and the root file system is sealed, so the check dies
immediately (hydra evaluation 117985 of this branch's jobset,
aarch64-darwin.checks.ogmios-unit):

    Running phase: buildPhase
    mkdir: cannot create directory '/build': Read-only file system

This is the last red job on aarch64-darwin: with the musl static build
gated to Linux hosts, everything else on the platform - the whole
Haskell dependency closure, packages.{ogmios,ogmios-lib,
ogmios-integration-tests,default} and both devShells - builds green on
that same evaluation, and `checks.ogmios-unit` is the one constituent
still failing `aarch64-darwin.required`.

Let the suite take the project root from an OGMIOS_TEST_PROJECT_ROOT
environment variable at run time, falling back to the baked path, and
have the check point that variable at a copy of the needed files under
its own (always writable) $TMPDIR. This removes the dependence on the
Linux sandbox layout altogether: the check now expresses the same thing
on every platform, and running the suite by hand keeps working exactly
as before since the fallback is untouched.
Two separate things kept `cabal build` from working inside
`nix develop .#integration` (and .#default alike):

1. cabal.project constrains `any.text source`. Its point is to keep the
   solver away from GHC's bundled text so that the `-simdutf` flag
   choice below actually applies to a from-source build, but `source`
   also rejects the text that haskell.nix registers in the shell's
   package database, while the shell's pre-built aeson only links
   against exactly that instance:

       rejecting: text; 2.1.4/installed-KVpHHZYgS6OIzTPD9vCwRV, ...
         (constraint from project config cabal.project requires source instance)
       rejecting: text; 2.1.4, 2.1.3, ...
         (conflict: aeson => text==2.1.4/installed-KVpHHZYgS6OIzTPD9vCwRV)

   Express the intent as a version bound instead: `>= 2.1.2` excludes
   the bundled text (2.1.1 as of GHC 9.8) just as effectively, while
   leaving installed instances of the versions it admits eligible. The
   nix-side plan is unchanged - the solver still picks text-2.1.4,
   built from Hackage with -simdutf (the very same unit id as before).

2. Even with that fixed, no target under cabal.project can be built
   from the shell, because cabal resolves the project as a whole and
   the ogmios package cannot be resolved there: it depends on public
   sublibraries (ouroboros-network:api among others, and
   ouroboros-network itself needs typed-protocols:cborg), and
   cabal-install cannot satisfy a sublibrary dependency from an
   installed package database, where each component is registered as
   its own disjoint unit:

       rejecting: ouroboros-network-1.1.0.0/installed-G4Sn...
         (does not contain library 'api', which is required by ogmios)

   That is a cabal-install limitation, not something project
   configuration can express; building the server itself in the shell
   keeps requiring nix until cabal learns to use installed
   sublibraries.

   The integration suite, however, does not depend on the ogmios
   library at all (it drives the ogmios *binary*), and everything in
   its build-depends is pre-built in the shell. So give it what it
   really is: its own one-package project. Cabal's project discovery
   stops at the first cabal.project on the way up, so from
   server/test/integration a plain

       cabal build
       cabal run ogmios-integration-tests

   now resolves instantly (only the suite itself gets compiled) and
   works in both devshells. The index-state pin mirrors the main
   project so the solver settles on the versions the shell provides
   rather than on newer Hackage releases it would have to build. The
   file changes nothing anywhere else: the outer project lists its
   packages explicitly, and haskell.nix keeps planning from
   server/cabal.project, so hydra and `nix build` are unaffected.
@palas
palas force-pushed the testnet-tx-gen-tests branch from 2998b73 to 47066c0 Compare August 1, 2026 02:53
palas added 5 commits August 1, 2026 04:55
cabal-install cannot take sublibraries out of an installed package
database: haskell.nix registers every component as its own disjoint
unit, and the solver wants a single ouroboros-network instance that
provides both the main library and `api` (likewise typed-protocols
with `cborg`, ouroboros-consensus with `cardano`, and so on). So it
re-plans that whole stack from source. Every such plan was in turn
dying on a single gate: ouroboros-consensus's `lmdb` sublibrary
declares `pkgconfig-depends: lmdb`, cabal checks the pkg-config
dependencies of every component of a source candidate at solve time -
whether or not that component would ever be built - and the shells'
pkg-config set (sodium, secp256k1, blst, ...) had no lmdb, since the
nix build of consensus never enables the lmdb backend either. Every
branch of the search failed on it, and the resulting "searching the
rest of the dependency tree exhaustively" error read as if the project
were structurally unresolvable from the shell. It is not.

Add lmdb to both shells. With it in the pkg-config set, plain

    cabal build exe:ogmios     # or lib:ogmios, or all

resolves with no extra flags and builds: the general Hackage layer
(aeson, websockets, wai, ...) is reused pre-built from the shell's
package db, while the Cardano layer - ledger with every era,
plutus-core, consensus, network, diffusion, io-classes, the crypto
packages; 52 components in all - compiles from source into cabal's
store. That happens once per checkout (measured ~8 minutes on an
8-core aarch64 machine) and is incremental afterwards. The lmdb
sublibrary itself is planned but never built, so liblmdb is only ever
consulted by pkg-config, not linked.

Verified: `cabal build exe:ogmios` from the integration shell links,
and the binary answers --version with the checkout's revision.

For iterating on the integration suite alone, the project root added
by the previous commit remains the cheap path - it needs none of this.
The Signatory schema in cardano.json fixes 'key' at 64 hex digits (a
plain Ed25519 verification key) and keeps the chain code in its own
optional 'chainCode' field, and upstream ogmios encodes Byron
transaction witnesses accordingly, splitting the 64-byte XPub in two:

    encodeVerificationKey x =
        "key" .= encodeByteStringBase16 (BS.take 32 xpub)
        <>
        "chainCode" .= encodeByteStringBase16 (BS.drop 32 xpub)

This branch dropped the split somewhere on the way to the dijkstra
integration and serialises the whole XPub (128 hex digits) under 'key',
so every Byron block with at least one transaction produces JSON that
violates ogmios' own schema. The unit suite's generative property over
NextBlockResponse catches it whenever QuickCheck draws such a block:

    ogmios.json#/properties/NextBlockResponse
      Assertion failed (after 42 tests):
      ...
      invalid property 'key': '6e82da29b600684e...c474c890' should
      have at most 64 character(s)

(Reproducible with --seed 1846810050. hspec randomises the seed on
every run, so each checks.ogmios-unit run on each platform is a dice
roll; the x86_64-linux job has simply been rolling well so far. It
first came up on an aarch64-linux run of the check.)

Restore the upstream encoding for witnesses. The block-level issuer and
delegate keys, and update-proposal voter keys, legitimately serialise
the full extended key - Block<BFT> points them at
ExtendedVerificationKey, 128 hex digits - so keep that behaviour under
upstream's name for it, encodeExtendedVerificationKey.
Test.Integration.Env resolves ogmios, cardano-node, cardano-cli,
cardano-testnet and tx-generator from PATH (failing with e.g.
"cardano-cli not found on PATH" otherwise). Inside `nix develop
.#integration` they come from the shell, but the packaged
`packages.ogmios-integration-tests` was the bare executable, so running
it any other way - `nix run`, or the store path a `nix build` prints -
fell over immediately.

Wrap the packaged binary with those five tools as an execution
dependency (prefixing PATH, so the pinned versions win no matter where
it runs). `nix run .?submodules=1#ogmios-integration-tests` is now
self-contained, which is also the shape a CI job that actually
*executes* the suite would want. The devshell workflow is unchanged:
`cabal run` builds stay unwrapped on purpose, so the shell's PATH (or a
deliberately shadowed tool, as in the tx-generator 2.16 negative
control) keeps deciding what the suite drives.

The wrapper does mean hydra's `required` aggregate now carries the
cardano-node tool closure on every platform; those store paths already
substitute from cache.iog.io on both x86_64-linux and aarch64-darwin
today (the `integration` devShell, green in the same jobset, pulls the
very same ones).

Verified: with no devshell at all, the wrapped binary runs the full
suite - all 11 tests pass, tx-generator submitting through ogmios
included.
So far CI only ever compiled the integration suite
(packages.ogmios-integration-tests sits in the `required` aggregate);
nothing executed it. Add `checks.ogmios-integration`, a derivation that
runs the packaged suite from the previous commit - its wrapper already
carries ogmios, cardano-node, cardano-cli, cardano-testnet and
tx-generator - against a throwaway testnet spun up entirely inside the
build, over loopback. On failure the build log ends with the tail of
every daemon log, so hydra shows more than just the tasty summary.

The check is defined for every system, so it can always be run by hand
(`nix build .#checks.<system>.ogmios-integration`, or `nix flake
check`; __darwinAllowLocalNetworking is set for sandboxed darwin
machines - the suite only talks to 127.0.0.1). What hydra makes of it
is decided separately, in hydraJobs.nix:

* hydra only schedules it on Linux, where every build gets a private
  network namespace and the suite's fixed ogmios port (11337) cannot
  collide with anything. On darwin that port is shared with the whole
  builder, so concurrent evaluations could race for it; the suite runs
  fine on Apple silicon by hand, but CI leaves the darwin variant to
  manual runs.

* on Linux it counts as advisory rather than merge-blocking: hydraJobs
  still lists every check as its own job, but the aggregates now split,
  `required` taking every check except those in `integrationChecks`,
  which land in `nonrequired` instead. A two-minute multi-daemon
  integration test has more ways to flake on a loaded builder than a
  pure unit suite; once it has a track record on the jobset, moving it
  over is a one-line change.

Verified here: `nix build .#checks.aarch64-linux.ogmios-integration`
runs the full suite inside the derivation - all 11 tests pass, the
tx-generator submission included. Evaluating the jobsets shows the
check present in flake checks on both platforms, scheduled by hydra
only on Linux (in nonrequired's constituents, absent from required's,
which are otherwise unchanged), and absent from the darwin jobset.
The repo defines no formatter, and flake-parts can only omit the
`formatter` output while its static heuristic manages to prove that no
perSystem module defines one. That proof is brittle in practice: with
the systems list as committed it holds, but merely enabling another
entry in `systems` made it give up, after which the flake exposes a
`formatter.<system>` attribute that throws when forced - and
`nix flake check` forces it:

    error: flake-parts could not determine statically that no formatter
    is defined for *all* systems.

Import flake-parts' touchup module and state the absence explicitly, as
that error message itself suggests. With this, `nix flake check
'.?submodules=1' --accept-flake-config` passes end to end here: it
evaluates every output (hydraJobs included), builds and runs the checks
for the current system - the unit suite and, since the previous commit,
the full integration suite - and omits other systems as usual unless
`--all-systems` is given.
@palas
palas force-pushed the testnet-tx-gen-tests branch from 47066c0 to 9b89655 Compare August 1, 2026 02:55
@palas
palas force-pushed the testnet-tx-gen-tests branch from 5346710 to 85b6e0c Compare August 1, 2026 03:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants