From 12ab239f5a202add1562b8e871a5d46de2c160b3 Mon Sep 17 00:00:00 2001 From: Jack Chan Date: Sun, 4 Jan 2026 11:42:13 +0800 Subject: [PATCH 1/4] fix: check transaction ID when matching RejectedInput in HTTP handler Under high load with concurrent TX submissions, the HTTP handler for POST /transaction was matching ANY RejectedInput for NewTx, not checking if it was the specific transaction submitted. This caused false-negative responses where a successful TX was reported as rejected because another concurrent TX was rejected. Now the handler checks txId transaction == txid before returning SubmitTxRejected, preventing the race condition. --- hydra-node/src/Hydra/API/HTTPServer.hs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hydra-node/src/Hydra/API/HTTPServer.hs b/hydra-node/src/Hydra/API/HTTPServer.hs index b5cd6e1240a..81aabac4d1a 100644 --- a/hydra-node/src/Hydra/API/HTTPServer.hs +++ b/hydra-node/src/Hydra/API/HTTPServer.hs @@ -581,8 +581,9 @@ handleSubmitL2Tx putClientInput apiTransactionTimeout responseChannel body = do go = do event <- atomically $ readTChan dupChannel case event of - Right (RejectedInput{clientInput = NewTx{}, reason}) -> do - pure $ SubmitTxRejectedResponse reason + Right (RejectedInput{clientInput = NewTx{transaction}, reason}) + | txId transaction == txid -> + pure $ SubmitTxRejectedResponse reason Left (TimedServerOutput{output}) -> case output of TxValid{transactionId} | transactionId == txid -> From 0489d7d96aba38891194d6cf49aa7cd723cf4da7 Mon Sep 17 00:00:00 2001 From: Jack Chan Date: Sun, 4 Jan 2026 11:46:56 +0800 Subject: [PATCH 2/4] test: add test for RejectedInput race condition fix Adds a test that verifies the HTTP handler correctly ignores RejectedInput events for different transactions. This ensures that when TX_A is submitted and a RejectedInput for TX_B appears, the handler for TX_A continues waiting and correctly returns success when TX_A is confirmed. --- hydra-node/test/Hydra/API/HTTPServerSpec.hs | 48 +++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/hydra-node/test/Hydra/API/HTTPServerSpec.hs b/hydra-node/test/Hydra/API/HTTPServerSpec.hs index 5dc51159a31..8d2161cbb6b 100644 --- a/hydra-node/test/Hydra/API/HTTPServerSpec.hs +++ b/hydra-node/test/Hydra/API/HTTPServerSpec.hs @@ -805,6 +805,54 @@ apiServerSpec = do $ do post "/transaction" (mkReq testTx) `shouldRespondWith` 503 + prop "ignores RejectedInput for different transaction" $ do + -- This test verifies the fix for the race condition where a RejectedInput + -- for one transaction was incorrectly matched by the handler waiting for + -- a different transaction. + responseChannel <- newTChanIO + let txA = SimpleTx 42 mempty mempty + txB = SimpleTx 99 mempty mempty + -- RejectedInput for txB (different from txA we're submitting) + rejectedB = RejectedInput{clientInput = NewTx txB, reason = "chain out of sync"} + -- SnapshotConfirmed includes txA + snapshot = + Snapshot + { headId = testHeadId + , version = 1 + , number = 7 + , confirmed = [txA] + , utxo = mempty + , utxoToCommit = mempty + , utxoToDecommit = mempty + } + confirmedEvent = + TimedServerOutput + { output = SnapshotConfirmed{snapshot = snapshot, signatures = mempty, headId = testHeadId} + , seq = 0 + , time = now + } + withApplication + ( httpApp @SimpleTx + nullTracer + dummyChainHandle + testEnvironment + dummyStatePath + defaultPParams + (pure inUnsyncedIdleState) + (pure CannotCommit) + (pure []) + -- First write RejectedInput for txB, then SnapshotConfirmed for txA + (const $ atomically $ do + writeTChan responseChannel (Right rejectedB) + writeTChan responseChannel (Left confirmedEvent)) + 10 + responseChannel + ) + $ do + -- Handler for txA should ignore the RejectedInput for txB + -- and correctly return 200 when txA is confirmed + post "/transaction" (mkReq txA) `shouldRespondWith` 200 + describe "POST /decommit" $ do it "returns 202 on timeout" $ do responseChannel <- newTChanIO From bed3c28fdedd68076834d1f27f863bd9609bff90 Mon Sep 17 00:00:00 2001 From: Jack Chan Date: Sat, 3 Jan 2026 18:42:18 +0800 Subject: [PATCH 3/4] ci: add disk cleanup step to prevent space exhaustion GitHub Actions runners have limited disk space (~14GB available). When building uncached Nix derivations (like our modified hydra-node), the build can exhaust disk space during compilation. This adds a cleanup step that removes unused tools before the build: - .NET SDK (~1.8GB) - Android SDK (~9GB) - GHC (~5GB) - CodeQL (~2.5GB) - Unused Docker images This frees up ~20GB of disk space, ensuring builds complete successfully. --- .github/actions/nix-cachix-setup/action.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/actions/nix-cachix-setup/action.yml b/.github/actions/nix-cachix-setup/action.yml index fcf19333bb5..6c24c24a284 100644 --- a/.github/actions/nix-cachix-setup/action.yml +++ b/.github/actions/nix-cachix-setup/action.yml @@ -9,6 +9,21 @@ runs: using: composite steps: + - name: 🧹 Free disk space + if: runner.os == 'Linux' + shell: bash + run: | + echo "Disk space before cleanup:" + df -h / + # Remove unnecessary tools to free up disk space + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force || true + echo "Disk space after cleanup:" + df -h / + - name: ❄ Prepare nix uses: cachix/install-nix-action@v30 with: From 343e157191d1bf4d623db96f6869304cd15a9143 Mon Sep 17 00:00:00 2001 From: Jack Chan Date: Wed, 7 Jan 2026 08:23:17 +0800 Subject: [PATCH 4/4] style: format HTTPServerSpec.hs with fourmolu --- hydra-node/test/Hydra/API/HTTPServerSpec.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hydra-node/test/Hydra/API/HTTPServerSpec.hs b/hydra-node/test/Hydra/API/HTTPServerSpec.hs index 8d2161cbb6b..6cb78a9e05c 100644 --- a/hydra-node/test/Hydra/API/HTTPServerSpec.hs +++ b/hydra-node/test/Hydra/API/HTTPServerSpec.hs @@ -842,9 +842,10 @@ apiServerSpec = do (pure CannotCommit) (pure []) -- First write RejectedInput for txB, then SnapshotConfirmed for txA - (const $ atomically $ do - writeTChan responseChannel (Right rejectedB) - writeTChan responseChannel (Left confirmedEvent)) + ( const $ atomically $ do + writeTChan responseChannel (Right rejectedB) + writeTChan responseChannel (Left confirmedEvent) + ) 10 responseChannel )