Skip to content

Fix non-canonical inline datum JSON round-trip#1238

Open
v0d1ch wants to merge 6 commits into
masterfrom
inline-datum-bug
Open

Fix non-canonical inline datum JSON round-trip#1238
v0d1ch wants to merge 6 commits into
masterfrom
inline-datum-bug

Conversation

@v0d1ch

@v0d1ch v0d1ch commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

● Fix non-canonical inline datum JSON round-trip

Problem

FromJSON (TxOut) crashes when parsing a TxOut whose inline datum was encoded with non-canonical CBOR bytes (e.g. definite-length arrays instead of the indefinite-length form Plutus normally emits).

The error:
"Inline datum not equivalent to inline datum hash"

This happens because ToJSON and FromJSON disagree on how to reconstruct HashableScriptData:

  • ToJSON serialises the inline datum as three fields:
    • inlineDatum — the human-readable JSON representation
    • inlineDatumhash — blake2b_256(originalCborBytes)
    • inlineDatumRaw — the original CBOR bytes as hex
  • FromJSON ignored inlineDatumRaw entirely and reconstructed HashableScriptData via scriptDataFromJson, which always re-serialises to canonical CBOR. For non-canonical original bytes:
    • H(canonical) ≠ H(original) = inlineDatumhash
    • → parse fails with the above error

Non-canonical Plutus CBOR is valid and accepted by the ledger. It arises when scripts use definite-length array encoding for constructor fields instead of the canonical indefinite-length form. Any node that stores such a UTxO entry and later replays it from a JSON snapshot (e.g. from a SQLite event log) would crash on
deserialisation.

Fix

inlineDatumRaw is always present in JSON produced by ToJSON (as null for non-inline outputs, as hex for inline ones). FromJSON now reads this field first and uses deserialiseFromCBOR AsHashableScriptData to reconstruct HashableScriptData with the original bytes preserved. The scriptDataFromJson path is kept as a fallback for JSON
produced by external tools or older versions that may not include inlineDatumRaw.

The fix is applied to all six affected sites: Babbage, Conway, and Dijkstra eras in both FromJSON (TxOut CtxTx era) and FromJSON (TxOut CtxUTxO era). The repeated logic is extracted into a parseInlineDatum helper.

Testing

Added prop_json_roundtrip_txout_noncanonical_inline_datum — a property test that generates a TxOut with a non-canonical inline datum (definite-length CBOR constructor) and asserts the JSON round-trip is identity. The test fails without the fix and passes with it.

Checklist

  • Commit sequence broadly makes sense and commits have useful messages
  • New tests are added if needed and existing tests are updated. See Running tests for more details
  • Self-reviewed the diff
  • Changelog fragment added in .changes/

Copilot AI review requested due to automatic review settings June 25, 2026 14:54
@v0d1ch v0d1ch changed the title Inline datum bug Fix non-canonical inline datum JSON round-trip Jun 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a JSON round-trip bug for TxOut values containing inline datums whose underlying CBOR bytes are non-canonical (yet ledger-valid), by ensuring FromJSON preserves the original datum bytes when inlineDatumRaw is available.

Changes:

  • Add parseInlineDatum helper to prefer decoding HashableScriptData from inlineDatumRaw CBOR bytes, with a JSON-based fallback for older/external JSON.
  • Apply the inline-datum parsing fix across Babbage/Conway/Dijkstra for both CtxTx and CtxUTxO FromJSON (TxOut ...) instances.
  • Add a property test (plus generator) that constructs a non-canonical inline datum and asserts JSON round-trip identity.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
cardano-api/test/cardano-api-test/Test/Cardano/Api/Json.hs Adds a property test covering JSON round-tripping of TxOut with non-canonical inline datum CBOR.
cardano-api/src/Cardano/Api/Tx/Internal/Output.hs Introduces parseInlineDatum and updates FromJSON (TxOut ...) to preserve original inline datum CBOR via inlineDatumRaw.
cardano-api/gen/Test/Gen/Cardano/Api/Typed.hs Adds a generator for non-canonical HashableScriptData used by the new property test.

Comment thread cardano-api/src/Cardano/Api/Tx/Internal/Output.hs Outdated
@v0d1ch v0d1ch self-assigned this Jun 25, 2026
@Crypto2099

Copy link
Copy Markdown

Thanks for getting on this so quickly @v0d1ch! This is currently causing my hydra heads to fail to restart when a non-canonical datum is present in the ledger and the hydra-node crashes for any reason and attempts to validate its snapshot state from the state database.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread cardano-api/test/cardano-api-test/Test/Cardano/Api/Json.hs Outdated
@noonio

noonio commented Jun 30, 2026

Copy link
Copy Markdown

Any change of getting this merged? @Jimbo4350

@v0d1ch v0d1ch force-pushed the inline-datum-bug branch from add6f20 to 053bf89 Compare July 6, 2026 11:48

@Jimbo4350 Jimbo4350 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice catch and LGTM. Just two small changes.

Comment thread cardano-api/src/Cardano/Api/Tx/Internal/Output.hs Outdated
Comment thread cardano-api/src/Cardano/Api/Tx/Internal/Output.hs Outdated
@v0d1ch v0d1ch force-pushed the inline-datum-bug branch from 5145fc4 to 01c8932 Compare July 7, 2026 11:10
@v0d1ch v0d1ch requested a review from Jimbo4350 July 7, 2026 11:10
v0d1ch and others added 6 commits July 9, 2026 09:49
  FromJSON (TxOut CtxUTxO era) ignores inlineDatumRaw and reconstructs
  HashableScriptData via scriptDataFromJson, which re-serialises to
  canonical CBOR bytes. For datums whose original CBOR uses definite-length
  arrays (non-canonical), H(canonical) ≠ H(original), causing "Inline datum
  not equivalent to inline datum hash" on parse.

  Adds genNonCanonicalHashableScriptData and a failing property test that
  will pass once FromJSON reads inlineDatumRaw to preserve original bytes.

Signed-off-by: Sasha Bogicevic <sasha.bogicevic@iohk.io>
  FromJSON (TxOut) for Babbage, Conway and Dijkstra ignored inlineDatumRaw
  and reconstructed HashableScriptData via scriptDataFromJson, which always
  re-serialises to canonical CBOR. For datums whose original on-chain CBOR
  uses definite-length arrays (non-canonical Plutus encoding), this caused
  H(canonical) ≠ H(original), triggering "Inline datum not equivalent to
  inline datum hash" when parsing JSON produced by the ToJSON instance.

  ToJSON always writes inlineDatumRaw containing the original CBOR bytes.
  FromJSON now reads that field first and uses deserialiseFromCBOR to
  reconstruct HashableScriptData with the original bytes intact, falling
  back to scriptDataFromJson only for JSON produced without inlineDatumRaw.

  Fix applied to both CtxTx and CtxUTxO instances for all three eras.
  The repeated logic is extracted into a parseInlineDatum helper.

Signed-off-by: Sasha Bogicevic <sasha.bogicevic@iohk.io>
Signed-off-by: Sasha Bogicevic <sasha.bogicevic@iohk.io>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@v0d1ch v0d1ch force-pushed the inline-datum-bug branch from 01c8932 to 66db5d3 Compare July 9, 2026 07:50
@v0d1ch

v0d1ch commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@Jimbo4350 I think this PR is ready to be merged now.

v0d1ch added a commit to cardano-scaling/hydra that referenced this pull request Jul 9, 2026
…ps (#2746)

  cardano-api's FromJSON for TxOut ignores the inlineDatumRaw field and
reconstructs HashableScriptData via scriptDataFromJson, which
re-serialises
canonically. For outputs whose datum was encoded with definite-length
CBOR
arrays (valid on L1 but non-canonical), H(canonical) ≠ H(original),
causing
  "Inline datum not equivalent to inline datum hash" on event replay.

Add parseTxOutFromJSON that reads inlineDatumRaw, deserialises the
original
bytes directly, and patches inlineDatumhash to the canonical hash before
delegating to the standard parser — then restores the original bytes.
Add
parseUTxOFromJSON that applies this per entry. Wire it in via an
OVERLAPPING
  orphan FromJSON UTxO instance in IsTx.hs so all deserialization paths
(Snapshot, HTTP handlers, event replay) are fixed without touching the
IsTx
  typeclass interface.

Add property tests: one pending the upstream cardano-api fix (xprop),
one
  verifying our parseTxOutFromJSON preserves the original datum hash.
  
  Upstream PR: IntersectMBO/cardano-api#1238

<!-- Describe your change here -->

---

<!-- Consider each and tick it off one way or the other -->
* [x] CHANGELOG updated or not needed
* [x] Documentation updated or not needed
* [x] Haddocks updated or not needed
* [x] No new TODOs introduced or explained herafter
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.

5 participants