Fix bug related to non-canonical inline datums in UTxO JSON round-trips - #2746
Conversation
Transaction cost differencesNo cost or size differences found |
End-to-end benchmark differencesComparing this PR ( Baseline Scenario
Three local nodes
|
Transaction costsSizes and execution budgets for Hydra protocol transactions. Note that unlisted parameters are currently using
Script summary
|
| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
|---|---|---|---|---|
| 1 | 5351 | 9.19 | 3.03 | 0.48 |
| 2 | 5445 | 9.56 | 3.14 | 0.49 |
| 3 | 5545 | 10.01 | 3.29 | 0.50 |
| 5 | 5736 | 11.43 | 3.76 | 0.52 |
| 10 | 6217 | 13.51 | 4.40 | 0.56 |
| 50 | 10058 | 34.52 | 10.96 | 0.95 |
| 100 | 14859 | 61.43 | 19.39 | 1.44 |
| 115 | 16298 | 70.47 | 22.24 | 1.60 |
Cost of Increment Transaction
| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
|---|---|---|---|---|
| 1 | 2282 | 18.33 | 6.59 | 0.45 |
| 2 | 2406 | 19.33 | 7.53 | 0.47 |
| 3 | 2540 | 19.67 | 8.24 | 0.48 |
| 5 | 2806 | 21.49 | 10.04 | 0.52 |
| 10 | 3456 | 26.59 | 14.74 | 0.62 |
| 50 | 8700 | 65.75 | 51.54 | 1.44 |
| 75 | 11974 | 89.59 | 74.37 | 1.94 |
Cost of Decrement Transaction
| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
|---|---|---|---|---|
| 1 | 599 | 16.03 | 5.75 | 0.35 |
| 2 | 728 | 16.92 | 6.65 | 0.37 |
| 3 | 861 | 17.84 | 7.56 | 0.39 |
| 5 | 1125 | 19.62 | 9.35 | 0.43 |
| 10 | 1782 | 24.10 | 13.84 | 0.52 |
| 50 | 7021 | 62.14 | 50.30 | 1.32 |
| 75 | 10295 | 86.34 | 73.20 | 1.83 |
Close transaction costs
| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
|---|---|---|---|---|
| 1 | 593 | 15.49 | 10.37 | 0.38 |
| 3 | 858 | 17.31 | 12.18 | 0.42 |
| 5 | 1117 | 19.11 | 13.99 | 0.45 |
| 10 | 1773 | 23.66 | 18.51 | 0.55 |
| 50 | 7013 | 61.78 | 55.09 | 1.36 |
| 75 | 10288 | 86.14 | 78.09 | 1.86 |
Contest transaction costs
| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
|---|---|---|---|---|
| 1 | 619 | 18.79 | 13.48 | 0.43 |
| 2 | 751 | 19.85 | 14.43 | 0.45 |
| 3 | 890 | 20.97 | 15.40 | 0.47 |
| 5 | 1149 | 23.12 | 17.31 | 0.51 |
| 10 | 1799 | 28.45 | 22.07 | 0.62 |
| 50 | 7044 | 73.86 | 60.80 | 1.50 |
| 73 | 10057 | 99.29 | 82.87 | 1.99 |
FanOut transaction costs
Involves spending head output and burning head tokens. Uses ada-only UTXO for better comparability.
| Parties | UTxO | UTxO (bytes) | Tx size | % max Mem | % max CPU | Min fee ₳ |
|---|---|---|---|---|---|---|
| 10 | 0 | 0 | 5529 | 22.68 | 41.73 | 0.88 |
| 10 | 1 | 57 | 5564 | 25.03 | 44.25 | 0.92 |
| 10 | 5 | 285 | 5699 | 35.28 | 54.57 | 1.08 |
| 10 | 10 | 568 | 5867 | 49.29 | 67.85 | 1.30 |
| 10 | 20 | 1139 | 6208 | 81.82 | 95.83 | 1.78 |
| 10 | 20 | 1137 | 6206 | 81.82 | 95.83 | 1.78 |
PartialFanOut transaction costs
Largest chunk of ada-only outputs that can be distributed in one partial fanout step, computed dynamically. The last row is the maximum total UTxO count where at least one output can still be distributed.
| Distributed | UTxO (bytes) | Tx size | % max Mem | % max CPU | Min fee ₳ |
|---|---|---|---|---|---|
| 11 | 570 | 987 | 34.31 | 65.19 | 0.94 |
| 25 | 1309 | 1428 | 67.64 | 98.26 | 1.47 |
| 30 | 1308 | 1427 | 67.64 | 98.26 | 1.47 |
| 40 | 1309 | 1428 | 67.64 | 98.26 | 1.47 |
| 50 | 1307 | 1426 | 67.64 | 98.26 | 1.47 |
| 100 | 1309 | 1428 | 67.64 | 98.26 | 1.47 |
| 150 | 1307 | 1422 | 67.64 | 98.26 | 1.47 |
| 200 | 1307 | 1426 | 67.64 | 98.26 | 1.47 |
| 200 | 1309 | 1428 | 67.64 | 98.26 | 1.47 |
PartialFanOut transaction costs (with native tokens)
Largest chunk of native-token outputs that can be distributed in one partial fanout step, computed dynamically. The last row is the maximum total UTxO count where at least one output can still be distributed.
| Distributed | UTxO (bytes) | Tx size | % max Mem | % max CPU | Min fee ₳ |
|---|---|---|---|---|---|
| 11 | 1110 | 1598 | 41.40 | 67.74 | 1.04 |
| 25 | 2247 | 2500 | 75.99 | 98.03 | 1.58 |
| 30 | 2352 | 2610 | 75.97 | 98.07 | 1.59 |
| 40 | 1953 | 2192 | 75.97 | 97.97 | 1.57 |
| 50 | 2142 | 2391 | 75.99 | 98.03 | 1.58 |
| 100 | 1995 | 2237 | 75.97 | 97.97 | 1.57 |
| 150 | 2226 | 2475 | 75.97 | 98.03 | 1.58 |
| 200 | 2310 | 2567 | 75.99 | 98.08 | 1.58 |
| 200 | 2058 | 2303 | 75.99 | 97.98 | 1.57 |
FinalPartialFanOut transaction costs (with native tokens)
Terminal partial fanout step (FanoutProgress → Final) with outputs carrying a native token. Burns all head tokens and proves accumulator exhaustion via BLS proof.
| Distributed | UTxO (bytes) | Tx size | % max Mem | % max CPU | Min fee ₳ |
|---|---|---|---|---|---|
| 1 | 111 | 5415 | 21.54 | 43.18 | 0.87 |
| 5 | 535 | 5756 | 35.15 | 54.71 | 1.08 |
| 10 | 1120 | 6236 | 53.23 | 69.49 | 1.36 |
| 10 | 1070 | 6185 | 53.35 | 69.50 | 1.35 |
End-to-end benchmark results
This page is intended to collect the latest end-to-end benchmark results produced by Hydra's continuous integration (CI) system from the latest master code.
Please note that these results are approximate as they are currently produced from limited cloud VMs and not controlled hardware. Rather than focusing on the absolute results, the emphasis should be on relative results, such as how the timings for a scenario evolve as the code changes.
Generated at 2026-07-09 08:50:09.137562698 UTC
Baseline Scenario
| Number of nodes | 1 |
|---|---|
| Number of txs | 300 |
| Avg. Confirmation Time (ms) | 517.8 |
| P99 | 531.1ms |
| P95 | 530.8ms |
| P50 | 520.8ms |
| End-to-end TPS | 559.40 tx/s |
| Snapshots observed | 4 |
| Per-snapshot TPS P50 | 4133.07 tx/s |
| Per-snapshot TPS P95 | 8102.01 tx/s |
| Per-snapshot TPS max | 8530.62 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Three local nodes
| Number of nodes | 3 |
|---|---|
| Number of txs | 900 |
| Avg. Confirmation Time (ms) | 2581.5 |
| P99 | 2847.8ms |
| P95 | 2831.0ms |
| P50 | 2630.5ms |
| End-to-end TPS | 313.08 tx/s |
| Snapshots observed | 10 |
| Per-snapshot TPS P50 | 1014.29 tx/s |
| Per-snapshot TPS P95 | 2647.76 tx/s |
| Per-snapshot TPS max | 2928.10 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Scenario benchmark results
This page collects results from the scenario matrix: every combination of cluster size, UTxO shape, and incremental-ops mode is exercised by CI from the latest master code and reported below.
Numbers are approximate. They come from cloud VMs rather than controlled hardware, so the useful signal is the relative change between cells and between commits, not the absolute throughput.
Generated at 2026-07-09 08:59:19.226145621 UTC
Summary across cells
TPS columns are rates (transactions per second); Wall clock (s) is the measured elapsed time from the first tx submission to the last confirmation. Times are rounded to one decimal.
| Scenario | Txs | Wall clock (s) | End-to-end TPS (tx/s) | Per-snapshot p50 TPS (tx/s) | Avg conf (ms) | P95 conf (ms) |
|---|---|---|---|---|---|---|
| Nodes=1, Constant, incremental ops off, fire and forget | 30 | 0.1 | 458.50 | 2215.91 | 64.7 | 65.1 |
| Nodes=1, Constant, incremental ops off, wait for tx valid | 30 | 0.2 | 161.73 | 174.95 | 6.1 | 8.5 |
| Nodes=1, Growing, incremental ops off, fire and forget | 30 | 0.1 | 415.40 | 865.02 | 71.1 | 72.0 |
| Nodes=1, Growing, incremental ops off, wait for tx valid | 30 | 0.3 | 117.44 | 118.05 | 8.4 | 10.9 |
| Nodes=1, Mixed, incremental ops off, fire and forget | 30 | 0.1 | 488.34 | 2084.73 | 60.7 | 61.2 |
| Nodes=1, Mixed, incremental ops off, wait for tx valid | 30 | 0.2 | 131.95 | 136.39 | 7.5 | 10.8 |
| Nodes=2, Constant, incremental ops off, fire and forget | 60 | 0.2 | 397.65 | 1624.09 | 149.1 | 149.9 |
| Nodes=2, Constant, incremental ops off, wait for tx valid | 60 | 0.5 | 116.16 | 122.26 | 17.0 | 22.7 |
| Nodes=2, Growing, incremental ops off, fire and forget | 60 | 0.2 | 334.79 | 683.54 | 177.1 | 178.9 |
| Nodes=2, Growing, incremental ops off, wait for tx valid | 60 | 0.9 | 64.19 | 68.07 | 30.6 | 45.3 |
| Nodes=2, Mixed, incremental ops off, fire and forget | 60 | 0.2 | 387.49 | 1978.97 | 153.3 | 154.5 |
| Nodes=2, Mixed, incremental ops off, wait for tx valid | 60 | 0.7 | 80.73 | 88.65 | 24.5 | 36.1 |
| Nodes=3, Constant, incremental ops off, fire and forget | 90 | 0.3 | 350.49 | 1598.58 | 253.9 | 256.4 |
| Nodes=3, Constant, incremental ops off, wait for tx valid | 90 | 1.0 | 93.25 | 84.25 | 31.5 | 44.0 |
| Nodes=3, Growing, incremental ops off, fire and forget | 90 | 0.3 | 272.34 | 489.33 | 325.6 | 329.1 |
| Nodes=3, Growing, incremental ops off, wait for tx valid | 90 | 2.0 | 44.76 | 44.46 | 63.6 | 99.9 |
| Nodes=3, Mixed, incremental ops off, fire and forget | 90 | 0.3 | 317.93 | 1015.10 | 278.6 | 281.8 |
| Nodes=3, Mixed, incremental ops off, wait for tx valid | 90 | 1.4 | 63.38 | 60.95 | 46.3 | 65.7 |
Nodes=1, Constant, incremental ops off, fire and forget
| Number of nodes | 1 |
|---|---|
| Number of txs | 30 |
| Avg. Confirmation Time (ms) | 64.7 |
| P99 | 65.2ms |
| P95 | 65.1ms |
| P50 | 64.9ms |
| End-to-end TPS | 458.50 tx/s |
| Snapshots observed | 2 |
| Per-snapshot TPS P50 | 2215.91 tx/s |
| Per-snapshot TPS P95 | 4194.94 tx/s |
| Per-snapshot TPS max | 4370.85 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=1, Constant, incremental ops off, wait for tx valid
| Number of nodes | 1 |
|---|---|
| Number of txs | 30 |
| Avg. Confirmation Time (ms) | 6.1 |
| P99 | 9.0ms |
| P95 | 8.5ms |
| P50 | 5.6ms |
| End-to-end TPS | 161.73 tx/s |
| Snapshots observed | 30 |
| Per-snapshot TPS P50 | 174.95 tx/s |
| Per-snapshot TPS P95 | 188.69 tx/s |
| Per-snapshot TPS max | 190.83 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=1, Growing, incremental ops off, fire and forget
| Number of nodes | 1 |
|---|---|
| Number of txs | 30 |
| Avg. Confirmation Time (ms) | 71.1 |
| P99 | 72.0ms |
| P95 | 72.0ms |
| P50 | 71.6ms |
| End-to-end TPS | 415.40 tx/s |
| Snapshots observed | 2 |
| Per-snapshot TPS P50 | 865.02 tx/s |
| Per-snapshot TPS P95 | 1627.26 tx/s |
| Per-snapshot TPS max | 1695.01 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=1, Growing, incremental ops off, wait for tx valid
| Number of nodes | 1 |
|---|---|
| Number of txs | 30 |
| Avg. Confirmation Time (ms) | 8.4 |
| P99 | 13.0ms |
| P95 | 10.9ms |
| P50 | 8.4ms |
| End-to-end TPS | 117.44 tx/s |
| Snapshots observed | 30 |
| Per-snapshot TPS P50 | 118.05 tx/s |
| Per-snapshot TPS P95 | 161.10 tx/s |
| Per-snapshot TPS max | 171.01 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=1, Mixed, incremental ops off, fire and forget
Each client first grows its UTxO set (1-in to 2-out) for half of its tx budget, then contracts it back (2-in to 1-out) for the remainder.
| Number of nodes | 1 |
|---|---|
| Number of txs | 30 |
| Avg. Confirmation Time (ms) | 60.7 |
| P99 | 61.2ms |
| P95 | 61.2ms |
| P50 | 61.0ms |
| End-to-end TPS | 488.34 tx/s |
| Snapshots observed | 2 |
| Per-snapshot TPS P50 | 2084.73 tx/s |
| Per-snapshot TPS P95 | 3944.46 tx/s |
| Per-snapshot TPS max | 4109.77 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=1, Mixed, incremental ops off, wait for tx valid
Each client first grows its UTxO set (1-in to 2-out) for half of its tx budget, then contracts it back (2-in to 1-out) for the remainder.
| Number of nodes | 1 |
|---|---|
| Number of txs | 30 |
| Avg. Confirmation Time (ms) | 7.5 |
| P99 | 11.9ms |
| P95 | 10.8ms |
| P50 | 7.2ms |
| End-to-end TPS | 131.95 tx/s |
| Snapshots observed | 30 |
| Per-snapshot TPS P50 | 136.39 tx/s |
| Per-snapshot TPS P95 | 177.98 tx/s |
| Per-snapshot TPS max | 196.75 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=2, Constant, incremental ops off, fire and forget
| Number of nodes | 2 |
|---|---|
| Number of txs | 60 |
| Avg. Confirmation Time (ms) | 149.1 |
| P99 | 150.0ms |
| P95 | 149.9ms |
| P50 | 149.4ms |
| End-to-end TPS | 397.65 tx/s |
| Snapshots observed | 2 |
| Per-snapshot TPS P50 | 1624.09 tx/s |
| Per-snapshot TPS P95 | 3078.96 tx/s |
| Per-snapshot TPS max | 3208.28 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=2, Constant, incremental ops off, wait for tx valid
| Number of nodes | 2 |
|---|---|
| Number of txs | 60 |
| Avg. Confirmation Time (ms) | 17.0 |
| P99 | 26.3ms |
| P95 | 22.7ms |
| P50 | 16.2ms |
| End-to-end TPS | 116.16 tx/s |
| Snapshots observed | 60 |
| Per-snapshot TPS P50 | 122.26 tx/s |
| Per-snapshot TPS P95 | 152.64 tx/s |
| Per-snapshot TPS max | 167.19 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=2, Growing, incremental ops off, fire and forget
| Number of nodes | 2 |
|---|---|
| Number of txs | 60 |
| Avg. Confirmation Time (ms) | 177.1 |
| P99 | 179.0ms |
| P95 | 178.9ms |
| P50 | 177.7ms |
| End-to-end TPS | 334.79 tx/s |
| Snapshots observed | 2 |
| Per-snapshot TPS P50 | 683.54 tx/s |
| Per-snapshot TPS P95 | 1292.05 tx/s |
| Per-snapshot TPS max | 1346.14 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=2, Growing, incremental ops off, wait for tx valid
| Number of nodes | 2 |
|---|---|
| Number of txs | 60 |
| Avg. Confirmation Time (ms) | 30.6 |
| P99 | 49.9ms |
| P95 | 45.3ms |
| P50 | 30.7ms |
| End-to-end TPS | 64.19 tx/s |
| Snapshots observed | 60 |
| Per-snapshot TPS P50 | 68.07 tx/s |
| Per-snapshot TPS P95 | 104.35 tx/s |
| Per-snapshot TPS max | 115.79 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=2, Mixed, incremental ops off, fire and forget
Each client first grows its UTxO set (1-in to 2-out) for half of its tx budget, then contracts it back (2-in to 1-out) for the remainder.
| Number of nodes | 2 |
|---|---|
| Number of txs | 60 |
| Avg. Confirmation Time (ms) | 153.3 |
| P99 | 154.6ms |
| P95 | 154.5ms |
| P50 | 153.6ms |
| End-to-end TPS | 387.49 tx/s |
| Snapshots observed | 2 |
| Per-snapshot TPS P50 | 1978.97 tx/s |
| Per-snapshot TPS P95 | 3753.57 tx/s |
| Per-snapshot TPS max | 3911.31 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=2, Mixed, incremental ops off, wait for tx valid
Each client first grows its UTxO set (1-in to 2-out) for half of its tx budget, then contracts it back (2-in to 1-out) for the remainder.
| Number of nodes | 2 |
|---|---|
| Number of txs | 60 |
| Avg. Confirmation Time (ms) | 24.5 |
| P99 | 66.3ms |
| P95 | 36.1ms |
| P50 | 22.7ms |
| End-to-end TPS | 80.73 tx/s |
| Snapshots observed | 60 |
| Per-snapshot TPS P50 | 88.65 tx/s |
| Per-snapshot TPS P95 | 129.51 tx/s |
| Per-snapshot TPS max | 151.31 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=3, Constant, incremental ops off, fire and forget
| Number of nodes | 3 |
|---|---|
| Number of txs | 90 |
| Avg. Confirmation Time (ms) | 253.9 |
| P99 | 256.5ms |
| P95 | 256.4ms |
| P50 | 255.0ms |
| End-to-end TPS | 350.49 tx/s |
| Snapshots observed | 2 |
| Per-snapshot TPS P50 | 1598.58 tx/s |
| Per-snapshot TPS P95 | 3033.34 tx/s |
| Per-snapshot TPS max | 3160.87 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=3, Constant, incremental ops off, wait for tx valid
| Number of nodes | 3 |
|---|---|
| Number of txs | 90 |
| Avg. Confirmation Time (ms) | 31.5 |
| P99 | 47.0ms |
| P95 | 44.0ms |
| P50 | 31.0ms |
| End-to-end TPS | 93.25 tx/s |
| Snapshots observed | 62 |
| Per-snapshot TPS P50 | 84.25 tx/s |
| Per-snapshot TPS P95 | 173.36 tx/s |
| Per-snapshot TPS max | 208.75 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=3, Growing, incremental ops off, fire and forget
| Number of nodes | 3 |
|---|---|
| Number of txs | 90 |
| Avg. Confirmation Time (ms) | 325.6 |
| P99 | 329.2ms |
| P95 | 329.1ms |
| P50 | 328.1ms |
| End-to-end TPS | 272.34 tx/s |
| Snapshots observed | 2 |
| Per-snapshot TPS P50 | 489.33 tx/s |
| Per-snapshot TPS P95 | 925.93 tx/s |
| Per-snapshot TPS max | 964.74 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=3, Growing, incremental ops off, wait for tx valid
| Number of nodes | 3 |
|---|---|
| Number of txs | 90 |
| Avg. Confirmation Time (ms) | 63.6 |
| P99 | 129.9ms |
| P95 | 99.9ms |
| P50 | 62.7ms |
| End-to-end TPS | 44.76 tx/s |
| Snapshots observed | 64 |
| Per-snapshot TPS P50 | 44.46 tx/s |
| Per-snapshot TPS P95 | 103.57 tx/s |
| Per-snapshot TPS max | 148.53 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=3, Mixed, incremental ops off, fire and forget
Each client first grows its UTxO set (1-in to 2-out) for half of its tx budget, then contracts it back (2-in to 1-out) for the remainder.
| Number of nodes | 3 |
|---|---|
| Number of txs | 90 |
| Avg. Confirmation Time (ms) | 278.6 |
| P99 | 281.9ms |
| P95 | 281.8ms |
| P50 | 280.9ms |
| End-to-end TPS | 317.93 tx/s |
| Snapshots observed | 2 |
| Per-snapshot TPS P50 | 1015.10 tx/s |
| Per-snapshot TPS P95 | 1924.88 tx/s |
| Per-snapshot TPS max | 2005.75 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
Nodes=3, Mixed, incremental ops off, wait for tx valid
Each client first grows its UTxO set (1-in to 2-out) for half of its tx budget, then contracts it back (2-in to 1-out) for the remainder.
| Number of nodes | 3 |
|---|---|
| Number of txs | 90 |
| Avg. Confirmation Time (ms) | 46.3 |
| P99 | 71.6ms |
| P95 | 65.7ms |
| P50 | 44.9ms |
| End-to-end TPS | 63.38 tx/s |
| Snapshots observed | 62 |
| Per-snapshot TPS P50 | 60.95 tx/s |
| Per-snapshot TPS P95 | 143.46 tx/s |
| Per-snapshot TPS max | 166.04 tx/s |
| Number of Invalid txs | 0 |
| Fanout outputs | 0 |
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. Signed-off-by: Sasha Bogicevic <sasha.bogicevic@iohk.io>
Signed-off-by: Sasha Bogicevic <sasha.bogicevic@iohk.io>
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