Repository: cardano-scaling/hydra
Pull request #1928: Don't rely on local state when observing decrement and recover txs
Author: login: v0d1ch
Created: 2025-04-02T14:33:27Z | Merged: 2025-04-10T11:27:51Z

- We should not prevent any on-chain information from propagating further since the only source of truth is actually what we observe from L1. This PR removes some parts of the code that made some assumptions using local state of a hydra-node which we can't really trust. 

This PR goes together with the one removing related check from the hydra specification https://github.com/cardano-scaling/hydra/issues/1930

---

<!-- 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



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e700e4c8e8a..e340d024384 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,8 @@ changes.
 
 - Introduce an option to publish hydra scripts using blockfrost.
 
+- Remove checks that rely on hydra-node's local state and trust on-chain data when we observe decrement/recover transactions.
+
 - Fix a bug in increment observation where wrong deposited UTxO was picked up.
 
 - Fix a bug where incremental commits / decommits were not correctly observed after restart of `hydra-node`. This was due to incorrect handling of internal chain state [#1894](https://github.com/cardano-scaling/hydra/pull/1894)
diff --git a/flake.lock b/flake.lock
index b708113bd85..80b5a5c05be 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1459,11 +1459,11 @@
         "nixpkgs": "nixpkgs_9"
       },
       "locked": {
-        "lastModified": 1738003268,
-        "narHash": "sha256-VdCO1qCzJBOiO+c6mdtGcJnRfbCRH7xYo9mt+fr63Vo=",
+        "lastModified": 1744277032,
+        "narHash": "sha256-Z83c60ZY8JQ3J7w1lD/6raEIuiXdxjGHXP/FND4YHLA=",
         "owner": "cardano-scaling",
         "repo": "hydra-formal-specification",
-        "rev": "27d17772d37c202bbb11d979d0e3f549838cb6f4",
+        "rev": "a626ac70c7b74ccfa6432512ad752ef37d80a1c6",
         "type": "github"
       },
       "original": {
diff --git a/hydra-chain-observer/golden/OnChainTx/OnDecrementTx.json b/hydra-chain-observer/golden/OnChainTx/OnDecrementTx.json
index 89ff7f8e8fd..e572be2183a 100644
--- a/hydra-chain-observer/golden/OnChainTx/OnDecrementTx.json
+++ b/hydra-chain-observer/golden/OnChainTx/OnDecrementTx.json
@@ -1,386 +1,26 @@
 {
     "samples": [
         {
-            "distributedOutputs": [
-                {
-                    "address": "addr_test1zqfwscgctst2l273ulge5574fuuq5ch33pgkaad4pnc7984sdlz7cj9qkle6lkwx0nna9nwhsq74hkdcssnutyxsv4sqzt2ayk",
-                    "datum": null,
-                    "datumhash": "13bc09f1a3e8c38aef2fd2c2c4878aae4946e4883dcbb66f7da8269c6bdd2355",
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
-                    "referenceScript": null,
-                    "value": {
-                        "lovelace": 2909139593927983884
-                    }
-                },
-                {
-                    "address": "addr_test1qzplk39zcy4uv0mkmh6592kq6emvc90r75dwen5evf40ena38cemr3ku34rzlv975tazqe793wjetc06pcg44vfg5y2s7wp3wp",
+            "distributedUTxO": {
+                "0000000100000001010100000100000100000000010100010001000000000000#9": {
+                    "address": "addr_test1yzlgdm2wyumekl0sm4uvgulmu6xlzzdc93q4qd52me5cyh9cha7lggwvpw9x5kfhapta4kwmsrj32v0vp4x4ukxlss5qwtz89e",
                     "datum": null,
                     "inlineDatum": {
-                        "constructor": 3,
-                        "fields": []
+                        "int": 4
                     },
-                    "inlineDatumRaw": "d87c80",
-                    "inlineDatumhash": "67d8ed01e13f33438ea9059ac9be2e159f943cffe054283485e0300271e3e9f9",
-                    "referenceScript": null,
-                    "value": {
-                        "4a1c412d8e2b3015a7fb7d382808fb7cb721bf93a56e8bb6661cdebe": {
-                            "059aca0e2bae510b1456085c2ad16953e4fbb4ee027b94ec": 2
-                        },
-                        "4d50a11e297e7783383bf06dd6e4e481230323bd96cd8b8d9ee3888d": {
-                            "e1f99e575804": 4494234613279104079
-                        },
-                        "lovelace": 5734827832589330678
-                    }
-                },
-                {
-                    "address": "addr_test1zp007hpaj7vrge98f7hxj74laqt39zy3gylnfj7gqyqfgskqvu7xnse7568jx9ykx50jvzs99x048jndjcn9qemjzcpqyrx6pn",
-                    "datum": null,
-                    "datumhash": "1f398f4ae66b4bf1067c2e555292e0207d033cfc02eef07c5d1eef95caf3cbda",
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
-                    "referenceScript": null,
-                    "value": {
-                        "04517997d3f606ffb7b699150ebb341470423cc8347bdf2cbbe43051": {
-                            "1770": 2
-                        },
-                        "lovelace": 10000002
-                    }
-                },
-                {
-                    "address": "addr_test1yq2trrx2paezxhenfmtjn7y760uy5ye9v6frgquj3qs0yyrwuv906e3xdympn985kwms6x94zha95t2y4dlcqts90xlsmvlf7v",
-                    "datum": null,
-                    "datumhash": null,
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
-                    "referenceScript": null,
-                    "value": {
-                        "lovelace": 8712745632667878423
-                    }
-                },
-                {
-                    "address": "addr_test1yr5hkpwfnjq8aks8e4jaw7u9gxsmpe8a37amfdh30m5yvrrt6jl89nv5v28unuv8v3esnhj2rfr3f0xfecm632yxwxvs6mphpm",
-                    "datum": null,
-                    "datumhash": "c384f98605feeda979f7b7b67487cd15727120bd543a83d190d28639e8b1d8f4",
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
-                    "referenceScript": null,
-                    "value": {
-                        "lovelace": 2141999343719942403
-                    }
-                },
-                {
-                    "address": "addr_test1wqll9x026d9krjfwqy0evxeqtjyd54dhr56fv3m8hs4vzds3akkc8",
-                    "datum": null,
-                    "datumhash": "9921e4f5f8c74a1a2ad03f8934cdaa7639fb873fbf569d32e8976500e3795462",
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
-                    "referenceScript": null,
-                    "value": {
-                        "7da79c20e232fb1822509d410d4ebf7fd4d18aa577ec763e7a13a935": {
-                            "3f49944b9f65c0fa6d": 2679775817426321015
-                        },
-                        "dbfc21ccd2f325b06f4ffdd806713223ca44343394ad1023a04b9a07": {
-                            "88f5d475b4759f3acee1cb2f18": 1
-                        },
-                        "lovelace": 10000001
-                    }
-                },
-                {
-                    "address": "addr_test1xpmfresfhtej429mk7njdg3kq2088tc55l8vhldj44utlq4pjw6hzjffse6ey77mezkn9sdd05zdl9vqhhy8a35an2wsd82w5g",
-                    "datum": null,
-                    "datumhash": null,
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
-                    "referenceScript": null,
-                    "value": {
-                        "lovelace": 10000001
-                    }
-                },
-                {
-                    "address": "addr_test1qrkgvjcuxyy78a09xxw5yg0qrx4f0hp69a6nfvrdeaac0nvg2cydsm67h8md6z60640hgrnaccgeguwju422q3cwwnuqtlvpkd",
-                    "datum": null,
-                    "datumhash": null,
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
-                    "referenceScript": null,
-                    "value": {
-                        "8f461954fe2f18fee1dca233f358907e643ff839ed1f995e4bf325e3": {
-                            "36": 5140655727312317857,
-                            "a7fe15476be1273e8e090bd5fb6a8397": 2
-                        },
-                        "lovelace": 3425546695661591285
-                    }
-                },
-                {
-                    "address": "addr_test1zzr29vrlag0gqk4q7g6y3avmtjhhxwf33knpa2rjzhpa57yhz2xayw95vc93ws7aly925ll900y9d4a3n9j2txl6gw7swu2et5",
-                    "datum": null,
-                    "datumhash": null,
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
+                    "inlineDatumRaw": "04",
+                    "inlineDatumhash": "642206314f534b29ad297d82440a5f9f210e30ca5ced805a587ca402de927342",
                     "referenceScript": null,
                     "value": {
-                        "lovelace": 4291012293855591668
-                    }
-                },
-                {
-                    "address": "addr_test1wrj2vj6avqdqz8qz57qpg7mddaqn0m0xwcjdjcadurkh3gs7x6v7p",
-                    "datum": null,
-                    "datumhash": "d32d703d97bc603f39f34e9bc63fe863e2b73dcd6d4b0a3f022ffe2a233fe57a",
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
-                    "referenceScript": null,
-                    "value": {
-                        "lovelace": 3650437601711463258
-                    }
-                },
-                {
-                    "address": "addr_test1xqp7et8lfazvgwrlt045vsnfnr8hsqtuadc0tuy34ukcjp9du8gchsdg2srzhu62wlzcyty2n5l6332m04s4hktsmzmsmd3j5c",
-                    "datum": null,
-                    "inlineDatum": {
-                        "bytes": "5aed"
-                    },
-                    "inlineDatumRaw": "425aed",
-                    "inlineDatumhash": "43a49636bc1d57b47c20809b2cc06b4cc8c268875b971c94fb774a342d38efb4",
-                    "referenceScript": null,
-                    "value": {
-                        "105a8f1bb56444cacc86378c95421aceeb326b0fb7743e493eb82fd5": {
-                            "664a21bf5fe7f22bbf8db20aa5a87215843998cb": 4551618346603932068,
-                            "a4fc80a6": 1
-                        },
-                        "lovelace": 10000001
-                    }
-                },
-                {
-                    "address": "addr_test1wr8e82f8t5a4cd2wcnmkqqcs0xfqqtfftlxutqyzwehxjqg7u42dn",
-                    "datum": null,
-                    "datumhash": null,
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
-                    "referenceScript": null,
-                    "value": {
-                        "5a7b5a5e74d7ca6594f2c2ea9479957c1042c0400e911d3142e7ea6e": {
-                            "33": 4093335798517418394
-                        },
-                        "lovelace": 10000001
-                    }
-                },
-                {
-                    "address": "addr_test1xqyv40qgj0lym5hkr9tslq2a7vsa8fqd8v9gxq8yel3mvj7dwumylr99a0q73xv042xsx0k2afvmaa7h3nx8nsj6wx8suyx6ay",
-                    "datum": null,
-                    "datumhash": null,
-                    "inlineDatum": null,
-                    "inlineDatumRaw": null,
-                    "referenceScript": null,
-                    "value": {
-                        "b0c53e2bf180858da4b64eb5598c5615bba7d723d2b604a83b7f9165": {
-                            "22ea0eb2d49637596131da4a3d44d3115c8b08": 3
-                        },
-                        "b3d07b926ab5566262d527cddd69d15c7159fc46c243737710d90289": {
-                            "92ee4b0bf1a5640a58c96932f2805269ba3fedccc299e368d71cc63a12": 2
-                        },
-                        "lovelace": 865423705214936192
-                    }
-                },
-                {
-                    "address": "addr_test1xraagce9z47dk0fmj5q7r4rwy4s9k84adwhcfucw9vw2g7c2lhq6pp8j92c6qs6dnn4mxfyf2k4ls8rk42ermutn43js4s2vvz",
-                    "datum": null,
-                    "inlineDatum": {
-                        "list": [
-                            {
-                                "bytes": "2bbc"
-                            },
-                            {
-                                "int": 1
-                            },
-                            {
-                                "list": [
-                                    {
-                                        "list": []
-                                    },
-                                    {
-                                        "list": []
-                                    },
-                                    {
-                                        "constructor": 3,
-                                        "fields": [
-                                            {
-                                                "int": -1
-                                            }
-                                        ]
-                                    },
-                                    {
-                                        "int": 5
-                                    }
-                                ]
-                            },
-                            {
-                                "map": []
-                            },
-                            {
-                                "constructor": 2,
-                                "fields": [
-                                    {
-                                        "list": [
-                                            {
-                                                "int": -2
-                                            },
-                                            {
-                                                "int": -3
-                                            }
-                                        ]
-                                    },
-                                    {
-                                        "list": [
-                                            {
-                                                "bytes": "ed1a39"
-                                            },
-                                            {
-                                                "bytes": "95"
-                                            },
-                                            {
-                                                "bytes": "08dc"
-                                            },
-                                            {
-                                                "bytes": "29bbc9b5"
-                                            },
-                                            {
-                                                "int": 1
-                                            }
-                                        ]
-                                    }
-                                ]
-                            }
-                        ]
-                    },
-                    "inlineDatumRaw": "9f422bbc019f8080d87c9f20ff05ffa0d87b9f9f2122ff9f43ed1a3941954208dc4429bbc9b501ffffff",
-                    "inlineDatumhash": "8482ed1d11f0d0b2572762f3e76514fc7846a6b172b44fec30ecea464a698f5b",
-                    "referenceScript": null,
-                    "value": {
-                        "lovelace": 973712514995818882
-                    }
-                },
-                {
-                    "address": "addr_test1xzljczp8s4f950w3a9qvtdjz2zkwdgzf4cv9mqd0p4celwpdzk7rk7hhvew0x97ruh32mnqgat94ae36juuw032cffqss9fpfa",
-                    "datum": null,
-                    "inlineDatum": {
-                        "bytes": "2795"
-                    },
-                    "inlineDatumRaw": "422795",
-                    "inlineDatumhash": "11dd532efba433f36bb385cad0f8470057cb90ce8e072f40236a5f73ffce203b",
-                    "referenceScript": null,
-                    "value": {
-                        "lovelace": 979912137287988031
-                    }
-                },
-                {
-                    "address": "addr_test1wz5xy5ka5c4zlvdexam8p4xqxeupphxf06n9kglh4nq558sjz2rv5",
-                    "datum": null,
-                    "inlineDatum": {
-                        "constructor": 4,
-                        "fields": [
-                            {
-                                "int": 3
-                            },
-                            {
-                                "map": [
-                                    {
-                                        "k": {
-                                            "int": 0
-                                        },
-                                        "v": {
-                                            "constructor": 0,
-                                            "fields": [
-                                                {
-                                                    "int": 1
-                                                },
-                                                {
-                                                    "bytes": "e521"
-                                                },
-                                                {
-                                                    "int": -2
-                                                },
-                                                {
-                                                    "bytes": "d9"
-                                                }
-                                            ]
-                                        }
-                                    }
-                                ]
-                            },
-                            {
-                                "constructor": 0,
-                                "fields": [
-                                    {
-                                        "constructor": 1,
-                                        "fields": []
-                                    }
-                                ]
-                            },
-                            {
-                                "int": 4
-                            },
-                            {
-                                "constructor": 4,
-                                "fields": [
-                                    {
-                                        "constructor": 4,
-                                        "fields": [
-                                            {
-                                                "int": 0
-                                            },
-                                            {
-                                                "int": 0
-                                            },
-                                            {
-                                                "int": 2
-                                            },
-                                            {
-                                                "bytes": "52e46f"
-                                            }
-                                        ]
-                                    },
-                                    {
-                                        "list": [
-                                            {
-                                                "bytes": "ac4b"
-                                            },
-                                            {
-                                                "bytes": "545c"
-                                            },
-                                            {
-                                                "bytes": "1c5719ba"
-                                            },
-                                            {
-                                                "bytes": "bc67a381"
-                                            },
-                                            {
-                                                "bytes": "636192"
-                                            }
-                                        ]
-                                    }
-                                ]
-                            }
-                        ]
-                    },
-                    "inlineDatumRaw": "d87d9f03a100d8799f0142e5212141d9ffd8799fd87a80ff04d87d9fd87d9f0000024352e46fff9f42ac4b42545c441c5719ba44bc67a38143636192ffffff",
-                    "inlineDatumhash": "c8800a3f6ad34cdd1823b85bf3011603e78028426025dd6224f1e280690b6b8e",
-                    "referenceScript": null,
-                    "value": {
-                        "105a8f1bb56444cacc86378c95421aceeb326b0fb7743e493eb82fd5": {
-                            "439130334dfebd0602db50a444650e01ef35171cd9a4b843745548": 3218821190953200038,
-                            "8fe33aabbb3447": 3
-                        },
-                        "453ff22fdb0729dab55252163f352b2e928525eb9667854d876f5582": {
-                            "0c807eaf560862d582c356e9f128b9e0": 6225489420224506439,
-                            "34": 3
+                        "5800acd842a25f5fe3345c91a859c00affefd47017a6ecf3345d50b0": {
+                            "367187cceecb5538d46cdd05c9b8fc": 8017389128832664882
                         },
-                        "lovelace": 10000001
+                        "lovelace": 7447919622186170647
                     }
                 }
-            ],
-            "headId": "be50a63fdd88c5552aeb6a2808a1e418",
-            "newVersion": 18,
+            },
+            "headId": "01010100000100000000010001010100",
+            "newVersion": 1,
             "tag": "OnDecrementTx"
         }
     ],
diff --git a/hydra-chain-observer/golden/OnChainTx/OnRecoverTx.json b/hydra-chain-observer/golden/OnChainTx/OnRecoverTx.json
index 6ab4040a349..04ebceca8e9 100644
--- a/hydra-chain-observer/golden/OnChainTx/OnRecoverTx.json
+++ b/hydra-chain-observer/golden/OnChainTx/OnRecoverTx.json
@@ -1,8 +1,31 @@
 {
     "samples": [
         {
-            "headId": "a42836de904bad82fe8684bee8a92cab",
-            "recoveredTxId": "8c912bcc3f52bb968cbe13d560ee871b4f11d26fab67a8bc7eb22f3d5b8ff8a8",
+            "headId": "01010001010101010000010000010100",
+            "recoveredTxId": "0101010001000101010101010000000100010101000101010000010100000000",
+            "recoveredUTxO": {
+                "0000010100000001010001010001000001010000000100010001010100010001#19": {
+                    "address": "addr1x92vfd793n7lrdf25997kcer6e6jw7uhqpe00ey5ankrgdk007z0vphatrd0wnnqtn03xtwm8a047zdfxzx5tdfcx3zqu0sl53",
+                    "datum": null,
+                    "datumhash": "41755210135075cee7ff8d57236c51b4ed7c3c4867d088794ccba361f4752f97",
+                    "inlineDatum": null,
+                    "inlineDatumRaw": null,
+                    "referenceScript": {
+                        "script": {
+                            "cborHex": "830303848202848202828200581c9bbabc12319ea48352a6a835890bd8fad0f14ab9ec0a0f251894184b8200581cd9b1df6c1ac835f8513c8cd0de277abf304a334d6d438e937817b53c830302838200581ce61b4b8251e699e032b4f4f43f4ea0c25b035f7d43f4b20211cbda028200581c21d70ae5ee17527d8e2f82688bcc20bd4d3eed680df390871412f4398200581cdeb1647f384d13c10570fa5c1161e7150afa5cacc8202bd9441de9308200581c2720245cb48418676d67bf8888272011a66d7bd6e39b4f64ce69b2e2830301818200581c33c415f5ab19d59ae612cb5ee119f07dbd7db9ebac6df054adb8f2d78201818201838200581c30faeb1e5fbb28ce466fc4da0cd5981845336a2dd02231441ce54f658200581c87b10fbac658e08c486e1ee81a5f5abdcbf3f136aea3e79bbb1bf1788200581c6d82b551bda55b8949a1d38d6dd4d59d7a0621d518d6bd25a385946c830303848200581ceaf3ebce47ee589cb3883e0bfccb9c6953aa78b4065d10c20df34e2b8202848200581cfbebf90ad162b1cd95ceaabfc25bd28980549fbea8f6dc1d448781178200581c16d184c4e3838078e4f550bd75556b63e8661e61c43c72f859fcddb18200581ce244244c609691dd6b104adfffa5fabb2f22a1b0d6d76bcf4475c7bf8200581cb76f0f9cb9010b48fecfba785686b76864926bce8c0ea474a3f47eac8200581ca3020a1e15cea47d4cbad968f6870e74531b831f5bde8a6d449684438200581cf4d02e30e2a91897ea4ab473f39fac97a9e7072534e49eff5f1da150820280",
+                            "description": "",
+                            "type": "SimpleScript"
+                        },
+                        "scriptLanguage": "SimpleScriptLanguage"
+                    },
+                    "value": {
+                        "245d5a7a06fe18358242e81281cd5ba9e6abe4efc54e7b659f25abae": {
+                            "55": 590494318831573294
+                        },
+                        "lovelace": 878270261755167725
+                    }
+                }
+            },
             "tag": "OnRecoverTx"
         }
     ],
diff --git a/hydra-cluster/src/Hydra/Cluster/Scenarios.hs b/hydra-cluster/src/Hydra/Cluster/Scenarios.hs
index adb589f819f..b221eeffc5b 100644
--- a/hydra-cluster/src/Hydra/Cluster/Scenarios.hs
+++ b/hydra-cluster/src/Hydra/Cluster/Scenarios.hs
@@ -54,7 +54,7 @@ import Hydra.Cardano.Api (
   LedgerProtocolParameters (..),
   PaymentKey,
   Tx,
-  TxId,
+  TxId (..),
   UTxO,
   addTxIns,
   addTxInsCollateral,
@@ -150,6 +150,7 @@ import Network.HTTP.Req (
 import Network.HTTP.Simple (getResponseBody, httpJSON, setRequestBodyJSON)
 import Network.HTTP.Types (urlEncode)
 import System.FilePath ((</>))
+import System.Process (proc, readCreateProcessWithExitCode)
 import Test.Hydra.Tx.Fixture (testNetworkId)
 import Test.Hydra.Tx.Gen (genKeyPair)
 import Test.QuickCheck (choose, elements, generate)
@@ -307,6 +308,131 @@ restartedNodeCanAbort tracer workDir cardanoNode hydraScriptsTxId = do
  where
   RunningNode{nodeSocket, networkId} = cardanoNode
 
+nodeReObservesOnChainTxs :: Tracer IO EndToEndLog -> FilePath -> RunningNode -> [TxId] -> IO ()
+nodeReObservesOnChainTxs tracer workDir cardanoNode hydraScriptsTxId = do
+  refuelIfNeeded tracer cardanoNode Alice 100_000_000
+  refuelIfNeeded tracer cardanoNode Bob 100_000_000
+  -- Start hydra-node on chain tip
+  tip <- queryTip networkId nodeSocket
+  let contestationPeriod = UnsafeContestationPeriod 2
+  let deadline = 10
+  let depositDeadline = UnsafeDepositDeadline deadline
+  aliceChainConfig <-
+    chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [Bob] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  bobChainConfig <-
+    chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  (aliceCardanoVk, aliceCardanoSk) <- keysFor Alice
+  commitUTxO <- seedFromFaucet cardanoNode aliceCardanoVk 5_000_000 (contramap FromFaucet tracer)
+
+  let hydraTracer = contramap FromHydraNode tracer
+
+  withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [bobVk] [2] $ \n1 -> do
+    (headId', decrementOuts) <- withHydraNode hydraTracer bobChainConfig workDir 2 bobSk [aliceVk] [1] $ \n2 -> do
+      send n1 $ input "Init" []
+
+      headId <- waitMatch (20 * blockTime) n1 $ headIsInitializingWith (Set.fromList [alice, bob])
+      _ <- waitMatch (20 * blockTime) n2 $ headIsInitializingWith (Set.fromList [alice, bob])
+
+      requestCommitTx n1 mempty >>= submitTx cardanoNode
+      requestCommitTx n2 mempty >>= submitTx cardanoNode
+
+      waitFor hydraTracer (20 * blockTime) [n1, n2] $
+        output "HeadIsOpen" ["utxo" .= object mempty, "headId" .= headId]
+
+      resp <-
+        parseUrlThrow ("POST " <> hydraNodeBaseUrl n1 <> "/commit")
+          <&> setRequestBodyJSON commitUTxO
+            >>= httpJSON
+
+      let depositTransaction = getResponseBody resp :: Tx
+      let tx = signTx aliceCardanoSk depositTransaction
+
+      submitTx cardanoNode tx
+
+      waitFor hydraTracer 10 [n1, n2] $
+        output "CommitApproved" ["headId" .= headId, "utxoToCommit" .= commitUTxO]
+
+      waitFor hydraTracer 10 [n1, n2] $
+        output "CommitFinalized" ["headId" .= headId, "depositTxId" .= getTxId (getTxBody tx)]
+
+      getSnapshotUTxO n1 `shouldReturn` commitUTxO
+
+      let aliceAddress = mkVkAddress networkId aliceCardanoVk
+
+      decommitTx <- do
+        let (i, o) = List.head $ UTxO.pairs commitUTxO
+        either (failure . show) pure $
+          mkSimpleTx (i, o) (aliceAddress, txOutValue o) aliceCardanoSk
+
+      let decommitUTxO = utxoFromTx decommitTx
+          decommitTxId = txId decommitTx
+
+      -- Sometimes use websocket, sometimes use HTTP
+      join . generate $
+        elements
+          [ send n1 $ input "Decommit" ["decommitTx" .= decommitTx]
+          , postDecommit n1 decommitTx
+          ]
+
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitRequested" ["headId" .= headId, "decommitTx" .= decommitTx, "utxoToDecommit" .= decommitUTxO]
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitApproved" ["headId" .= headId, "decommitTxId" .= decommitTxId, "utxoToDecommit" .= decommitUTxO]
+
+      failAfter 10 $ waitForUTxO cardanoNode decommitUTxO
+
+      distributedUTxO <- waitForAllMatch 10 [n1, n2] $ \v -> do
+        guard $ v ^? key "tag" == Just "DecommitFinalized"
+        guard $ v ^? key "headId" == Just (toJSON headId)
+        v ^? key "distributedUTxO" . _JSON
+
+      guard $ distributedUTxO `UTxO.containsOutputs` utxoFromTx decommitTx
+
+      pure (headId, decommitUTxO)
+
+    bobChainConfigFromTip <-
+      chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
+        <&> modifyConfig (\config -> config{networkId, startChainFrom = Just tip})
+
+    withTempDir "blank-state" $ \tmpDir -> do
+      void $ readCreateProcessWithExitCode (proc "cp" ["-r", workDir </> "state-2", tmpDir]) ""
+      void $ readCreateProcessWithExitCode (proc "rm" ["-rf", tmpDir </> "state-2" </> "state"]) ""
+      void $ readCreateProcessWithExitCode (proc "rm" ["-rf", tmpDir </> "state-2" </> "last-known-revision"]) ""
+      withHydraNode hydraTracer bobChainConfigFromTip tmpDir 2 bobSk [aliceVk] [1] $ \n2 -> do
+        -- Also expect to see past server outputs replayed
+        headId2 <- waitMatch 5 n2 $ headIsInitializingWith (Set.fromList [alice, bob])
+        headId2 `shouldBe` headId'
+        waitFor hydraTracer 5 [n2] $
+          output "HeadIsOpen" ["utxo" .= object mempty, "headId" .= headId2]
+
+        distributedUTxO <- waitForAllMatch 5 [n2] $ \v -> do
+          guard $ v ^? key "tag" == Just "DecommitFinalized"
+          guard $ v ^? key "headId" == Just (toJSON headId2)
+          v ^? key "distributedUTxO" . _JSON
+
+        guard $ distributedUTxO `UTxO.containsOutputs` decrementOuts
+
+        send n1 $ input "Close" []
+
+        deadline' <- waitMatch (20 * blockTime) n2 $ \v -> do
+          guard $ v ^? key "tag" == Just "HeadIsClosed"
+          guard $ v ^? key "headId" == Just (toJSON headId')
+          v ^? key "contestationDeadline" . _JSON
+        remainingTime <- diffUTCTime deadline' <$> getCurrentTime
+        waitFor hydraTracer (remainingTime + 3 * blockTime) [n1, n2] $
+          output "ReadyToFanout" ["headId" .= headId']
+        send n1 $ input "Fanout" []
+
+        waitForAllMatch (10 * blockTime) [n1, n2] $ checkFanout headId' mempty
+ where
+  RunningNode{nodeSocket, networkId, blockTime} = cardanoNode
+
+  hydraNodeBaseUrl HydraClient{hydraNodeId} = "http://127.0.0.1:" <> show (4000 + hydraNodeId)
+
 -- | Step through the full life cycle of a Hydra Head with only a single
 -- participant. This scenario is also used by the smoke test run via the
 -- `hydra-cluster` executable.
@@ -1165,6 +1291,7 @@ canRecoverDeposit tracer workDir node hydraScriptsTxId =
 
         waitForAllMatch 20 [n1] $ \v -> do
           guard $ v ^? key "tag" == Just "CommitRecovered"
+          guard $ v ^? key "recoveredUTxO" == Just (toJSON commitUTxO)
 
         (balance <$> queryUTxOFor networkId nodeSocket QueryTip walletVk)
           `shouldReturn` lovelaceToValue commitAmount
@@ -1350,8 +1477,13 @@ canDecommit tracer workDir node hydraScriptsTxId =
     waitFor hydraTracer 10 [n] $
       output "DecommitApproved" ["headId" .= headId, "decommitTxId" .= decommitTxId, "utxoToDecommit" .= decommitUTxO]
     failAfter 10 $ waitForUTxO node decommitUTxO
-    waitFor hydraTracer 10 [n] $
-      output "DecommitFinalized" ["headId" .= headId, "decommitTxId" .= decommitTxId]
+
+    distributedUTxO <- waitForAllMatch 10 [n] $ \v -> do
+      guard $ v ^? key "tag" == Just "DecommitFinalized"
+      guard $ v ^? key "headId" == Just (toJSON headId)
+      v ^? key "distributedUTxO" . _JSON
+
+    guard $ distributedUTxO `UTxO.containsOutputs` decommitUTxO
 
   expectFailureOnUnsignedDecommitTx n headId decommitTx = do
     let unsignedDecommitTx = makeSignedTransaction [] $ getTxBody decommitTx
diff --git a/hydra-cluster/test/Test/ChainObserverSpec.hs b/hydra-cluster/test/Test/ChainObserverSpec.hs
index dd9a5e8c153..a968cc19a6b 100644
--- a/hydra-cluster/test/Test/ChainObserverSpec.hs
+++ b/hydra-cluster/test/Test/ChainObserverSpec.hs
@@ -15,17 +15,16 @@ import CardanoNode (NodeLog, withCardanoNodeDevnet)
 import Control.Concurrent.Class.MonadSTM (modifyTVar', newTVarIO, readTVarIO)
 import Control.Lens ((^?))
 import Data.Aeson as Aeson
-import Data.Aeson.Lens (key, _String)
+import Data.Aeson.Lens (key, _JSON, _String)
 import Data.ByteString (hGetLine)
 import Data.List qualified as List
 import Data.Text qualified as T
-import Hydra.Cardano.Api (NetworkId (..), NetworkMagic (..), lovelaceToValue, mkVkAddress, signTx, unFile)
+import Hydra.Cardano.Api (NetworkId (..), NetworkMagic (..), lovelaceToValue, mkVkAddress, signTx, unFile, utxoFromTx)
 import Hydra.Cluster.Faucet (FaucetLog, publishHydraScriptsAs, seedFromFaucet, seedFromFaucet_)
 import Hydra.Cluster.Fixture (Actor (..))
 import Hydra.Cluster.Util (chainConfigFor, keysFor)
 import Hydra.Ledger.Cardano (mkSimpleTx)
 import Hydra.Logging (showLogsOnFailure)
-import Hydra.Tx.IsTx (txId)
 import HydraNode (HydraNodeLog, input, output, requestCommitTx, send, waitFor, waitMatch, withHydraNode)
 import System.IO.Error (isEOFError, isIllegalOperation)
 import System.Process (CreateProcess (std_out), StdStream (..), proc, withCreateProcess)
@@ -85,8 +84,12 @@ spec = do
 
                 chainObserverSees observer "HeadDecrementTx" headId
 
-                waitFor hydraTracer 50 [hydraNode] $
-                  output "DecommitFinalized" ["headId" .= headId, "decommitTxId" .= txId decommitTx]
+                distributedUTxO <- waitMatch 50 hydraNode $ \v -> do
+                  guard $ v ^? key "tag" == Just "DecommitFinalized"
+                  guard $ v ^? key "headId" == Just (toJSON headId)
+                  v ^? key "distributedUTxO" . _JSON
+
+                guard $ distributedUTxO `UTxO.containsOutputs` utxoFromTx decommitTx
 
                 send hydraNode $ input "Close" []
 
diff --git a/hydra-cluster/test/Test/EndToEndSpec.hs b/hydra-cluster/test/Test/EndToEndSpec.hs
index 60d518f903c..d4ffae3ceb0 100644
--- a/hydra-cluster/test/Test/EndToEndSpec.hs
+++ b/hydra-cluster/test/Test/EndToEndSpec.hs
@@ -60,6 +60,7 @@ import Hydra.Cluster.Scenarios (
   checkFanout,
   headIsInitializingWith,
   initWithWrongKeys,
+  nodeReObservesOnChainTxs,
   oneOfThreeNodesStopsForAWhile,
   persistenceCanLoadWithEmptyCommit,
   refuelIfNeeded,
@@ -261,6 +262,11 @@ spec = around (showLogsOnFailure "EndToEndSpec") $ do
           withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node ->
             publishHydraScriptsAs node Faucet
               >>= persistenceCanLoadWithEmptyCommit tracer tmpDir node
+      it "node re-observes on-chain txs" $ \tracer -> do
+        withClusterTempDir $ \tmpDir -> do
+          withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node ->
+            publishHydraScriptsAs node Faucet
+              >>= nodeReObservesOnChainTxs tracer tmpDir node
 
     describe "three hydra nodes scenario" $ do
       it "can survive a bit of downtime of 1 in 3 nodes" $ \tracer -> do
diff --git a/hydra-node/golden/ServerOutput/DecommitFinalized.json b/hydra-node/golden/ServerOutput/DecommitFinalized.json
index 5eb4fd3534b..93b61b8d43f 100644
--- a/hydra-node/golden/ServerOutput/DecommitFinalized.json
+++ b/hydra-node/golden/ServerOutput/DecommitFinalized.json
@@ -1,8 +1,8 @@
 {
     "samples": [
         {
-            "decommitTxId": "171e664c04a43149bfd8c1cb1e9a9f7646076421bff99b652891cf15db78ec7a",
-            "headId": "203296a3f8557f12f55a28c25b68c335",
+            "distributedUTxO": {},
+            "headId": "01000101010100010100000001010100",
             "tag": "DecommitFinalized"
         }
     ],
diff --git a/hydra-node/golden/StateChanged/DecommitFinalized.json b/hydra-node/golden/StateChanged/DecommitFinalized.json
index a3bb55bc209..a53e2c54da2 100644
--- a/hydra-node/golden/StateChanged/DecommitFinalized.json
+++ b/hydra-node/golden/StateChanged/DecommitFinalized.json
@@ -3,20 +3,20 @@
         {
             "chainState": {
                 "recordedAt": {
-                    "blockHash": "0100010101010101010100000100010001000100000000010000010000010001",
+                    "blockHash": "0000010000000001000001000101010100010101010100010101000100010001",
                     "slot": 1,
                     "tag": "ChainPoint"
                 },
                 "spendableUTxO": {
-                    "0000000000010100010101000001010100010100010100010100000001010100#19": {
-                        "address": "addr_test1zpscm90puxtm785w02th7qws0w0v0vphzyh88g2w90e50387lg66ceyc8n04addy7qhqfwgpem2qkkhe3gafa47ltdls8pjj0t",
+                    "0000000000000101000101000000000001000100010101000100000001000000#33": {
+                        "address": "addr1z8x2xz27e7rtnr7sg5srqz3xvw5ent7lk8swq6nhv6cxez2vsky84frr8aun8vkqm7rcsyea5fh7unnl7l98cgqwhrjqgvpy82",
                         "datum": null,
-                        "datumhash": null,
+                        "datumhash": "27dfefd7b5d62489c2656fa160886efdc5abbf8a3a180049a439c3417db7c16e",
                         "inlineDatum": null,
                         "inlineDatumRaw": null,
                         "referenceScript": {
                             "script": {
-                                "cborHex": "83030081820180",
+                                "cborHex": "830301828202838200581cff262700384cf402b1dd67c64885bb86daaa4fb001b0dc23993f010f830301818200581cfa7e2656f1c916a670ca9eb1ab960d8bd95c8b54c9589d10f2952b2f8201828200581cff6a39027ffd86c3816475723fc81d4b40935c877ff0249da7829b298200581c7767c6cb10c95c7dee64e1bff8624bca269ff4e4c8646f1418b63936820180",
                                 "description": "",
                                 "type": "SimpleScript"
                             },
@@ -24,15 +24,40 @@
                         },
                         "value": {
                             "b0c53e2bf180858da4b64eb5598c5615bba7d723d2b604a83b7f9165": {
-                                "eb0211177e54": 1
-                            }
+                                "38": 1
+                            },
+                            "lovelace": 5089451595132491754
                         }
                     }
                 }
             },
-            "decommitTxId": "0101000101010000000101000001000000000101010001000100010100000101",
-            "headId": "00000101010100010001000000000100",
-            "newVersion": 0,
+            "distributedUTxO": {
+                "0100000101000101000000010000000100000000010101000001010000000100#77": {
+                    "address": "addr_test1wpwe0u03kaxzzah58su5dg23uza090pr0q9544wn3jgprlgrlennf",
+                    "datum": null,
+                    "inlineDatum": {
+                        "map": []
+                    },
+                    "inlineDatumRaw": "a0",
+                    "inlineDatumhash": "d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c",
+                    "referenceScript": {
+                        "script": {
+                            "cborHex": "820180",
+                            "description": "",
+                            "type": "SimpleScript"
+                        },
+                        "scriptLanguage": "SimpleScriptLanguage"
+                    },
+                    "value": {
+                        "d239ea01587fbc7f83f42878838d76b355c9a507ec41ffd1e9a6551e": {
+                            "38": 1
+                        },
+                        "lovelace": 5303846555036031747
+                    }
+                }
+            },
+            "headId": "00000001000000000001000101010100",
+            "newVersion": 1,
             "tag": "DecommitFinalized"
         }
     ],
diff --git a/hydra-node/json-schemas/api.yaml b/hydra-node/json-schemas/api.yaml
index 6c50ed76b81..4eb6f8724e6 100644
--- a/hydra-node/json-schemas/api.yaml
+++ b/hydra-node/json-schemas/api.yaml
@@ -1423,7 +1423,7 @@ components:
       required:
         - tag
         - headId
-        - decommitTxId
+        - distributedUTxO
         - seq
         - timestamp
       properties:
@@ -1432,8 +1432,8 @@ components:
           enum: ["DecommitFinalized"]
         headId:
           $ref: "api.yaml#/components/schemas/HeadId"
-        decommitTxId:
-          $ref: "api.yaml#/components/schemas/TxId"
+        distributedUTxO:
+          $ref: "api.yaml#/components/schemas/UTxO"
         seq:
           $ref: "api.yaml#/components/schemas/SequenceNumber"
         timestamp:
@@ -1542,7 +1542,7 @@ components:
         recoveredUTxO:
           $ref: "api.yaml#/components/schemas/UTxO"
         recoveredTxId:
-          type: string
+          $ref: "api.yaml#/components/schemas/TxId"
         seq:
           $ref: "api.yaml#/components/schemas/SequenceNumber"
         timestamp:
@@ -1988,6 +1988,7 @@ components:
             - tag
             - headId
             - recoverTxId
+            - recoverUTxO
             - deadline
           description: |
             Post a transaction to recover deposited UTxO back to L1.
@@ -1999,6 +2000,8 @@ components:
               $ref: "api.yaml#/components/schemas/HeadId"
             recoverTxId:
               type: string
+            recoverUTxO:
+              $ref: "api.yaml#/components/schemas/UTxO"
             deadline:
               $ref: "api.yaml#/components/schemas/ChainSlot"
         - title: IncrementTx
diff --git a/hydra-node/json-schemas/common.yaml b/hydra-node/json-schemas/common.yaml
index 4cc8af0a9fa..e3734a0e175 100644
--- a/hydra-node/json-schemas/common.yaml
+++ b/hydra-node/json-schemas/common.yaml
@@ -70,6 +70,7 @@ definitions:
           - tag
           - headId
           - recoveredTxId
+          - recoveredUTxO
         description: >-
           The recover transaction unlocking the deposited UTxO.
         properties:
@@ -79,7 +80,9 @@ definitions:
           headId:
             $ref: "api.yaml#/components/schemas/HeadId"
           recoveredTxId:
-            type: string
+            $ref: "api.yaml#/components/schemas/TxId"
+          recoveredUTxO:
+            $ref: "api.yaml#/components/schemas/UTxO"
       - title: OnIncrementTx
         type: object
         additionalProperties: false
@@ -161,7 +164,7 @@ definitions:
           - tag
           - headId
           - newVersion
-          - distributedOutputs
+          - distributedUTxO
         properties:
           tag:
             type: string
@@ -170,10 +173,8 @@ definitions:
             $ref: "api.yaml#/components/schemas/HeadId"
           newVersion:
             $ref: "api.yaml#/components/schemas/SnapshotVersion"
-          distributedOutputs:
-            type: array
-            items:
-              $ref: "api.yaml#/components/schemas/TxOut"
+          distributedUTxO:
+            $ref: "api.yaml#/components/schemas/UTxO"
       - title: OnCollectComTx
         type: object
         additionalProperties: false
diff --git a/hydra-node/json-schemas/logs.yaml b/hydra-node/json-schemas/logs.yaml
index 8e35af453d0..87e55d65727 100644
--- a/hydra-node/json-schemas/logs.yaml
+++ b/hydra-node/json-schemas/logs.yaml
@@ -1550,7 +1550,7 @@ definitions:
           newLocalUTxO:
             $ref: "api.yaml#/components/schemas/UTxO"
           recoveredTxId:
-            type: string
+            $ref: "api.yaml#/components/schemas/TxId"
       - title: "DecommitRecorded"
         additionalProperties: false
         required:
diff --git a/hydra-node/src/Hydra/API/ServerOutput.hs b/hydra-node/src/Hydra/API/ServerOutput.hs
index 3015e2ce42b..c0fc3c1af0f 100644
--- a/hydra-node/src/Hydra/API/ServerOutput.hs
+++ b/hydra-node/src/Hydra/API/ServerOutput.hs
@@ -180,7 +180,7 @@ data ServerOutput tx
   | DecommitRequested {headId :: HeadId, decommitTx :: tx, utxoToDecommit :: UTxOType tx}
   | DecommitInvalid {headId :: HeadId, decommitTx :: tx, decommitInvalidReason :: DecommitInvalidReason tx}
   | DecommitApproved {headId :: HeadId, decommitTxId :: TxIdType tx, utxoToDecommit :: UTxOType tx}
-  | DecommitFinalized {headId :: HeadId, decommitTxId :: TxIdType tx}
+  | DecommitFinalized {headId :: HeadId, distributedUTxO :: UTxOType tx}
   | CommitRecorded {headId :: HeadId, utxoToCommit :: UTxOType tx, pendingDeposit :: TxIdType tx, deadline :: UTCTime}
   | CommitApproved {headId :: HeadId, utxoToCommit :: UTxOType tx}
   | CommitFinalized {headId :: HeadId, depositTxId :: TxIdType tx}
diff --git a/hydra-node/src/Hydra/Chain.hs b/hydra-node/src/Hydra/Chain.hs
index 38b1960b49b..6eddeaa61c2 100644
--- a/hydra-node/src/Hydra/Chain.hs
+++ b/hydra-node/src/Hydra/Chain.hs
@@ -66,6 +66,7 @@ data PostChainTx tx
       { headId :: HeadId
       , recoverTxId :: TxIdType tx
       , deadline :: ChainSlot
+      , recoverUTxO :: UTxOType tx
       }
   | DecrementTx
       { headId :: HeadId
@@ -100,8 +101,8 @@ instance ArbitraryIsTx tx => Arbitrary (PostChainTx tx) where
     CollectComTx{utxo, headId, headParameters} -> CollectComTx <$> shrink utxo <*> shrink headId <*> shrink headParameters
     IncrementTx{headId, headParameters, incrementingSnapshot, depositTxId} ->
       IncrementTx <$> shrink headId <*> shrink headParameters <*> shrink incrementingSnapshot <*> shrink depositTxId
-    RecoverTx{headId, recoverTxId, deadline} ->
-      RecoverTx <$> shrink headId <*> shrink recoverTxId <*> shrink deadline
+    RecoverTx{headId, recoverTxId, deadline, recoverUTxO} ->
+      RecoverTx <$> shrink headId <*> shrink recoverTxId <*> shrink deadline <*> shrink recoverUTxO
     DecrementTx{headId, headParameters, decrementingSnapshot} -> DecrementTx <$> shrink headId <*> shrink headParameters <*> shrink decrementingSnapshot
     CloseTx{headId, headParameters, openVersion, closingSnapshot} -> CloseTx <$> shrink headId <*> shrink headParameters <*> shrink openVersion <*> shrink closingSnapshot
     ContestTx{headId, headParameters, openVersion, contestingSnapshot} -> ContestTx <$> shrink headId <*> shrink headParameters <*> shrink openVersion <*> shrink contestingSnapshot
@@ -132,6 +133,7 @@ data OnChainTx tx
   | OnRecoverTx
       { headId :: HeadId
       , recoveredTxId :: TxIdType tx
+      , recoveredUTxO :: UTxOType tx
       }
   | OnIncrementTx
       { headId :: HeadId
@@ -141,7 +143,7 @@ data OnChainTx tx
   | OnDecrementTx
       { headId :: HeadId
       , newVersion :: SnapshotVersion
-      , distributedOutputs :: [TxOutType tx]
+      , distributedUTxO :: UTxOType tx
       }
   | OnCloseTx
       { headId :: HeadId
diff --git a/hydra-node/src/Hydra/Chain/Direct/Handlers.hs b/hydra-node/src/Hydra/Chain/Direct/Handlers.hs
index 7add7e6e9cb..536554a7181 100644
--- a/hydra-node/src/Hydra/Chain/Direct/Handlers.hs
+++ b/hydra-node/src/Hydra/Chain/Direct/Handlers.hs
@@ -336,12 +336,12 @@ convertObservation = \case
     pure OnCollectComTx{headId}
   Deposit DepositObservation{headId, deposited, depositTxId, deadline} ->
     pure $ OnDepositTx{headId, deposited, depositTxId, deadline = posixToUTCTime deadline}
-  Recover RecoverObservation{headId, recoveredTxId} ->
-    pure OnRecoverTx{headId, recoveredTxId}
+  Recover RecoverObservation{headId, recoveredTxId, recoveredUTxO} ->
+    pure OnRecoverTx{headId, recoveredTxId, recoveredUTxO}
   Increment IncrementObservation{headId, newVersion, depositTxId} ->
     pure OnIncrementTx{headId, newVersion, depositTxId}
-  Decrement DecrementObservation{headId, newVersion, distributedOutputs} ->
-    pure OnDecrementTx{headId, newVersion, distributedOutputs}
+  Decrement DecrementObservation{headId, newVersion, distributedUTxO} ->
+    pure OnDecrementTx{headId, newVersion, distributedUTxO}
   -- XXX: Needing ClosedThreadOutput feels weird here
   Close CloseObservation{headId, snapshotNumber, threadOutput = ClosedThreadOutput{closedContestationDeadline}} ->
     pure
diff --git a/hydra-node/src/Hydra/Chain/Direct/State.hs b/hydra-node/src/Hydra/Chain/Direct/State.hs
index 226cf918995..0b5a8bca695 100644
--- a/hydra-node/src/Hydra/Chain/Direct/State.hs
+++ b/hydra-node/src/Hydra/Chain/Direct/State.hs
@@ -1161,7 +1161,7 @@ genIncrementTx numParties = do
     , unsafeIncrement cctx (openUTxO <> utxo) headId (ctxHeadParameters ctx) snapshot depositTxId slotNo
     )
 
-genDecrementTx :: Int -> Gen (ChainContext, [TxOut CtxUTxO], OpenState, UTxO, Tx)
+genDecrementTx :: Int -> Gen (ChainContext, UTxO, OpenState, UTxO, Tx)
 genDecrementTx numParties = do
   ctx <- genHydraContextFor numParties
   (u0, stOpen@OpenState{headId}) <- genStOpen ctx `suchThat` \(u, _) -> not (null u)
@@ -1172,7 +1172,7 @@ genDecrementTx numParties = do
   let openUTxO = getKnownUTxO stOpen
   pure
     ( cctx
-    , maybe mempty toList (utxoToDecommit $ getSnapshot snapshot)
+    , fromMaybe mempty (utxoToDecommit $ getSnapshot snapshot)
     , stOpen
     , mempty
     , unsafeDecrement cctx openUTxO headId (ctxHeadParameters ctx) snapshot
diff --git a/hydra-node/src/Hydra/HeadLogic.hs b/hydra-node/src/Hydra/HeadLogic.hs
index 524c9f2f082..4115aeb76da 100644
--- a/hydra-node/src/Hydra/HeadLogic.hs
+++ b/hydra-node/src/Hydra/HeadLogic.hs
@@ -71,7 +71,6 @@ import Hydra.HeadLogic.State (
 import Hydra.Ledger (
   Ledger (..),
   applyTransactions,
-  outputsOfTx,
  )
 import Hydra.Network qualified as Network
 import Hydra.Network.Message (Message (..), NetworkEvent (..))
@@ -737,7 +736,7 @@ onOpenClientRecover headId currentSlot coordinatedHeadState recoverTxId =
   case Map.lookup recoverTxId pendingDeposits of
     Nothing ->
       Error $ RequireFailed RecoverNotMatchingDeposit
-    Just _ ->
+    Just recoverUTxO ->
       causes
         [ OnChainEffect
             { postChainTx =
@@ -745,6 +744,7 @@ onOpenClientRecover headId currentSlot coordinatedHeadState recoverTxId =
                   { headId
                   , recoverTxId = recoverTxId
                   , deadline = currentSlot
+                  , recoverUTxO
                   }
             }
         ]
@@ -959,23 +959,21 @@ onOpenChainRecoverTx ::
   ChainStateType tx ->
   HeadId ->
   TxIdType tx ->
+  UTxOType tx ->
   Outcome tx
-onOpenChainRecoverTx st newChainState headId recoveredTxId =
-  case Map.lookup recoveredTxId pendingDeposits of
-    Nothing -> Error $ RequireFailed RecoverNotMatchingDeposit
-    Just recoveredUTxO ->
-      newState
-        CommitRecovered
-          { chainState = newChainState
-          , headId
-          , recoveredUTxO
-          , newLocalUTxO = localUTxO `withoutUTxO` recoveredUTxO
-          , recoveredTxId
-          }
+onOpenChainRecoverTx st newChainState headId recoveredTxId recoveredUTxO =
+  newState
+    CommitRecovered
+      { chainState = newChainState
+      , headId
+      , recoveredUTxO
+      , newLocalUTxO = localUTxO `withoutUTxO` recoveredUTxO
+      , recoveredTxId
+      }
  where
   OpenState{coordinatedHeadState} = st
 
-  CoordinatedHeadState{localUTxO, pendingDeposits} = coordinatedHeadState
+  CoordinatedHeadState{localUTxO} = coordinatedHeadState
 
 -- | Observe a increment transaction. If the outputs match the ones of the
 -- pending commit UTxO, then we consider the deposit/increment finalized, and remove the
@@ -1004,34 +1002,23 @@ onOpenChainIncrementTx openState newChainState newVersion depositTxId =
 --
 -- __Transition__: 'OpenState' → 'OpenState'
 onOpenChainDecrementTx ::
-  IsTx tx =>
   OpenState tx ->
   ChainStateType tx ->
   -- | New open state version
   SnapshotVersion ->
   -- | Outputs removed by the decrement
-  [TxOutType tx] ->
+  UTxOType tx ->
   Outcome tx
-onOpenChainDecrementTx openState newChainState newVersion distributedTxOuts =
-  -- Spec: if outputs(txω) = 𝑈ω
-  case decommitTx of
-    Nothing -> Error $ AssertionFailed "decrement observed but no decommit pending"
-    Just tx
-      | outputsOfTx tx == distributedTxOuts ->
-          -- Spec: txω ← ⊥
-          --       v  ← v
-          newState
-            DecommitFinalized
-              { chainState = newChainState
-              , headId
-              , newVersion
-              , decommitTxId = txId tx
-              }
-      | otherwise -> Error $ AssertionFailed "decrement not matching pending decommit"
+onOpenChainDecrementTx openState newChainState newVersion distributedUTxO =
+  newState
+    DecommitFinalized
+      { chainState = newChainState
+      , headId
+      , newVersion
+      , distributedUTxO
+      }
  where
-  OpenState{coordinatedHeadState, headId} = openState
-
-  CoordinatedHeadState{decommitTx} = coordinatedHeadState
+  OpenState{headId} = openState
 
 isLeader :: HeadParameters -> Party -> SnapshotNumber -> Bool
 isLeader HeadParameters{parties} p sn =
@@ -1349,8 +1336,8 @@ update env ledger st ev = case (st, ev) of
     | ourHeadId == headId -> onOpenChainDepositTx newChainState headId env openState deposited depositTxId deadline
     | otherwise ->
         Error NotOurHead{ourHeadId, otherHeadId = headId}
-  (Open openState@OpenState{headId = ourHeadId}, ChainInput Observation{observedTx = OnRecoverTx{headId, recoveredTxId}, newChainState})
-    | ourHeadId == headId -> onOpenChainRecoverTx openState newChainState headId recoveredTxId
+  (Open openState@OpenState{headId = ourHeadId}, ChainInput Observation{observedTx = OnRecoverTx{headId, recoveredTxId, recoveredUTxO}, newChainState})
+    | ourHeadId == headId -> onOpenChainRecoverTx openState newChainState headId recoveredTxId recoveredUTxO
     | otherwise ->
         Error NotOurHead{ourHeadId, otherHeadId = headId}
   (Open openState@OpenState{headId = ourHeadId}, ChainInput Observation{observedTx = OnIncrementTx{headId, newVersion, depositTxId}, newChainState})
@@ -1358,10 +1345,10 @@ update env ledger st ev = case (st, ev) of
         onOpenChainIncrementTx openState newChainState newVersion depositTxId
     | otherwise ->
         Error NotOurHead{ourHeadId, otherHeadId = headId}
-  (Open openState@OpenState{headId = ourHeadId}, ChainInput Observation{observedTx = OnDecrementTx{headId, newVersion, distributedOutputs}, newChainState})
+  (Open openState@OpenState{headId = ourHeadId}, ChainInput Observation{observedTx = OnDecrementTx{headId, newVersion, distributedUTxO}, newChainState})
     -- TODO: What happens if observed decrement tx get's rolled back?
     | ourHeadId == headId ->
-        onOpenChainDecrementTx openState newChainState newVersion distributedOutputs
+        onOpenChainDecrementTx openState newChainState newVersion distributedUTxO
     | otherwise ->
         Error NotOurHead{ourHeadId, otherHeadId = headId}
   -- Closed
diff --git a/hydra-node/src/Hydra/HeadLogic/Outcome.hs b/hydra-node/src/Hydra/HeadLogic/Outcome.hs
index e76ff493238..ae7e21b6055 100644
--- a/hydra-node/src/Hydra/HeadLogic/Outcome.hs
+++ b/hydra-node/src/Hydra/HeadLogic/Outcome.hs
@@ -126,7 +126,7 @@ data StateChanged tx
   | DecommitFinalized
       { chainState :: ChainStateType tx
       , headId :: HeadId
-      , decommitTxId :: TxIdType tx
+      , distributedUTxO :: UTxOType tx
       , newVersion :: SnapshotVersion
       }
   | HeadClosed {headId :: HeadId, snapshotNumber :: SnapshotNumber, chainState :: ChainStateType tx, contestationDeadline :: UTCTime}
diff --git a/hydra-node/src/Hydra/Ledger.hs b/hydra-node/src/Hydra/Ledger.hs
index da32735ea1b..45877c4372e 100644
--- a/hydra-node/src/Hydra/Ledger.hs
+++ b/hydra-node/src/Hydra/Ledger.hs
@@ -10,10 +10,6 @@ import Hydra.Tx.IsTx (IsTx (..))
 import Test.QuickCheck.Instances.Natural ()
 import Test.QuickCheck.Instances.Text ()
 
--- | Get outputs of a transaction.
-outputsOfTx :: IsTx tx => tx -> [TxOutType tx]
-outputsOfTx = outputsOfUTxO . utxoFromTx
-
 -- | Get the next chain slot. Use this instead of giving 'Enum' or 'Num'
 -- instances to 'ChainSlot'.
 nextChainSlot :: ChainSlot -> ChainSlot
diff --git a/hydra-node/test/Hydra/BehaviorSpec.hs b/hydra-node/test/Hydra/BehaviorSpec.hs
index 9933f382f16..b3b38f59e0b 100644
--- a/hydra-node/test/Hydra/BehaviorSpec.hs
+++ b/hydra-node/test/Hydra/BehaviorSpec.hs
@@ -571,8 +571,8 @@ spec = parallel $ do
                         maybe False (42 `member`) utxoToDecommit
                       _ -> False
 
-                  waitUntil [n1, n2] $ DecommitApproved testHeadId (txId decommitTx) (utxoRefs [42])
-                  waitUntil [n1, n2] $ DecommitFinalized testHeadId (txId decommitTx)
+                  waitUntil [n1, n2] $ DecommitApproved{headId = testHeadId, decommitTxId = txId decommitTx, utxoToDecommit = utxoRef 42}
+                  waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, distributedUTxO = utxoRef 42}
                   send n1 Close
                   waitUntil [n1, n2] $ ReadyToFanout{headId = testHeadId}
                   send n2 Fanout
@@ -609,8 +609,8 @@ spec = parallel $ do
                         maybe False (88 `member`) utxoToDecommit
                       _ -> False
 
-                  waitUntil [n1, n2] $ DecommitApproved testHeadId (txId decommitTx) (utxoRefs [88])
-                  waitUntil [n1, n2] $ DecommitFinalized testHeadId (txId decommitTx)
+                  waitUntil [n1, n2] $ DecommitApproved{headId = testHeadId, decommitTxId = txId decommitTx, utxoToDecommit = utxoRefs [88]}
+                  waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, distributedUTxO = utxoRef 88}
 
                   headUTxO2 <- getHeadUTxO <$> queryState n1
                   fromMaybe mempty headUTxO2 `shouldSatisfy` (not . member 11)
@@ -645,8 +645,8 @@ spec = parallel $ do
                         maybe False (42 `member`) utxoToDecommit
                       _ -> False
 
-                  waitUntil [n1, n2] $ DecommitApproved testHeadId (txId decommitTx) (utxoRefs [42])
-                  waitUntil [n1, n2] $ DecommitFinalized testHeadId (txId decommitTx)
+                  waitUntil [n1, n2] $ DecommitApproved{headId = testHeadId, decommitTxId = txId decommitTx, utxoToDecommit = utxoRefs [42]}
+                  waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, distributedUTxO = utxoRef 42}
 
                   headUTxO <- getHeadUTxO <$> queryState n1
                   fromMaybe mempty headUTxO `shouldSatisfy` (not . member 42)
@@ -671,11 +671,11 @@ spec = parallel $ do
                       , decommitInvalidReason = DecommitAlreadyInFlight{otherDecommitTxId = txId decommitTx1}
                       }
 
-                  waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, decommitTxId = txId decommitTx1}
+                  waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, distributedUTxO = utxoRef 42}
 
                   send n2 (Decommit{decommitTx = decommitTx2})
                   waitUntil [n1, n2] $ DecommitApproved{headId = testHeadId, decommitTxId = txId decommitTx2, utxoToDecommit = utxoRefs [22]}
-                  waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, decommitTxId = txId decommitTx2}
+                  waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, distributedUTxO = utxoRef 22}
 
         it "can process transactions while decommit pending" $
           shouldRunInSim $ do
@@ -697,7 +697,7 @@ spec = parallel $ do
                     SnapshotConfirmed{snapshot = Snapshot{confirmed}} -> normalTx `elem` confirmed
                     _ -> False
 
-                  waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, decommitTxId = 1}
+                  waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, distributedUTxO = utxoRef 42}
 
         it "can close with decommit in flight" $
           shouldRunInSim $ do
@@ -753,14 +753,14 @@ spec = parallel $ do
                   waitUntil [n1, n2] $
                     DecommitFinalized
                       { headId = testHeadId
-                      , decommitTxId = txId decommitTx
+                      , distributedUTxO = utxoRef 42
                       }
                   let decommitTx2 = SimpleTx 2 (utxoRef 2) (utxoRef 88)
                   send n1 (Decommit{decommitTx = decommitTx2})
                   waitUntil [n1, n2] $
                     DecommitFinalized
                       { headId = testHeadId
-                      , decommitTxId = txId decommitTx2
+                      , distributedUTxO = utxoRef 88
                       }
                   send n1 Close
                   waitUntil [n1, n2] $ ReadyToFanout{headId = testHeadId}
@@ -1116,8 +1116,8 @@ toOnChainTx now = \case
     OnAbortTx{headId = testHeadId}
   CollectComTx{headId} ->
     OnCollectComTx{headId}
-  RecoverTx{headId, recoverTxId} ->
-    OnRecoverTx{headId, recoveredTxId = recoverTxId}
+  RecoverTx{headId, recoverTxId, recoverUTxO} ->
+    OnRecoverTx{headId, recoveredTxId = recoverTxId, recoveredUTxO = recoverUTxO}
   IncrementTx{headId, incrementingSnapshot, depositTxId} ->
     OnIncrementTx
       { headId
@@ -1130,7 +1130,7 @@ toOnChainTx now = \case
     OnDecrementTx
       { headId
       , newVersion = version + 1
-      , distributedOutputs = maybe mempty outputsOfUTxO utxoToDecommit
+      , distributedUTxO = fromMaybe mempty utxoToDecommit
       }
    where
     Snapshot{version, utxoToDecommit} = getSnapshot decrementingSnapshot
diff --git a/hydra-node/test/Hydra/Chain/Direct/StateSpec.hs b/hydra-node/test/Hydra/Chain/Direct/StateSpec.hs
index ac3d6a81524..727da0e1b8e 100644
--- a/hydra-node/test/Hydra/Chain/Direct/StateSpec.hs
+++ b/hydra-node/test/Hydra/Chain/Direct/StateSpec.hs
@@ -11,11 +11,9 @@ import Cardano.Binary (serialize)
 import Data.ByteString.Lazy qualified as LBS
 import Data.Set qualified as Set
 import Hydra.Cardano.Api (
-  CtxUTxO,
   NetworkId (Mainnet),
   Tx,
   TxIn,
-  TxOut,
   UTxO,
   findRedeemerSpending,
   genTxIn,
@@ -364,8 +362,8 @@ spec = parallel $ do
   prop "observes distributed outputs" $
     forAllDecrement' $ \toDistribute utxo tx ->
       case observeDecrementTx utxo tx of
-        Just DecrementObservation{distributedOutputs} ->
-          distributedOutputs === toDistribute
+        Just DecrementObservation{distributedUTxO} ->
+          toList distributedUTxO === toList toDistribute
         Nothing ->
           False & counterexample ("observeDecrementTx ignored transaction: " <> renderTxWithUTxO utxo tx)
 
@@ -716,7 +714,7 @@ forAllDecrement action = do
 
 forAllDecrement' ::
   Testable property =>
-  ([TxOut CtxUTxO] -> UTxO -> Tx -> property) ->
+  (UTxO -> UTxO -> Tx -> property) ->
   Property
 forAllDecrement' action = do
   forAllShrink (genDecrementTx maximumNumberOfParties) shrink $ \(ctx, distributed, st, utxo', tx) ->
diff --git a/hydra-node/test/Hydra/HeadLogicSpec.hs b/hydra-node/test/Hydra/HeadLogicSpec.hs
index c03455a4262..ca88373857b 100644
--- a/hydra-node/test/Hydra/HeadLogicSpec.hs
+++ b/hydra-node/test/Hydra/HeadLogicSpec.hs
@@ -700,12 +700,13 @@ spec =
                 OnRecoverTx
                   { headId = otherHeadId
                   , recoveredTxId = 1
+                  , recoveredUTxO = utxoRef 1
                   }
         update bobEnv ledger (inOpenState threeParties) recoverOtherHead
           `shouldBe` Error (NotOurHead{ourHeadId = testHeadId, otherHeadId})
 
       prop "ignores decrementTx of another head" $ \otherHeadId -> do
-        let decrementOtherHead = observeTx $ OnDecrementTx{headId = otherHeadId, newVersion = 1, distributedOutputs = mempty}
+        let decrementOtherHead = observeTx $ OnDecrementTx{headId = otherHeadId, newVersion = 1, distributedUTxO = mempty}
         update bobEnv ledger (inOpenState threeParties) decrementOtherHead
           `shouldBe` Error (NotOurHead{ourHeadId = testHeadId, otherHeadId})
 
diff --git a/hydra-tui/src/Hydra/TUI/Handlers.hs b/hydra-tui/src/Hydra/TUI/Handlers.hs
index 1e1e85b1ba3..4616d05afa1 100644
--- a/hydra-tui/src/Hydra/TUI/Handlers.hs
+++ b/hydra-tui/src/Hydra/TUI/Handlers.hs
@@ -196,10 +196,10 @@ handleHydraEventsInfo = \case
         <> show decommitTxId
         <> " "
         <> foldMap UTxO.render (UTxO.pairs utxoToDecommit)
-  Update (ApiTimedServerOutput TimedServerOutput{time, output = API.DecommitFinalized{decommitTxId}}) ->
+  Update (ApiTimedServerOutput TimedServerOutput{time, output = API.DecommitFinalized{distributedUTxO}}) ->
     report Success time $
       "Decommit finalized "
-        <> show decommitTxId
+        <> show distributedUTxO
   Update (ApiTimedServerOutput TimedServerOutput{time, output = API.DecommitInvalid{decommitTx, decommitInvalidReason}}) ->
     warn time $
       "Decommit Transaction with id "
diff --git a/hydra-tx/src/Hydra/Tx/Decrement.hs b/hydra-tx/src/Hydra/Tx/Decrement.hs
index 0d07eaa4262..ee83575b92a 100644
--- a/hydra-tx/src/Hydra/Tx/Decrement.hs
+++ b/hydra-tx/src/Hydra/Tx/Decrement.hs
@@ -3,6 +3,7 @@ module Hydra.Tx.Decrement where
 import Hydra.Cardano.Api
 import Hydra.Prelude
 
+import Cardano.Api.UTxO qualified as UTxO
 import Hydra.Contract.Head qualified as Head
 import Hydra.Contract.HeadState qualified as Head
 import Hydra.Ledger.Cardano.Builder (
@@ -95,7 +96,7 @@ decrementTx scriptRegistry vk headId headParameters (headInput, headOutput) snap
 data DecrementObservation = DecrementObservation
   { headId :: HeadId
   , newVersion :: SnapshotVersion
-  , distributedOutputs :: [TxOut CtxUTxO]
+  , distributedUTxO :: UTxO
   }
   deriving stock (Show, Eq, Generic)
 
@@ -120,10 +121,13 @@ observeDecrementTx utxo tx = do
             DecrementObservation
               { headId
               , newVersion = fromChainSnapshotVersion version
-              , distributedOutputs =
-                  toCtxUTxOTxOut <$> txOuts' tx
-                    & drop 1 -- NOTE: Head output must be in first position
-                    & take (fromIntegral numberOfDecommitOutputs)
+              , distributedUTxO =
+                  let inputs = txIns' tx
+                      outputs =
+                        toCtxUTxOTxOut <$> txOuts' tx
+                          & drop 1 -- NOTE: Head output must be in first position
+                          & take (fromIntegral numberOfDecommitOutputs)
+                   in UTxO.fromPairs $ zip inputs outputs
               }
         _ -> Nothing
     _ -> Nothing
diff --git a/hydra-tx/src/Hydra/Tx/Recover.hs b/hydra-tx/src/Hydra/Tx/Recover.hs
index 4fe374ef395..a6fb842720f 100644
--- a/hydra-tx/src/Hydra/Tx/Recover.hs
+++ b/hydra-tx/src/Hydra/Tx/Recover.hs
@@ -45,6 +45,7 @@ recoverTx depositTxId deposited lowerBoundSlot =
 data RecoverObservation = RecoverObservation
   { headId :: HeadId
   , recoveredTxId :: TxId
+  , recoveredUTxO :: UTxO
   }
   deriving stock (Show, Eq, Generic)
 
@@ -71,6 +72,7 @@ observeRecoverTx networkId utxo tx = do
         ( RecoverObservation
             { headId
             , recoveredTxId = depositTxId
+            , recoveredUTxO = deposits
             }
         )
     else Nothing


Reviews:
user: login: ch1bo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T15:42:43Z
user: login: v0d1ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T09:55:13Z
user: login: v0d1ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:54:22Z
user: login: noonio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T10:41:46Z
user: login: noonio
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T10:43:35Z
user: login: ffakenz
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T23:28:16Z
user: login: v0d1ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:14:35Z
user: login: v0d1ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:15:29Z
user: login: v0d1ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:16:56Z
user: login: v0d1ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:17:44Z
user: login: v0d1ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:17:58Z
user: login: v0d1ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:19:57Z
user: login: v0d1ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:22:53Z

Review comments:
diff_hunk: @@ -126,7 +126,7 @@ data StateChanged tx
   | DecommitFinalized
       { chainState :: ChainStateType tx
       , headId :: HeadId
-      , decommitTxId :: TxIdType tx
+      , distributedOutputs :: [TxOutType tx]
path: hydra-node/src/Hydra/HeadLogic/Outcome.hs
user: login: ch1bo
body: Why is this not `UTxOType tx`? It would be consistent with fanout
created_at: 2025-04-02T15:42:35Z
author_association: MEMBER
diff_hunk: @@ -126,7 +126,7 @@ data StateChanged tx
   | DecommitFinalized
       { chainState :: ChainStateType tx
       , headId :: HeadId
-      , decommitTxId :: TxIdType tx
+      , distributedOutputs :: [TxOutType tx]
path: hydra-node/src/Hydra/HeadLogic/Outcome.hs
user: login: v0d1ch
body: To do that we would need to know how to construct the correct _TxIn_ part of the `UTxO` if we want to make assertions in our tests about it. Since the increment tx is posted by the hydra-node _automatically_ we don't have enough information locally to make those tests assertions but could match on other fields. Do you see this as important or the list of `TxOut` is fine? Other observations are also all over the place and the information in there is not uniform.
created_at: 2025-04-07T09:55:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,7 +126,7 @@ data StateChanged tx
   | DecommitFinalized
       { chainState :: ChainStateType tx
       , headId :: HeadId
-      , decommitTxId :: TxIdType tx
+      , distributedOutputs :: [TxOutType tx]
path: hydra-node/src/Hydra/HeadLogic/Outcome.hs
user: login: v0d1ch
body: Scratch my last message, I wanted to implement the decrement observation using `UTxO` and ended up rewriting couple of related tests.
created_at: 2025-04-07T15:54:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -745,6 +744,7 @@ onOpenClientRecover headId currentSlot coordinatedHeadState recoverTxId =
                   { headId
                   , recoverTxId = recoverTxId
                   , deadline = currentSlot
+                  , recoverUTxO = pendingDeposits Map.! recoverTxId
path: hydra-node/src/Hydra/HeadLogic.hs
user: login: noonio
body: If you bring in the value in the `Just utxo` can just use it, instead of looking it up again :)
created_at: 2025-04-08T10:41:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,6 +308,143 @@ restartedNodeCanAbort tracer workDir cardanoNode hydraScriptsTxId = do
  where
   RunningNode{nodeSocket, networkId} = cardanoNode
 
+nodeReObservesOnChainTxs :: Tracer IO EndToEndLog -> FilePath -> RunningNode -> [TxId] -> IO ()
+nodeReObservesOnChainTxs tracer workDir cardanoNode hydraScriptsTxId = do
+  refuelIfNeeded tracer cardanoNode Alice 100_000_000
+  refuelIfNeeded tracer cardanoNode Bob 100_000_000
+  -- Start hydra-node on chain tip
+  tip <- queryTip networkId nodeSocket
+  let contestationPeriod = UnsafeContestationPeriod 2
+  let deadline = 10
+  let depositDeadline = UnsafeDepositDeadline deadline
+  aliceChainConfig <-
+    chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [Bob] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  bobChainConfig <-
+    chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  (aliceCardanoVk, aliceCardanoSk) <- keysFor Alice
+  (bobCardanoVk, bobCardanoSk) <- keysFor Bob
+  commitUTxO <- seedFromFaucet cardanoNode aliceCardanoVk 5_000_000 (contramap FromFaucet tracer)
+  commitUTxO2 <- seedFromFaucet cardanoNode bobCardanoVk 7_000_000 (contramap FromFaucet tracer)
+
+  let hydraTracer = contramap FromHydraNode tracer
+
+  withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [bobVk] [2] $ \n1 -> do
+    (headId1, decrementOuts) <- withHydraNode hydraTracer bobChainConfig workDir 2 bobSk [aliceVk] [1] $ \n2 -> do
+      send n1 $ input "Init" []
+
+      headId <- waitMatch (20 * blockTime) n1 $ headIsInitializingWith (Set.fromList [alice, bob])
+      _ <- waitMatch (20 * blockTime) n2 $ headIsInitializingWith (Set.fromList [alice, bob])
+
+      requestCommitTx n1 mempty >>= submitTx cardanoNode
+      requestCommitTx n2 mempty >>= submitTx cardanoNode
+
+      waitFor hydraTracer (20 * blockTime) [n1, n2] $
+        output "HeadIsOpen" ["utxo" .= object mempty, "headId" .= headId]
+
+      resp <-
+        parseUrlThrow ("POST " <> hydraNodeBaseUrl n2 <> "/commit")
+          <&> setRequestBodyJSON commitUTxO
+            >>= httpJSON
+
+      let depositTransaction = getResponseBody resp :: Tx
+      let tx = signTx aliceCardanoSk depositTransaction
+
+      submitTx cardanoNode tx
+
+      waitFor hydraTracer 10 [n2] $
+        output "CommitApproved" ["headId" .= headId, "utxoToCommit" .= commitUTxO]
+      waitFor hydraTracer 10 [n2] $
+        output "CommitFinalized" ["headId" .= headId, "depositTxId" .= getTxId (getTxBody tx)]
+
+      getSnapshotUTxO n1 `shouldReturn` commitUTxO
+
+      let aliceAddress = mkVkAddress networkId aliceCardanoVk
+
+      decommitTx <- do
+        let (i, o) = List.head $ UTxO.pairs commitUTxO
+        either (failure . show) pure $
+          mkSimpleTx (i, o) (aliceAddress, txOutValue o) aliceCardanoSk
+
+      let decommitUTxO = utxoFromTx decommitTx
+          decommitTxId = txId decommitTx
+
+      -- Sometimes use websocket, sometimes use HTTP
+      join . generate $
+        elements
+          [ send n2 $ input "Decommit" ["decommitTx" .= decommitTx]
+          , postDecommit n1 decommitTx
+          ]
+
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitRequested" ["headId" .= headId, "decommitTx" .= decommitTx, "utxoToDecommit" .= decommitUTxO]
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitApproved" ["headId" .= headId, "decommitTxId" .= decommitTxId, "utxoToDecommit" .= decommitUTxO]
+      failAfter 10 $ waitForUTxO cardanoNode decommitUTxO
+
+      distributedUTxO <- waitForAllMatch 10 [n1, n2] $ \v -> do
+        guard $ v ^? key "tag" == Just "DecommitFinalized"
+        guard $ v ^? key "headId" == Just (toJSON headId)
+        v ^? key "distributedUTxO" . _JSON
+
+      guard $ distributedUTxO `UTxO.containsOutputs` utxoFromTx decommitTx
path: hydra-cluster/src/Hydra/Cluster/Scenarios.hs
user: login: ffakenz
body: nice one! 👏 
created_at: 2025-04-08T23:15:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,6 +308,143 @@ restartedNodeCanAbort tracer workDir cardanoNode hydraScriptsTxId = do
  where
   RunningNode{nodeSocket, networkId} = cardanoNode
 
+nodeReObservesOnChainTxs :: Tracer IO EndToEndLog -> FilePath -> RunningNode -> [TxId] -> IO ()
+nodeReObservesOnChainTxs tracer workDir cardanoNode hydraScriptsTxId = do
+  refuelIfNeeded tracer cardanoNode Alice 100_000_000
+  refuelIfNeeded tracer cardanoNode Bob 100_000_000
+  -- Start hydra-node on chain tip
+  tip <- queryTip networkId nodeSocket
+  let contestationPeriod = UnsafeContestationPeriod 2
+  let deadline = 10
+  let depositDeadline = UnsafeDepositDeadline deadline
+  aliceChainConfig <-
+    chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [Bob] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  bobChainConfig <-
+    chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  (aliceCardanoVk, aliceCardanoSk) <- keysFor Alice
+  (bobCardanoVk, bobCardanoSk) <- keysFor Bob
+  commitUTxO <- seedFromFaucet cardanoNode aliceCardanoVk 5_000_000 (contramap FromFaucet tracer)
+  commitUTxO2 <- seedFromFaucet cardanoNode bobCardanoVk 7_000_000 (contramap FromFaucet tracer)
+
+  let hydraTracer = contramap FromHydraNode tracer
+
+  withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [bobVk] [2] $ \n1 -> do
+    (headId1, decrementOuts) <- withHydraNode hydraTracer bobChainConfig workDir 2 bobSk [aliceVk] [1] $ \n2 -> do
+      send n1 $ input "Init" []
+
+      headId <- waitMatch (20 * blockTime) n1 $ headIsInitializingWith (Set.fromList [alice, bob])
+      _ <- waitMatch (20 * blockTime) n2 $ headIsInitializingWith (Set.fromList [alice, bob])
+
+      requestCommitTx n1 mempty >>= submitTx cardanoNode
+      requestCommitTx n2 mempty >>= submitTx cardanoNode
+
+      waitFor hydraTracer (20 * blockTime) [n1, n2] $
+        output "HeadIsOpen" ["utxo" .= object mempty, "headId" .= headId]
+
+      resp <-
+        parseUrlThrow ("POST " <> hydraNodeBaseUrl n2 <> "/commit")
+          <&> setRequestBodyJSON commitUTxO
+            >>= httpJSON
+
+      let depositTransaction = getResponseBody resp :: Tx
+      let tx = signTx aliceCardanoSk depositTransaction
+
+      submitTx cardanoNode tx
+
+      waitFor hydraTracer 10 [n2] $
+        output "CommitApproved" ["headId" .= headId, "utxoToCommit" .= commitUTxO]
+      waitFor hydraTracer 10 [n2] $
+        output "CommitFinalized" ["headId" .= headId, "depositTxId" .= getTxId (getTxBody tx)]
+
+      getSnapshotUTxO n1 `shouldReturn` commitUTxO
+
+      let aliceAddress = mkVkAddress networkId aliceCardanoVk
+
+      decommitTx <- do
+        let (i, o) = List.head $ UTxO.pairs commitUTxO
+        either (failure . show) pure $
+          mkSimpleTx (i, o) (aliceAddress, txOutValue o) aliceCardanoSk
+
+      let decommitUTxO = utxoFromTx decommitTx
+          decommitTxId = txId decommitTx
+
+      -- Sometimes use websocket, sometimes use HTTP
+      join . generate $
+        elements
+          [ send n2 $ input "Decommit" ["decommitTx" .= decommitTx]
+          , postDecommit n1 decommitTx
+          ]
+
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitRequested" ["headId" .= headId, "decommitTx" .= decommitTx, "utxoToDecommit" .= decommitUTxO]
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitApproved" ["headId" .= headId, "decommitTxId" .= decommitTxId, "utxoToDecommit" .= decommitUTxO]
+      failAfter 10 $ waitForUTxO cardanoNode decommitUTxO
+
+      distributedUTxO <- waitForAllMatch 10 [n1, n2] $ \v -> do
+        guard $ v ^? key "tag" == Just "DecommitFinalized"
+        guard $ v ^? key "headId" == Just (toJSON headId)
+        v ^? key "distributedUTxO" . _JSON
+
+      guard $ distributedUTxO `UTxO.containsOutputs` utxoFromTx decommitTx
+
+      pure (headId, decommitUTxO)
+
+    -- Here we post a deposit while one node is down so we can test if recover works later on
path: hydra-cluster/src/Hydra/Cluster/Scenarios.hs
user: login: ffakenz
body: good call!
created_at: 2025-04-08T23:16:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,6 +308,143 @@ restartedNodeCanAbort tracer workDir cardanoNode hydraScriptsTxId = do
  where
   RunningNode{nodeSocket, networkId} = cardanoNode
 
+nodeReObservesOnChainTxs :: Tracer IO EndToEndLog -> FilePath -> RunningNode -> [TxId] -> IO ()
+nodeReObservesOnChainTxs tracer workDir cardanoNode hydraScriptsTxId = do
+  refuelIfNeeded tracer cardanoNode Alice 100_000_000
+  refuelIfNeeded tracer cardanoNode Bob 100_000_000
+  -- Start hydra-node on chain tip
+  tip <- queryTip networkId nodeSocket
+  let contestationPeriod = UnsafeContestationPeriod 2
+  let deadline = 10
+  let depositDeadline = UnsafeDepositDeadline deadline
+  aliceChainConfig <-
+    chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [Bob] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  bobChainConfig <-
+    chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  (aliceCardanoVk, aliceCardanoSk) <- keysFor Alice
+  (bobCardanoVk, bobCardanoSk) <- keysFor Bob
+  commitUTxO <- seedFromFaucet cardanoNode aliceCardanoVk 5_000_000 (contramap FromFaucet tracer)
+  commitUTxO2 <- seedFromFaucet cardanoNode bobCardanoVk 7_000_000 (contramap FromFaucet tracer)
+
+  let hydraTracer = contramap FromHydraNode tracer
+
+  withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [bobVk] [2] $ \n1 -> do
+    (headId1, decrementOuts) <- withHydraNode hydraTracer bobChainConfig workDir 2 bobSk [aliceVk] [1] $ \n2 -> do
+      send n1 $ input "Init" []
+
+      headId <- waitMatch (20 * blockTime) n1 $ headIsInitializingWith (Set.fromList [alice, bob])
+      _ <- waitMatch (20 * blockTime) n2 $ headIsInitializingWith (Set.fromList [alice, bob])
+
+      requestCommitTx n1 mempty >>= submitTx cardanoNode
+      requestCommitTx n2 mempty >>= submitTx cardanoNode
+
+      waitFor hydraTracer (20 * blockTime) [n1, n2] $
+        output "HeadIsOpen" ["utxo" .= object mempty, "headId" .= headId]
+
+      resp <-
+        parseUrlThrow ("POST " <> hydraNodeBaseUrl n2 <> "/commit")
+          <&> setRequestBodyJSON commitUTxO
+            >>= httpJSON
+
+      let depositTransaction = getResponseBody resp :: Tx
+      let tx = signTx aliceCardanoSk depositTransaction
+
+      submitTx cardanoNode tx
+
+      waitFor hydraTracer 10 [n2] $
+        output "CommitApproved" ["headId" .= headId, "utxoToCommit" .= commitUTxO]
+      waitFor hydraTracer 10 [n2] $
+        output "CommitFinalized" ["headId" .= headId, "depositTxId" .= getTxId (getTxBody tx)]
+
+      getSnapshotUTxO n1 `shouldReturn` commitUTxO
+
+      let aliceAddress = mkVkAddress networkId aliceCardanoVk
+
+      decommitTx <- do
+        let (i, o) = List.head $ UTxO.pairs commitUTxO
+        either (failure . show) pure $
+          mkSimpleTx (i, o) (aliceAddress, txOutValue o) aliceCardanoSk
+
+      let decommitUTxO = utxoFromTx decommitTx
+          decommitTxId = txId decommitTx
+
+      -- Sometimes use websocket, sometimes use HTTP
+      join . generate $
+        elements
+          [ send n2 $ input "Decommit" ["decommitTx" .= decommitTx]
+          , postDecommit n1 decommitTx
+          ]
+
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitRequested" ["headId" .= headId, "decommitTx" .= decommitTx, "utxoToDecommit" .= decommitUTxO]
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitApproved" ["headId" .= headId, "decommitTxId" .= decommitTxId, "utxoToDecommit" .= decommitUTxO]
+      failAfter 10 $ waitForUTxO cardanoNode decommitUTxO
+
+      distributedUTxO <- waitForAllMatch 10 [n1, n2] $ \v -> do
+        guard $ v ^? key "tag" == Just "DecommitFinalized"
+        guard $ v ^? key "headId" == Just (toJSON headId)
+        v ^? key "distributedUTxO" . _JSON
+
+      guard $ distributedUTxO `UTxO.containsOutputs` utxoFromTx decommitTx
+
+      pure (headId, decommitUTxO)
+
+    -- Here we post a deposit while one node is down so we can test if recover works later on
+    resp2 <-
+      parseUrlThrow ("POST " <> hydraNodeBaseUrl n1 <> "/commit")
+        <&> setRequestBodyJSON commitUTxO2
+          >>= httpJSON
+
+    let depositTransaction2 = getResponseBody resp2 :: Tx
+    let tx2 = signTx bobCardanoSk depositTransaction2
+
+    submitTx cardanoNode tx2
+
+    threadDelay $ fromIntegral (deadline * 2 + 1)
+
+    bobChainConfigFromTip <-
+      chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
+        <&> modifyConfig (\config -> config{networkId, startChainFrom = Just tip})
+
+    withTempDir "blank-state" $ \tmpDir -> do
+      void $ readCreateProcessWithExitCode (proc "cp" ["-r", workDir </> "state-2", tmpDir]) ""
+      void $ readCreateProcessWithExitCode (proc "rm" ["-rf", tmpDir </> "state-2" </> "state"]) ""
path: hydra-cluster/src/Hydra/Cluster/Scenarios.hs
user: login: ffakenz
body: minor: add helper function to prune node's local state
created_at: 2025-04-08T23:17:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,6 +308,143 @@ restartedNodeCanAbort tracer workDir cardanoNode hydraScriptsTxId = do
  where
   RunningNode{nodeSocket, networkId} = cardanoNode
 
+nodeReObservesOnChainTxs :: Tracer IO EndToEndLog -> FilePath -> RunningNode -> [TxId] -> IO ()
+nodeReObservesOnChainTxs tracer workDir cardanoNode hydraScriptsTxId = do
+  refuelIfNeeded tracer cardanoNode Alice 100_000_000
+  refuelIfNeeded tracer cardanoNode Bob 100_000_000
+  -- Start hydra-node on chain tip
+  tip <- queryTip networkId nodeSocket
+  let contestationPeriod = UnsafeContestationPeriod 2
+  let deadline = 10
+  let depositDeadline = UnsafeDepositDeadline deadline
+  aliceChainConfig <-
+    chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [Bob] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  bobChainConfig <-
+    chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  (aliceCardanoVk, aliceCardanoSk) <- keysFor Alice
+  (bobCardanoVk, bobCardanoSk) <- keysFor Bob
+  commitUTxO <- seedFromFaucet cardanoNode aliceCardanoVk 5_000_000 (contramap FromFaucet tracer)
+  commitUTxO2 <- seedFromFaucet cardanoNode bobCardanoVk 7_000_000 (contramap FromFaucet tracer)
+
+  let hydraTracer = contramap FromHydraNode tracer
+
+  withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [bobVk] [2] $ \n1 -> do
+    (headId1, decrementOuts) <- withHydraNode hydraTracer bobChainConfig workDir 2 bobSk [aliceVk] [1] $ \n2 -> do
+      send n1 $ input "Init" []
+
+      headId <- waitMatch (20 * blockTime) n1 $ headIsInitializingWith (Set.fromList [alice, bob])
+      _ <- waitMatch (20 * blockTime) n2 $ headIsInitializingWith (Set.fromList [alice, bob])
+
+      requestCommitTx n1 mempty >>= submitTx cardanoNode
+      requestCommitTx n2 mempty >>= submitTx cardanoNode
+
+      waitFor hydraTracer (20 * blockTime) [n1, n2] $
+        output "HeadIsOpen" ["utxo" .= object mempty, "headId" .= headId]
+
+      resp <-
+        parseUrlThrow ("POST " <> hydraNodeBaseUrl n2 <> "/commit")
+          <&> setRequestBodyJSON commitUTxO
+            >>= httpJSON
+
+      let depositTransaction = getResponseBody resp :: Tx
+      let tx = signTx aliceCardanoSk depositTransaction
+
+      submitTx cardanoNode tx
+
+      waitFor hydraTracer 10 [n2] $
+        output "CommitApproved" ["headId" .= headId, "utxoToCommit" .= commitUTxO]
+      waitFor hydraTracer 10 [n2] $
+        output "CommitFinalized" ["headId" .= headId, "depositTxId" .= getTxId (getTxBody tx)]
+
+      getSnapshotUTxO n1 `shouldReturn` commitUTxO
+
+      let aliceAddress = mkVkAddress networkId aliceCardanoVk
+
+      decommitTx <- do
+        let (i, o) = List.head $ UTxO.pairs commitUTxO
+        either (failure . show) pure $
+          mkSimpleTx (i, o) (aliceAddress, txOutValue o) aliceCardanoSk
+
+      let decommitUTxO = utxoFromTx decommitTx
+          decommitTxId = txId decommitTx
+
+      -- Sometimes use websocket, sometimes use HTTP
+      join . generate $
+        elements
+          [ send n2 $ input "Decommit" ["decommitTx" .= decommitTx]
+          , postDecommit n1 decommitTx
+          ]
+
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitRequested" ["headId" .= headId, "decommitTx" .= decommitTx, "utxoToDecommit" .= decommitUTxO]
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitApproved" ["headId" .= headId, "decommitTxId" .= decommitTxId, "utxoToDecommit" .= decommitUTxO]
+      failAfter 10 $ waitForUTxO cardanoNode decommitUTxO
+
+      distributedUTxO <- waitForAllMatch 10 [n1, n2] $ \v -> do
+        guard $ v ^? key "tag" == Just "DecommitFinalized"
+        guard $ v ^? key "headId" == Just (toJSON headId)
+        v ^? key "distributedUTxO" . _JSON
+
+      guard $ distributedUTxO `UTxO.containsOutputs` utxoFromTx decommitTx
+
+      pure (headId, decommitUTxO)
+
+    -- Here we post a deposit while one node is down so we can test if recover works later on
+    resp2 <-
+      parseUrlThrow ("POST " <> hydraNodeBaseUrl n1 <> "/commit")
+        <&> setRequestBodyJSON commitUTxO2
+          >>= httpJSON
+
+    let depositTransaction2 = getResponseBody resp2 :: Tx
+    let tx2 = signTx bobCardanoSk depositTransaction2
+
+    submitTx cardanoNode tx2
+
+    threadDelay $ fromIntegral (deadline * 2 + 1)
+
+    bobChainConfigFromTip <-
+      chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
+        <&> modifyConfig (\config -> config{networkId, startChainFrom = Just tip})
+
+    withTempDir "blank-state" $ \tmpDir -> do
+      void $ readCreateProcessWithExitCode (proc "cp" ["-r", workDir </> "state-2", tmpDir]) ""
+      void $ readCreateProcessWithExitCode (proc "rm" ["-rf", tmpDir </> "state-2" </> "state"]) ""
+      withHydraNode hydraTracer bobChainConfigFromTip tmpDir 2 bobSk [aliceVk] [1] $ \n2 -> do
+        -- Also expect to see past server outputs replayed
+        headId2 <- waitMatch 5 n2 $ headIsInitializingWith (Set.fromList [alice, bob])
+        headId2 `shouldBe` headId1
+        waitFor hydraTracer 5 [n2] $
+          output "HeadIsOpen" ["utxo" .= object mempty, "headId" .= headId2]
+
+        distributedUTxO <- waitForAllMatch 5 [n2] $ \v -> do
+          guard $ v ^? key "tag" == Just "DecommitFinalized"
+          guard $ v ^? key "headId" == Just (toJSON headId2)
+          v ^? key "distributedUTxO" . _JSON
+
+        guard $ distributedUTxO `UTxO.containsOutputs` decrementOuts
+
+        let depositTxId = getTxId $ getTxBody depositTransaction2
+        let path = BSC.unpack $ urlEncode False $ encodeUtf8 $ T.pack $ show depositTxId
+
+        recoverResp <-
+          parseUrlThrow ("DELETE " <> hydraNodeBaseUrl n2 <> "/commits/" <> path)
+            >>= httpJSON
+
+        (getResponseBody recoverResp :: String) `shouldBe` "OK"
+
+        waitForAllMatch (20 * blockTime) [n2] $ \v -> do
+          guard $ v ^? key "tag" == Just "CommitRecovered"
+          guard $ v ^? key "recoveredTxId" == Just (toJSON depositTxId)
path: hydra-cluster/src/Hydra/Cluster/Scenarios.hs
user: login: ffakenz
body: minor: i think it might be cool to continue the lifecycle and see how close/fanout look, wdyt?
created_at: 2025-04-08T23:19:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1988,6 +1988,7 @@ components:
             - tag
             - headId
             - recoverTxId
path: hydra-node/json-schemas/api.yaml
user: login: ffakenz
body: question: why do we need the txId if we have the utxo?
created_at: 2025-04-08T23:20:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1999,6 +2000,8 @@ components:
               $ref: "api.yaml#/components/schemas/HeadId"
             recoverTxId:
               type: string
path: hydra-node/json-schemas/api.yaml
user: login: ffakenz
body: note: why this is not $ref: "api.yaml#/components/schemas/TxId" ?
created_at: 2025-04-08T23:21:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -80,6 +81,8 @@ definitions:
             $ref: "api.yaml#/components/schemas/HeadId"
           recoveredTxId:
path: hydra-node/json-schemas/common.yaml
user: login: ffakenz
body: idem above question
created_at: 2025-04-08T23:21:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -737,14 +736,15 @@ onOpenClientRecover headId currentSlot coordinatedHeadState recoverTxId =
   case Map.lookup recoverTxId pendingDeposits of
     Nothing ->
       Error $ RequireFailed RecoverNotMatchingDeposit
-    Just _ ->
+    Just recoverUTxO ->
       causes
         [ OnChainEffect
             { postChainTx =
                 RecoverTx
                   { headId
                   , recoverTxId = recoverTxId
                   , deadline = currentSlot
+                  , recoverUTxO
path: hydra-node/src/Hydra/HeadLogic.hs
user: login: ffakenz
body: 🍾 .
created_at: 2025-04-08T23:23:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1004,34 +1002,23 @@ onOpenChainIncrementTx openState newChainState newVersion depositTxId =
 --
 -- __Transition__: 'OpenState' → 'OpenState'
 onOpenChainDecrementTx ::
-  IsTx tx =>
   OpenState tx ->
   ChainStateType tx ->
   -- | New open state version
   SnapshotVersion ->
   -- | Outputs removed by the decrement
-  [TxOutType tx] ->
+  UTxOType tx ->
   Outcome tx
-onOpenChainDecrementTx openState newChainState newVersion distributedTxOuts =
-  -- Spec: if outputs(txω) = 𝑈ω
-  case decommitTx of
-    Nothing -> Error $ AssertionFailed "decrement observed but no decommit pending"
-    Just tx
-      | outputsOfTx tx == distributedTxOuts ->
-          -- Spec: txω ← ⊥
-          --       v  ← v
path: hydra-node/src/Hydra/HeadLogic.hs
user: login: ffakenz
body: note: should not we maintain these spec comments?
created_at: 2025-04-08T23:26:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1004,34 +1002,23 @@ onOpenChainIncrementTx openState newChainState newVersion depositTxId =
 --
 -- __Transition__: 'OpenState' → 'OpenState'
 onOpenChainDecrementTx ::
-  IsTx tx =>
   OpenState tx ->
   ChainStateType tx ->
   -- | New open state version
   SnapshotVersion ->
   -- | Outputs removed by the decrement
-  [TxOutType tx] ->
+  UTxOType tx ->
   Outcome tx
-onOpenChainDecrementTx openState newChainState newVersion distributedTxOuts =
-  -- Spec: if outputs(txω) = 𝑈ω
path: hydra-node/src/Hydra/HeadLogic.hs
user: login: ffakenz
body: question: so this has to be removed from spec?
created_at: 2025-04-08T23:27:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -959,23 +959,21 @@ onOpenChainRecoverTx ::
   ChainStateType tx ->
   HeadId ->
   TxIdType tx ->
+  UTxOType tx ->
   Outcome tx
-onOpenChainRecoverTx st newChainState headId recoveredTxId =
-  case Map.lookup recoveredTxId pendingDeposits of
path: hydra-node/src/Hydra/HeadLogic.hs
user: login: ffakenz
body: note: no spec changes here?
created_at: 2025-04-08T23:27:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,6 +308,143 @@ restartedNodeCanAbort tracer workDir cardanoNode hydraScriptsTxId = do
  where
   RunningNode{nodeSocket, networkId} = cardanoNode
 
+nodeReObservesOnChainTxs :: Tracer IO EndToEndLog -> FilePath -> RunningNode -> [TxId] -> IO ()
+nodeReObservesOnChainTxs tracer workDir cardanoNode hydraScriptsTxId = do
+  refuelIfNeeded tracer cardanoNode Alice 100_000_000
+  refuelIfNeeded tracer cardanoNode Bob 100_000_000
+  -- Start hydra-node on chain tip
+  tip <- queryTip networkId nodeSocket
+  let contestationPeriod = UnsafeContestationPeriod 2
+  let deadline = 10
+  let depositDeadline = UnsafeDepositDeadline deadline
+  aliceChainConfig <-
+    chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [Bob] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  bobChainConfig <-
+    chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
+      <&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})
+
+  (aliceCardanoVk, aliceCardanoSk) <- keysFor Alice
+  (bobCardanoVk, bobCardanoSk) <- keysFor Bob
+  commitUTxO <- seedFromFaucet cardanoNode aliceCardanoVk 5_000_000 (contramap FromFaucet tracer)
+  commitUTxO2 <- seedFromFaucet cardanoNode bobCardanoVk 7_000_000 (contramap FromFaucet tracer)
+
+  let hydraTracer = contramap FromHydraNode tracer
+
+  withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [bobVk] [2] $ \n1 -> do
+    (headId1, decrementOuts) <- withHydraNode hydraTracer bobChainConfig workDir 2 bobSk [aliceVk] [1] $ \n2 -> do
+      send n1 $ input "Init" []
+
+      headId <- waitMatch (20 * blockTime) n1 $ headIsInitializingWith (Set.fromList [alice, bob])
+      _ <- waitMatch (20 * blockTime) n2 $ headIsInitializingWith (Set.fromList [alice, bob])
+
+      requestCommitTx n1 mempty >>= submitTx cardanoNode
+      requestCommitTx n2 mempty >>= submitTx cardanoNode
+
+      waitFor hydraTracer (20 * blockTime) [n1, n2] $
+        output "HeadIsOpen" ["utxo" .= object mempty, "headId" .= headId]
+
+      resp <-
+        parseUrlThrow ("POST " <> hydraNodeBaseUrl n2 <> "/commit")
+          <&> setRequestBodyJSON commitUTxO
+            >>= httpJSON
+
+      let depositTransaction = getResponseBody resp :: Tx
+      let tx = signTx aliceCardanoSk depositTransaction
+
+      submitTx cardanoNode tx
+
+      waitFor hydraTracer 10 [n2] $
+        output "CommitApproved" ["headId" .= headId, "utxoToCommit" .= commitUTxO]
+      waitFor hydraTracer 10 [n2] $
+        output "CommitFinalized" ["headId" .= headId, "depositTxId" .= getTxId (getTxBody tx)]
+
+      getSnapshotUTxO n1 `shouldReturn` commitUTxO
+
+      let aliceAddress = mkVkAddress networkId aliceCardanoVk
+
+      decommitTx <- do
+        let (i, o) = List.head $ UTxO.pairs commitUTxO
+        either (failure . show) pure $
+          mkSimpleTx (i, o) (aliceAddress, txOutValue o) aliceCardanoSk
+
+      let decommitUTxO = utxoFromTx decommitTx
+          decommitTxId = txId decommitTx
+
+      -- Sometimes use websocket, sometimes use HTTP
+      join . generate $
+        elements
+          [ send n2 $ input "Decommit" ["decommitTx" .= decommitTx]
+          , postDecommit n1 decommitTx
+          ]
+
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitRequested" ["headId" .= headId, "decommitTx" .= decommitTx, "utxoToDecommit" .= decommitUTxO]
+      waitFor hydraTracer 10 [n1, n2] $
+        output "DecommitApproved" ["headId" .= headId, "decommitTxId" .= decommitTxId, "utxoToDecommit" .= decommitUTxO]
+      failAfter 10 $ waitForUTxO cardanoNode decommitUTxO
+
+      distributedUTxO <- waitForAllMatch 10 [n1, n2] $ \v -> do
+        guard $ v ^? key "tag" == Just "DecommitFinalized"
+        guard $ v ^? key "headId" == Just (toJSON headId)
+        v ^? key "distributedUTxO" . _JSON
+
+      guard $ distributedUTxO `UTxO.containsOutputs` utxoFromTx decommitTx
+
+      pure (headId, decommitUTxO)
+
+    -- Here we post a deposit while one node is down so we can test if recover works later on
+    resp2 <-
+      parseUrlThrow ("POST " <> hydraNodeBaseUrl n1 <> "/commit")
+        <&> setRequestBodyJSON commitUTxO2
+          >>= httpJSON
+
+    let depositTransaction2 = getResponseBody resp2 :: Tx
+    let tx2 = signTx bobCardanoSk depositTransaction2
+
+    submitTx cardanoNode tx2
+
+    threadDelay $ fromIntegral (deadline * 2 + 1)
+
+    bobChainConfigFromTip <-
+      chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
+        <&> modifyConfig (\config -> config{networkId, startChainFrom = Just tip})
+
+    withTempDir "blank-state" $ \tmpDir -> do
+      void $ readCreateProcessWithExitCode (proc "cp" ["-r", workDir </> "state-2", tmpDir]) ""
+      void $ readCreateProcessWithExitCode (proc "rm" ["-rf", tmpDir </> "state-2" </> "state"]) ""
path: hydra-cluster/src/Hydra/Cluster/Scenarios.hs
user: login: v0d1ch
body: wdym? hydra-node is stopped at this point, which local state are you referring to?
created_at: 2025-04-09T07:14:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1988,6 +1988,7 @@ components:
             - tag
             - headId
             - recoverTxId
path: hydra-node/json-schemas/api.yaml
user: login: v0d1ch
body: Maybe we don't - I kept it around since more information the better I guess?
created_at: 2025-04-09T07:15:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1999,6 +2000,8 @@ components:
               $ref: "api.yaml#/components/schemas/HeadId"
             recoverTxId:
               type: string
path: hydra-node/json-schemas/api.yaml
user: login: v0d1ch
body: I am not sure why is this a `String`. I don't believe I touched this code but this should be changed.
created_at: 2025-04-09T07:16:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1004,34 +1002,23 @@ onOpenChainIncrementTx openState newChainState newVersion depositTxId =
 --
 -- __Transition__: 'OpenState' → 'OpenState'
 onOpenChainDecrementTx ::
-  IsTx tx =>
   OpenState tx ->
   ChainStateType tx ->
   -- | New open state version
   SnapshotVersion ->
   -- | Outputs removed by the decrement
-  [TxOutType tx] ->
+  UTxOType tx ->
   Outcome tx
-onOpenChainDecrementTx openState newChainState newVersion distributedTxOuts =
-  -- Spec: if outputs(txω) = 𝑈ω
-  case decommitTx of
-    Nothing -> Error $ AssertionFailed "decrement observed but no decommit pending"
-    Just tx
-      | outputsOfTx tx == distributedTxOuts ->
-          -- Spec: txω ← ⊥
-          --       v  ← v
path: hydra-node/src/Hydra/HeadLogic.hs
user: login: v0d1ch
body: No, these are also removed from the spec. There is a separate PR linked in the PR description.
created_at: 2025-04-09T07:17:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1004,34 +1002,23 @@ onOpenChainIncrementTx openState newChainState newVersion depositTxId =
 --
 -- __Transition__: 'OpenState' → 'OpenState'
 onOpenChainDecrementTx ::
-  IsTx tx =>
   OpenState tx ->
   ChainStateType tx ->
   -- | New open state version
   SnapshotVersion ->
   -- | Outputs removed by the decrement
-  [TxOutType tx] ->
+  UTxOType tx ->
   Outcome tx
-onOpenChainDecrementTx openState newChainState newVersion distributedTxOuts =
-  -- Spec: if outputs(txω) = 𝑈ω
path: hydra-node/src/Hydra/HeadLogic.hs
user: login: v0d1ch
body: https://github.com/cardano-scaling/hydra/pull/1928#discussion_r2034648428
created_at: 2025-04-09T07:17:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -959,23 +959,21 @@ onOpenChainRecoverTx ::
   ChainStateType tx ->
   HeadId ->
   TxIdType tx ->
+  UTxOType tx ->
   Outcome tx
-onOpenChainRecoverTx st newChainState headId recoveredTxId =
-  case Map.lookup recoveredTxId pendingDeposits of
path: hydra-node/src/Hydra/HeadLogic.hs
user: login: v0d1ch
body: recovering is not mentioned in the spec since it is not relevant for the Head protocol. It is just a way of getting back your stuck funds.
created_at: 2025-04-09T07:19:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1999,6 +2000,8 @@ components:
               $ref: "api.yaml#/components/schemas/HeadId"
             recoverTxId:
               type: string
path: hydra-node/json-schemas/api.yaml
user: login: v0d1ch
body: It should be.
created_at: 2025-04-09T07:22:53Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-02T14:39:25Z
body: # Transaction cost differences

## Script summary

| Name     | Size (Bytes)   |
|:---------|:---------------|
| νInitial | -              |
| νCommit  | -              |
| νHead    | -              |
| μHead    | -              |
| νDeposit | -              |

## `Init` transaction costs

|   Parties | Tx size   | % max Mem   | % max CPU   | Min fee ₳   |
|----------:|:----------|:------------|:------------|:------------|
|         1 | -         | -           | -           | -           |
|         2 | -         | -           | -           | -           |
|         3 | -         | -           | -           | -           |
|         5 | -         | -           | -           | -           |
|        10 | -         | -           | -           | -           |
|        40 | -         | -           | -           | -           |

## `Commit` transaction costs

|   UTxO | Tx size   | % max Mem   | % max CPU   | Min fee ₳   |
|-------:|:----------|:------------|:------------|:------------|
|      1 | -         | -           | -           | -           |
|      2 | -         | -           | -           | -           |
|      3 | -         | -           | -           | -           |
|      5 | -         | -           | -           | -           |
|     10 | -         | -           | -           | -           |
|     54 | -         | -           | -           | -           |

## `CollectCom` transaction costs

|   Parties | UTxO (bytes)   | Tx size   | % max Mem   | % max CPU   | Min fee ₳   |
|----------:|:---------------|:----------|:------------|:------------|:------------|
|         1 | -              | -         | -           | -           | -           |
|         2 | -              | -         | -           | -           | -           |
|         3 | -              | -         | -           | -           | -           |
|         4 | -              | -         | -           | -           | -           |
|         5 | -              | -         | -           | -           | -           |
|         6 | -              | -         | -           | -           | -           |
|         7 | -              | -         | -           | -           | -           |
|         8 | -              | -         | -           | -           | -           |

## Cost of Increment Transaction

|   Parties | Tx size   | % max Mem                | % max CPU                | Min fee ₳                |
|----------:|:----------|:-------------------------|:-------------------------|:-------------------------|
|         1 | -         | +0.39                    | +0.09                    | -                        |
|         2 | -         | -                        | -                        | -                        |
|         3 | -         | -                        | -                        | -                        |
|         5 | -         | $${\color{green}-0.39}$$ | $${\color{green}-0.09}$$ | -                        |
|        10 | -         | $${\color{green}-0.39}$$ | $${\color{green}-0.09}$$ | $${\color{green}-0.01}$$ |
|        37 | -         | -                        | -                        | -                        |

## Cost of Decrement Transaction

|   Parties | Tx size   | % max Mem   | % max CPU   | Min fee ₳   |
|----------:|:----------|:------------|:------------|:------------|
|         1 | -         | -           | -           | -           |
|         2 | -         | -           | -           | -           |
|         3 | -         | -           | -           | -           |
|         5 | -         | -           | -           | -           |
|        10 | -         | -           | -           | -           |
|        40 | -         | -           | -           | -           |

## `Close` transaction costs

|   Parties | Tx size   | % max Mem   | % max CPU   | Min fee ₳   |
|----------:|:----------|:------------|:------------|:------------|
|         1 | -         | -           | -           | -           |
|         2 | -         | -           | -           | -           |
|         3 | -         | -           | -           | -           |
|         5 | -         | -           | -           | -           |
|        10 | -         | -           | -           | -           |
|        34 | -         | -           | -           | -           |

## `Contest` transaction costs

|   Parties | Tx size   | % max Mem   | % max CPU   | Min fee ₳   |
|----------:|:----------|:------------|:------------|:------------|
|         1 | -         | -           | -           | -           |
|         2 | -         | -           | -           | -           |
|         3 | -         | -           | -           | -           |
|         5 | -         | -           | -           | -           |
|        10 | -         | -           | -           | -           |
|        27 | -         | -           | -           | -           |

## `FanOut` transaction costs

| UTxO, Parties   | UTxO (bytes)   | Tx size   | % max Mem   | % max CPU   | Min fee ₳   |
|:----------------|:---------------|:----------|:------------|:------------|:------------|
| (0, 10)         | -              | -         | -           | -           | -           |
| (1, 10)         | -              | -         | -           | -           | -           |
| (5, 10)         | -              | -         | -           | -           | -           |
| (10, 10)        | -              | -         | -           | -           | -           |
| (20, 10)        | -              | -         | -           | -           | -           |
| (37, 10)        | -              | -         | -           | -           | -           |

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-02T14:40:34Z
body: ## Transaction costs 

Sizes and execution budgets for Hydra protocol transactions. Note that unlisted parameters are currently using `arbitrary` values and results are not fully deterministic and comparable to previous runs.

| Metadata | |
| :--- | :--- |
| _Generated at_ | 2025-04-10 10:57:25.415506063 UTC |
| _Max. memory units_ | 14000000 |
| _Max. CPU units_ | 10000000000 |
| _Max. tx size (kB)_ | 16384 |

### Script summary

| Name   | Hash | Size (Bytes) 
| :----- | :--- | -----------: 
| νInitial | c8a101a5c8ac4816b0dceb59ce31fc2258e387de828f02961d2f2045 | 2652 | 
| νCommit | 61458bc2f297fff3cc5df6ac7ab57cefd87763b0b7bd722146a1035c | 685 | 
| νHead | 0e35115a2c7c13c68ecd8d74e4987c04d4539e337643be20bb3274bd | 14756 | 
| μHead | 57166715eadb8d3135964325c016eea546c21e1c0aae974ca67df9a5* | 5541 | 
| νDeposit | ae01dade3a9c346d5c93ae3ce339412b90a0b8f83f94ec6baa24e30c | 1102 | 

* The minting policy hash is only usable for comparison. As the script is parameterized, the actual script is unique per head.

### `Init` transaction costs

| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
| :------ | ------: | --------: | --------: | --------: |
| 1| 6091 | 11.02 | 3.43 | 0.53 |
| 2| 6299 | 13.74 | 4.28 | 0.57 |
| 3| 6500 | 15.28 | 4.73 | 0.59 |
| 5| 6895 | 20.38 | 6.31 | 0.66 |
| 10| 7903 | 30.97 | 9.53 | 0.82 |
| 40| 13935 | 98.27 | 30.17 | 1.77 |


### `Commit` transaction costs
 This uses ada-only outputs for better comparability.

| UTxO | Tx size | % max Mem | % max CPU | Min fee ₳ |
| :--- | ------: | --------: | --------: | --------: |
| 1| 561 | 2.44 | 1.16 | 0.20 |
| 2| 742 | 3.38 | 1.73 | 0.22 |
| 3| 923 | 4.36 | 2.33 | 0.24 |
| 5| 1279 | 6.41 | 3.60 | 0.28 |
| 10| 2171 | 12.13 | 7.25 | 0.40 |
| 54| 10078 | 98.61 | 68.52 | 1.88 |


### `CollectCom` transaction costs

| Parties | UTxO (bytes) |Tx size | % max Mem | % max CPU | Min fee ₳ |
| :------ | :----------- |------: | --------: | --------: | --------: |
| 1 | 57 | 529 | 25.64 | 7.39 | 0.43 |
| 2 | 113 | 636 | 34.89 | 9.99 | 0.53 |
| 3 | 170 | 747 | 43.26 | 12.38 | 0.62 |
| 4 | 228 | 862 | 56.51 | 15.96 | 0.76 |
| 5 | 284 | 969 | 64.76 | 18.37 | 0.85 |
| 6 | 340 | 1081 | 76.71 | 21.55 | 0.97 |
| 7 | 393 | 1192 | 80.52 | 22.90 | 1.02 |
| 8 | 452 | 1303 | 95.28 | 27.02 | 1.17 |


### Cost of Increment Transaction

| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
| :------ | ------: | --------: | --------: | --------: |
| 1| 1787 | 25.03 | 8.22 | 0.49 |
| 2| 1931 | 26.64 | 9.38 | 0.52 |
| 3| 2017 | 27.16 | 10.15 | 0.53 |
| 5| 2478 | 33.87 | 13.75 | 0.63 |
| 10| 3226 | 43.83 | 20.32 | 0.79 |
| 39| 7322 | 95.96 | 56.29 | 1.65 |


### Cost of Decrement Transaction

| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
| :------ | ------: | --------: | --------: | --------: |
| 1| 597 | 23.71 | 7.54 | 0.42 |
| 2| 815 | 26.46 | 8.97 | 0.46 |
| 3| 903 | 28.41 | 10.16 | 0.49 |
| 5| 1127 | 29.72 | 11.83 | 0.53 |
| 10| 1900 | 39.76 | 17.94 | 0.68 |
| 39| 6197 | 98.25 | 53.40 | 1.60 |


### `Close` transaction costs

| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
| :------ | ------: | --------: | --------: | --------: |
| 1| 690 | 29.15 | 9.21 | 0.48 |
| 2| 782 | 32.85 | 10.92 | 0.53 |
| 3| 930 | 34.71 | 12.16 | 0.56 |
| 5| 1444 | 39.20 | 15.35 | 0.64 |
| 10| 1935 | 49.48 | 21.50 | 0.79 |
| 34| 5387 | 99.16 | 53.18 | 1.57 |


### `Contest` transaction costs

| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
| :------ | ------: | --------: | --------: | --------: |
| 1| 628 | 35.29 | 10.70 | 0.54 |
| 2| 808 | 38.07 | 12.30 | 0.58 |
| 3| 904 | 39.54 | 13.31 | 0.61 |
| 5| 1286 | 45.37 | 16.59 | 0.69 |
| 10| 2030 | 57.51 | 23.75 | 0.88 |
| 27| 4529 | 97.41 | 47.53 | 1.48 |


### `Abort` transaction costs
There is some variation due to the random mixture of initial and already committed outputs.

| Parties | Tx size | % max Mem | % max CPU | Min fee ₳ |
| :------ | ------: | --------: | --------: | --------: |
| 1| 6004 | 28.29 | 9.33 | 0.71 |
| 2| 6050 | 36.38 | 11.95 | 0.80 |
| 3| 6355 | 48.94 | 16.23 | 0.94 |
| 4| 6326 | 56.44 | 18.58 | 1.02 |
| 5| 6526 | 66.81 | 22.09 | 1.14 |
| 6| 6590 | 69.26 | 22.86 | 1.16 |
| 7| 6723 | 82.04 | 27.02 | 1.30 |
| 8| 6891 | 96.96 | 32.13 | 1.47 |


### `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 | 6092 | 19.66 | 6.46 | 0.62 |
| 10 | 5 | 285 | 6261 | 30.97 | 10.82 | 0.75 |
| 10 | 10 | 567 | 6428 | 41.61 | 14.95 | 0.88 |
| 10 | 30 | 1705 | 7108 | 84.03 | 31.40 | 1.37 |
| 10 | 37 | 2102 | 7344 | 98.48 | 37.03 | 1.54 |

## 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_  2025-04-10 11:00:09.402675533 UTC


### Baseline Scenario



| Number of nodes |  1 | 
| -- | -- |
| _Number of txs_ | 300 |
| _Avg. Confirmation Time (ms)_ | 4.392931530 |
| _P99_ | 6.7976263599999935ms |
| _P95_ | 5.4041033ms |
| _P50_ | 4.218446999999999ms |
| _Number of Invalid txs_ | 0 |
      

#### Memory data 

 | Time | Used | Free | 
|------|------|------|
 | 2025-04-10 10:58:54.409843994 UTC | 902M | 2920M | 
 | 2025-04-10 10:58:59.409689716 UTC | 996M | 2817M | 
 | 2025-04-10 10:59:04.409665133 UTC | 998M | 2816M | 
 | 2025-04-10 10:59:09.409670072 UTC | 1008M | 2805M | 
 | 2025-04-10 10:59:14.409665603 UTC | 1008M | 2805M | 
 | 2025-04-10 10:59:19.40967258 UTC | 1009M | 2803M | 


### Three local nodes



| Number of nodes |  3 | 
| -- | -- |
| _Number of txs_ | 900 |
| _Avg. Confirmation Time (ms)_ | 27.762516286 |
| _P99_ | 41.82732957ms |
| _P95_ | 36.180953049999985ms |
| _P50_ | 26.819914ms |
| _Number of Invalid txs_ | 0 |
      

#### Memory data 

 | Time | Used | Free | 
|------|------|------|
 | 2025-04-10 10:59:32.409296971 UTC | 906M | 2915M | 
 | 2025-04-10 10:59:37.410954639 UTC | 1174M | 2643M | 
 | 2025-04-10 10:59:42.411277751 UTC | 1226M | 2523M | 
 | 2025-04-10 10:59:47.409266426 UTC | 1247M | 2460M | 
 | 2025-04-10 10:59:52.409280502 UTC | 1254M | 2452M | 
 | 2025-04-10 10:59:57.409288569 UTC | 1249M | 2457M | 
 | 2025-04-10 11:00:02.40929206 UTC | 1251M | 2454M | 
 | 2025-04-10 11:00:07.409275118 UTC | 1254M | 2450M | 


author_association: NONE