Skip to content

Bump Cardano dependencies and report Dijkstra submission errors - #17

Merged
carbolymer merged 1 commit into
masterfrom
mgalazyn/chore/bump-cardano-deps
Nov 3, 2026
Merged

carbolymer merged 1 commit into
masterfrom
mgalazyn/chore/bump-cardano-deps

Conversation

@carbolymer

@carbolymer carbolymer commented Oct 27, 2026 •

Copy link
Copy Markdown

This PR moves ogmios to the latest Cardano packages on CHaP and maps every Dijkstra-era submission failure to an Ogmios error, with 8 new error codes, fixes for two existing errors, and a schema fix. ogmios depends on neither cardano-node nor cardano-api, so it does not have to wait for a node release built on these versions.

The switch to GHC 9.12.4 came in #16, which is merged. The new packages need it, because the new cardano-protocol-tpraos and cardano-ledger-shelley-test releases are not buildable with GHC 9.8, and ouroboros-consensus 5.2 needs both.

Dependencies

Package Before After
cardano-ledger-core 1.21 1.22
cardano-ledger-conway 1.23 1.24
cardano-ledger-dijkstra 0.3 0.4
ouroboros-consensus 4.2 5.2
ouroboros-network 1.2 2.0
cardano-diffusion 1.1 1.3

The CHaP index-state moves to 2026-10-27 and the Hackage index-state to 2026-10-23, with matching CHaP and hackage.nix inputs in flake.lock. server/cabal.project drops the cardano-node == 11.1.1 pin and the any.io-classes ^>= 1.8 constraint, and removes the allow-newer entries the new set no longer needs (*:formatting, io-sim:time, io-classes:time). katip:Win32 stays, since only a Windows build can show whether it is still needed.

The package bounds change as well. ouroboros-consensus gets ^>= 5.2 and ouroboros-network gets ^>= 2.0. The new lower bounds are small-steps >= 1.2 and cardano-ledger-alonzo >= 1.17. nodeToClientV_Min moves from V17 to V23.

ogmios now depends on ram instead of memory, following crypton 1.1.

Client-visible changes

  • ogmios negotiates node-to-client V23 only, because cardano-diffusion 1.3 removed V16 to V22. cardano-node 10.7.1 and later offer V23 by default, and older nodes are refused at the handshake. A node that does not offer V23 now produces an error that says so, instead of a raw handshake protocol exception.
  • Stake pools get an optional blsKey field with verificationKey and proofOfPossession. It appears in the stakePools query and in genesis stake pools when the pool has a BLS key. It will appear in pool registration certificates once the Dijkstra transaction encoder emits them. cardano.json gains a BlsKey definition.
  • Error 3142 (RetirementTooLate) now reports the right epochs for a pool retirement. It used to swap the current epoch and the listed epoch, and gave the last valid epoch as firstInvalidEpoch. The bug was already on master, so Conway was affected too. The message now also covers retirements that are too early, not only too late.
  • Two new script-context error messages: "Unexpected failure while building the script context: …" in all Plutus eras, and "Couldn't find the script executed for …" in Dijkstra.
  • The Dijkstra pointer-address message now says whether the pointer address was in a spent input or in an output, because ledger-dijkstra 0.4 raises it for inputs too.
  • The JSON schema (cardano.json, and so the TypeScript types) now lists dijkstra in the Era enum. This covers EraMismatch, incompatibleEra, unsupportedEra, minimumRequiredEra and currentNodeEra, and the era field of Praos blocks.
  • Script<Native> now lists the Dijkstra guard clause ({"clause":"guard","from":<hash>}). The server already emitted it, but the schema rejected it.
  • The schema also lists the guard script purpose. "guard" is now allowed in RedeemerPointer.purpose, which evaluation results, invalid redeemer pointers and extraneousRedeemers use, and missingRedeemers accepts a {"purpose": "guard", "script": <hash>} variant. The server already emitted both, and the CI schema test caught the gap. It also fixes two more gaps. The withdrawals of a treasuryWithdrawals governance action (RewardTransfer) are now keyed by stake address, which is what Ogmios sends, where the schema expected 56 hex characters, so no response containing such a proposal matched it. That bug was already on master, and CI found it with a random seed. The Dijkstra protocol parameters now list maxReferenceScriptsSizePerBlock and maxReferenceScriptsSizePerTransaction.
  • When the node offers no node-to-client version that Ogmios supports, the handshake error lists the versions the node offers and the versions Ogmios supports, instead of naming a fixed version.
  • In Dijkstra, withdrawals from accounts that are not registered, including those in sub-transactions, are reported as 3146 StakeCredentialNotRegistered. Earlier eras still report 3141. The 3141 message no longer says a withdrawal must always take the full balance.
  • If encoding a submission result fails, submitTransaction now returns a JSON internal error instead of closing the client's connection with no reply.

Existing error codes keep their meaning. The message changes are the two script-context messages, the Dijkstra pointer-address message, the 3141 message and the 3142 fix above.

Dijkstra submission errors

Before this PR, every Dijkstra-era rejection closed the connection with no reply, because the encoder was a stub. Now all 163 ledger failure constructors map to Ogmios errors. Existing codes are used where an equivalent exists, and there are 8 new codes. Code 3176 is not Dijkstra-only: it is reported in any era where the ledger rejects a pool registration or update that reuses the VRF key of another pool.

Code Name
3169 MissingRequiredGuards
3170 MalformedGuardDatums
3171 PointerAddressInCollateralReturn
3172 InvalidBlsProofOfPossession
3173 AllInputsAreSpent
3174 WithdrawalsExceedingBalance
3175 BalancesOutsideIntervals
3176 StakePoolVrfKeyAlreadyRegistered

3176 carries the pool id and the VRF key hash. Before, this failure was reported as an unknown stake pool. ogmios.json, the local-tx-submission docs page and the TypeScript types include the new codes. In the docs page, 3176 follows 3175 and 3997 and 3998 move down one place.

Notes for reviewers

  • The Dijkstra submission-error test generator now produces each of the 8 new codes, 3169 to 3176, so every one appears in the SubmitTransactionResponse vectors and passes the schema check. The unit test suite gains a cardano-data dependency for it. New unit tests check the exact error code and data for retirement limits, VRF key reuse, partial withdrawals and missing accounts. The era of the saved examples is now an explicit test argument.
  • The generator cannot build a Dijkstra EraMismatch, because genMismatchEraInfo only makes Byron and Shelley pairs. Tests do not exercise that schema case.
  • cardano-ledger-conway 1.24 does not export the CertificateNotSupported and PlutusPurposeNotSupported constructors of ConwayContextError. ogmios tells them apart by their JSON kind so that both keep their old messages. Deterministic tests now pin this workaround for Conway and Dijkstra.
  • The ledger now reports missing withdrawal accounts from the LEDGER rule instead of CERTS. Earlier eras still map this to IncompleteWithdrawals (3141), and Dijkstra maps it to StakeCredentialNotRegistered (3146).
  • Some Dijkstra mappings are approximate:
    • wrong-network failures for withdrawals, direct deposits and balance intervals map to NetworkMismatch;
    • inexact legacy withdrawals map to IncompleteWithdrawals;
    • missing direct-deposit or balance-interval accounts map to StakeCredentialNotRegistered, first account only;
    • ValueNotConservedInLegacyMode maps to ValueNotConserved;
    • SubTreasuryValueMismatch maps to TreasuryWithdrawalMismatch;
    • sub-transaction failures map like their top-level versions.
  • 151 test vectors are regenerated: 101 SubmitTransactionResponse, 15 NextBlockResponse, 10 QueryLedgerStateStakePools, 9 QueryLedgerStateLiveStakeDistribution, 8 QueryNetworkGenesisConfiguration/Shelley, 7 EvaluateTransactionResponse and 1 NextTransactionResponse. The ledger generators now produce different pool ids and VRF hashes (pool parameters have a BLS key slot), the new script-context messages, and different submission error variants.
  • The test suites now use QuickCheck's withNumTests instead of the deprecated withMaxSuccess (18 call sites in JsonSpec.hs and Bech32Spec.hs), which removes the deprecation warnings. Behaviour and vectors are unchanged.
  • Stake-pool vectors are now generated from Dijkstra, so some contain blsKey, and the schema check validates them. The evaluation vectors include Dijkstra context errors.
  • The per-era branches of the stake-pools query parser now share helpers. The output does not change.
  • After this schema fix, the schema-validation properties for SubmitTransactionResponse and EvaluateTransactionResponse ran with 30 seeds each and found no further gaps.
  • The Hydra ogmios-unit failure on seed 2007211148 was the treasury-withdrawal key pattern, and that seed now passes.
  • The TypeScript schema types are regenerated from cardano.json. The regeneration also picks up plutus:v4 and a Signatory doc change that were already in cardano.json on master.

Not included

  • 19 new Dijkstra protocol parameters are not encoded yet, because their JSON names need a schema decision: 9 Leios parameters, 6 Peras parameters, maxPledgeLeverage, minPoolMargin and the 2 refInputsCostPer* parameters. The gap also affects governance proposals: an evaluateTransaction failure that shows a ParameterChange touching only new fields renders "parameters": {}.

  • liveStakeDistribution does not show the pool BLS key.

  • Dijkstra submission errors do not say which sub-transaction failed, because the ledger does not report it.

  • server/Makefile still enters the ghc96-static-minimal-iog devx shell, because github:CardanoSolutions/devx has no GHC 9.12 shell yet. The Makefile target cannot build this branch until devx adds one.

How to trust this PR

  • nix build .#ogmios passes with the production flag (-Werror).
  • The unit tests pass: 158 examples, 0 failures. In my sandbox the checks.x86_64-linux.ogmios-unit derivation stops before the tests because it cannot create /build, so I ran the built test binary by hand on the same tree. A standard nix sandbox, as on Hydra, has /build.
  • The node compatibility claim comes from reading the source, not from a live node. In each release's ouroboros-consensus, latestReleasedNodeVersion caps the node-to-client versions a node offers. For example, ouroboros-consensus 3.0.1.0 (cardano-node 10.7.1 to 11.0.1) and 4.2.1.0 (cardano-node 11.1.1 and 11.1.3) both return NodeToClientV_23, and consensus 5.2 keeps the V23 codecs. cardano-node 10.7.0 also offers V23 but is a pre-release.
cardano-node Newest node-to-client version offered by default
10.1.x V18
10.2.x V19
10.3.x to 10.5.x V20
10.6.x V22
10.7.1 and later V23

@carbolymer
carbolymer changed the base branch from master to mgalazyn/chore/use-ghc-9.12 October 27, 2026 12:09
@carbolymer
carbolymer added this pull request to stack #18 October 27, 2026 12:09
@carbolymer carbolymer changed the title Mgalazyn/chore/bump cardano deps Bump Cardano dependencies to the latest CHaP releases Oct 27, 2026
@carbolymer
carbolymer force-pushed the mgalazyn/chore/bump-cardano-deps branch from 0434a59 to 8ddf1c8 Compare October 27, 2026 16:50
Base automatically changed from mgalazyn/chore/use-ghc-9.12 to master October 28, 2026 12:36
@carbolymer
carbolymer force-pushed the mgalazyn/chore/bump-cardano-deps branch 8 times, most recently from fa91de3 to 379f58b Compare October 28, 2026 18:18
@carbolymer carbolymer changed the title Bump Cardano dependencies to the latest CHaP releases Bump Cardano dependencies and report Dijkstra submission errors Oct 28, 2026
@carbolymer
carbolymer marked this pull request as ready for review October 29, 2026 14:57
@carbolymer
carbolymer force-pushed the mgalazyn/chore/bump-cardano-deps branch from 379f58b to 3c0b0c3 Compare October 29, 2026 15:25
Comment thread server/src/Ogmios/App/Health.hs Outdated
, "it runs on a different network!"
, hint
]
HandshakeError (Handshake.VersionMismatch _ _) ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should elaborate the versions otherwise this error message will go stale.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point. The message now lists the versions the node offered and the ones this Ogmios supports, taken from the handshake, so there is no hard-coded version left to go stale.

ValidationTagMismatch { validationTag, mismatchReason }
Cn.CollectErrors errors ->
pickPredicateFailure (encodeCollectErrors AlonzoBasedEraConway (toList errors))
pickPredicateFailure (encodeCollectErrors era (toList errors))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@carbolymer in a follow up PR can you try to remove the eons as much as possible and replace them with ShelleyBasedEra era where appropriate?

@carbolymer carbolymer Nov 2, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed. I replaced the eons with ShelleyBasedEra in a follow-up PR:

let nEras = length eras

-- NOTE: Stake pool vectors come from Dijkstra to show the BLS key.
let isVectorEra = \case

@Jimbo4350 Jimbo4350 Oct 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could we make the era for saved JSON examples an explicit argument, rather than checking for "stakePools" inside this helper? Which eras we test and which era supplies the saved examples should be separate decisions.

For example:

validateLedgerStateQuery ::
    Int ->
    SomeShelleyEra -> -- Era used for saved JSON examples
    Text ->
    Maybe Json.Value ->
    (Json.Value -> Json.Parser (QueryInEra Gen Block)) ->
    SpecWith ()

The stake-pool test could explicitly select Dijkstra:

validateLedgerStateQuery
    10
    (SomeShelleyEra ShelleyBasedEraDijkstra)
    "stakePools"
    Nothing
    (parseQueryLedgerStakePools
        genPoolParametersResultNoStake
        genPoolParametersResult)

Then replace isVectorEra era with sameEra era vectorEra, and handle the list of eras being tested independently.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. The era of the saved examples is now an explicit argument of validateLedgerStateQuery, and the check on the method name is gone.

encodePoolFailure = \case
Di.StakePoolNotRegisteredOnKeyPOOL poolId ->
UnknownStakePool { poolId }
Di.StakePoolRetirementWrongEpochPOOL (Mismatch currentEpoch listedEpoch) (Mismatch _ firstInvalidEpoch) ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The retirement epochs are reversed here. Mismatch stores the supplied value first and the expected value second, so this first mismatch contains the requested retirement epoch followed by the current epoch. The second mismatch's expected value is the last permitted retirement epoch, not the first invalid one.

For example, with current epoch 100, requested retirement 120, and maximum permitted retirement 110, this mapping returns currentEpoch = 120, declaredEpoch = 100, and firstInvalidEpoch = 110. Those should be 100, 120, and 111 respectively.

Please swap the first two bindings and convert the inclusive upper limit to the first invalid epoch. The older Shelley mapper has the same problem, so it would be worth correcting both rather than copying it into Dijkstra.

The pinned ledger rule shows the field ordering and the inclusive limit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch. The mapper read both mismatches the wrong way round, so currentEpoch and declaredEpoch came out swapped, and firstInvalidEpoch reported the last valid epoch. I've fixed it in the Dijkstra mapper and in the Shelley one, which Conway also uses, so Conway submissions had the same problem. firstInvalidEpoch is now the limit plus one. The message only talked about retiring too late, but the same failure fires when the epoch is not after the current one, so I reworded it to cover both cases.

StakePoolMetadataHashTooLarge { poolId, computedMetadataHashSize }
-- TODO: VRF-key reuse has no dedicated failure; fold into UnknownStakePool,
-- as the Shelley encoder does.
Di.VRFKeyHashAlreadyRegistered poolId _vrfKeyHash ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

VRF-key reuse should not become UnknownStakePool. This failure can occur when an already registered pool updates its parameters to use a VRF key belonging to another pool. Returning error 3140 tells the client that the pool is unknown and suggests checking registration/submission order, which is the wrong diagnosis.

Could we give this failure a distinct mapping and retain the offending VRF hash? The Shelley mapper has the same approximation, but copying it here preserves misleading client-visible behavior.

The pinned ledger implementation raises it for both new registrations and updates to existing pools.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed, reporting an unknown pool here sends clients the wrong way. I've added a new error, 3176 StakePoolVrfKeyAlreadyRegistered, which carries the pool id and the VRF key hash. The Shelley mapper (also used by Conway) and the Dijkstra one now both map VRFKeyHashAlreadyRegistered to it. The schema, docs and changelog are updated.

Di.WithdrawalAddressesWithWrongNetwork expectedNetwork accounts ->
encodeWrongNetworkAccounts expectedNetwork accounts
-- TODO: missing accounts fold into IncompleteWithdrawals, as in earlier eras.
Di.WithdrawalAccountsMissing ws ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Mapping missing accounts to IncompleteWithdrawals also reuses its message that withdrawals must consume the entire account balance. That statement is incorrect for Dijkstra outside legacy mode, where partial withdrawals are allowed.

Please distinguish the missing-account diagnosis from the legacy requirement to withdraw everything, either through a separate failure or a message that accounts for the actual cause. The same issue applies to WithdrawalAccountsMissingFromOriginal and the corresponding sub-transaction mappings. A client fixing a missing account should not also be told that a valid partial withdrawal is forbidden.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed on both points. Missing withdrawal accounts, in the transaction and in its sub-transactions, now map to the existing 3146 error, StakeCredentialNotRegistered, with the account's credential. When several accounts are missing, it names the first one, since Ogmios reports one error per submission. I've also reworded the 3141 message so it no longer says a withdrawal must always take the whole balance, as Dijkstra allows partial withdrawals outside legacy mode.


-- | The ledger's own generator rarely yields the newest failures; add them explicitly.
genDijkstraMempoolFailure :: Gen (Di.DijkstraMempoolPredFailure DijkstraEra)
genDijkstraMempoolFailure = frequency

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could we add deterministic tests for the meaning of these mappings as well as the generated JSON/schema checks? Swapping currentEpoch and declaredEpoch, or mapping VRF-key reuse to UnknownStakePool, still produces schema-valid JSON.

In particular, please assert the exact error code and data for retirement epoch boundaries, VRF-key reuse, and the distinction between missing withdrawal accounts and inexact legacy withdrawals. Generating more samples alone will not catch these semantic mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed. The schema tests couldn't catch the swapped epochs, because swapped numbers still produce valid JSON. I've added unit tests that check the exact error code and data for the retirement epoch bounds, VRF key reuse, and missing versus partial withdrawals.

@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.

LGTM

@carbolymer
carbolymer force-pushed the mgalazyn/chore/bump-cardano-deps branch from 1ed2fcb to c039310 Compare November 3, 2026 11:36
Move to cardano-ledger-core 1.22, cardano-ledger-conway 1.24,
cardano-ledger-dijkstra 0.4, ouroboros-consensus 5.2,
ouroboros-network 2.0 and cardano-diffusion 1.3, at CHaP index-state
2026-10-27 and Hackage index-state 2026-10-23, with matching flake.lock
inputs. Drop the cardano-node version pin and the allow-newer entries
the new stack no longer needs. The ram package replaces memory.

The new stack speaks only node-to-client V23, which cardano-node 10.7.1
and later offer. Ogmios now refuses a node without V23 at the handshake
with a clear error, and stops. The error lists the node-to-client
versions the node offers and the ones Ogmios supports.

Stake pools gain an optional blsKey field.

Failed Dijkstra-era transaction submissions are now reported as errors.
Before, the connection closed with no reply. Existing error codes are
used where they fit. New codes 3169 to 3175 cover guards, a pointer
address in the collateral return, the BLS proof of possession, all
inputs spent, withdrawals that exceed the balance, and balances outside
their intervals. If the encoder fails while building a submitTransaction
reply, Ogmios now returns an internal error instead of closing the
connection.

In Dijkstra, withdrawals from accounts that are not registered, including
those in sub-transactions, are reported as 3146
StakeCredentialNotRegistered. The 3141 message no longer says a
withdrawal must always take the full balance.

New error 3176 StakePoolVrfKeyAlreadyRegistered is reported when a pool
registration or update reuses the VRF key of another pool. Before, this
was reported as an unknown stake pool.

Fix error 3142 for pool retirements in Conway and Dijkstra. It swapped
the current epoch and the listed epoch, and gave the last valid epoch as
firstInvalidEpoch. It now also covers retirements that are too early,
not only too late.

Script-context build failures get new error messages, and the Dijkstra
pointer-address message now says whether the address is an input or an
output.

Fix the JSON schema: add dijkstra to the Era enum, the guard clause of
native scripts, and the guard script purpose. Add the Dijkstra reference
script size parameters, maxReferenceScriptsSizePerBlock and
maxReferenceScriptsSizePerTransaction. Key treasury withdrawals by stake
address. Regenerate the TypeScript types from the schema.

Check Dijkstra stake pools with blsKey and every new error code against
the schema, and regenerate the test vectors. Unit tests now check the
exact error code and data for these cases.
@carbolymer
carbolymer force-pushed the mgalazyn/chore/bump-cardano-deps branch from c039310 to 851726b Compare November 3, 2026 11:37
@carbolymer
carbolymer merged commit d0d8cd4 into master Nov 3, 2026
6 checks passed
@carbolymer
carbolymer deleted the mgalazyn/chore/bump-cardano-deps branch November 3, 2026 11:56
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