Skip to content

Commit 1af2fb7

Browse files
committed
Hotfix Checkpoint handling of API server projections (#2152)
The projections in the API server were not correctly handing `Checkpoint` events (introduced with event log rotation). I tried to create a regression test, but we don't have a `Gen [StateChanged tx]` generators that honors the constraints required for `aggregate` so I could not get hold of an expected list of `StateChanged` events with a consistent `Checkpoint` to do property testing. The whole `Projection` business is a bit contrived since we have the full `StateChanged` event stream available (the API server is now an `EventSink`). As we are also not needing to track changes onto the projected values (resource-specific websockets / subscriptions would require that), we should consider dropping the whole mechanism and just stick with a `getHeadState :: STM (HeadState tx)` + getter functions (or lenses) to acquire the relevant things in the HTTP and WS API. This PR though is a hotfix and I kept the diff to a minimum (especially as its not covered by tests!) --- * [x] CHANGELOG updated * [x] Documentation update not needed * [x] Haddocks updated * [ ] No new TODOs introduced - An XXX note on the same as above
1 parent b4b6f97 commit 1af2fb7

File tree

3 files changed

+60
-36
lines changed

3 files changed

+60
-36
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,32 @@ changes.
1010

1111
## [0.23.0] - UNRELEASED
1212

13+
## [0.22.4] - UNRELEASED
14+
1315
- Accept additional field `amount` when depositing to specify the amount of Lovelace that should be depositted to a Head returning any leftover to the user.
1416

17+
- Fix API not correctly handling event log rotation. This was evident in not
18+
being able to use `/commit` although the head is initializing or outdated
19+
information in the `Greetings` message.
20+
21+
- Ignore snapshot signatures of already confirmed snapshots. This was previously
22+
resulting in the node waiting for the accompanying snapshot request and
23+
occurred when running heads with mirror nodes.
24+
25+
- Fix an internal persistent queue blocking after restart when it reached
26+
capacity.
27+
28+
- Handle failing lease keep alive in network component and avoid bursts in
29+
heartbeating.
30+
31+
## [0.22.3] - 2025-07-21
32+
33+
* Change behavior of `Hydra.Network.Etcd` to fallback to earliest possible
34+
revision if `last-known-revision` is missing or too old. This can happen if a
35+
node is down for a long time and the `etcd` cluster compacted the last known
36+
revision in the meantime
37+
[#2136](https://github.com/cardano-scaling/hydra/issues/2136).
38+
1539
- Don't keep around invalid transactions as they could lead to stuck Head.
1640

1741
- Hydra API server responds with the correct `Content-Type` header `application-json`.

hydra-node/src/Hydra/API/Server.hs

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Control.Concurrent.STM.TChan (newBroadcastTChanIO, writeTChan)
1212
import Control.Exception (IOException)
1313
import Data.Conduit.Combinators (map)
1414
import Data.Conduit.List (catMaybes)
15+
import Data.Map qualified as Map
1516
import Hydra.API.APIServerLog (APIServerLog (..))
1617
import Hydra.API.ClientInput (ClientInput)
1718
import Hydra.API.HTTPServer (httpApp)
@@ -31,19 +32,22 @@ import Hydra.Chain (Chain (..))
3132
import Hydra.Chain.ChainState (IsChainState)
3233
import Hydra.Chain.Direct.State ()
3334
import Hydra.Events (EventSink (..), EventSource (..))
34-
import Hydra.HeadLogic (aggregate)
35-
import Hydra.HeadLogic.Outcome qualified as StateChanged
36-
import Hydra.HeadLogic.State (
35+
import Hydra.HeadLogic (
36+
CoordinatedHeadState (..),
3737
Deposit (..),
38-
HeadState (Idle),
38+
HeadState (..),
3939
IdleState (..),
40+
InitialState (..),
41+
OpenState (..),
42+
aggregate,
4043
)
44+
import Hydra.HeadLogic.Outcome qualified as StateChanged
4145
import Hydra.HeadLogic.StateEvent (StateEvent (..))
4246
import Hydra.Logging (Tracer, traceWith)
4347
import Hydra.Network (IP, PortNumber)
4448
import Hydra.Node.ApiTransactionTimeout (ApiTransactionTimeout)
4549
import Hydra.Node.Environment (Environment)
46-
import Hydra.Tx (HeadId, IsTx (..), Party, txId)
50+
import Hydra.Tx (IsTx (..), Party, txId)
4751
import Network.HTTP.Types (status500)
4852
import Network.Wai (responseLBS)
4953
import Network.Wai.Handler.Warp (
@@ -96,8 +100,10 @@ withAPIServer config env party eventSource tracer chain pparams serverOutputFilt
96100
-- Initialize our read models from stored events
97101
-- NOTE: we do not keep the stored events around in memory
98102
headStateP <- mkProjection (Idle $ IdleState mkChainState) aggregate
103+
-- XXX: We never subscribe to changes of commitInfoP et al directly so a
104+
-- single read model and normal functions mapping from HeadState ->
105+
-- CommitInfo etc. would suffice and are less fragile
99106
commitInfoP <- mkProjection CannotCommit projectCommitInfo
100-
headIdP <- mkProjection Nothing projectInitializingHeadId
101107
pendingDepositsP <- mkProjection [] projectPendingDeposits
102108
let historyTimedOutputs = sourceEvents .| map mkTimedServerOutputFromStateEvent .| catMaybes
103109
_ <-
@@ -108,7 +114,6 @@ withAPIServer config env party eventSource tracer chain pparams serverOutputFilt
108114
lift $ atomically $ do
109115
update headStateP stateChanged
110116
update commitInfoP stateChanged
111-
update headIdP stateChanged
112117
update pendingDepositsP stateChanged
113118
)
114119
(notifyServerRunning, waitForServerRunning) <- setupServerNotification
@@ -127,7 +132,7 @@ withAPIServer config env party eventSource tracer chain pparams serverOutputFilt
127132
. simpleCors
128133
$ websocketsOr
129134
defaultConnectionOptions
130-
(wsApp env party tracer historyTimedOutputs callback headStateP headIdP responseChannel serverOutputFilter)
135+
(wsApp env party tracer historyTimedOutputs callback headStateP responseChannel serverOutputFilter)
131136
( httpApp
132137
tracer
133138
chain
@@ -150,7 +155,6 @@ withAPIServer config env party eventSource tracer chain pparams serverOutputFilt
150155
atomically $ do
151156
update headStateP stateChanged
152157
update commitInfoP stateChanged
153-
update headIdP stateChanged
154158
update pendingDepositsP stateChanged
155159
-- Send to the client if it maps to a server output
156160
case mkTimedServerOutputFromStateEvent event of
@@ -260,6 +264,9 @@ mkTimedServerOutputFromStateEvent event =
260264
-- | Projection to obtain the list of pending deposits.
261265
projectPendingDeposits :: IsTx tx => [TxIdType tx] -> StateChanged.StateChanged tx -> [TxIdType tx]
262266
projectPendingDeposits txIds = \case
267+
StateChanged.Checkpoint{state} -> case state of
268+
Open OpenState{coordinatedHeadState = CoordinatedHeadState{pendingDeposits}} -> Map.keys pendingDeposits
269+
_ -> txIds
263270
StateChanged.DepositRecorded{depositTxId} -> depositTxId : txIds
264271
StateChanged.DepositRecovered{depositTxId} -> filter (/= depositTxId) txIds
265272
StateChanged.CommitFinalized{depositTxId} -> filter (/= depositTxId) txIds
@@ -270,18 +277,12 @@ projectPendingDeposits txIds = \case
270277
-- state since this is when Head parties need to commit some funds.
271278
projectCommitInfo :: CommitInfo -> StateChanged.StateChanged tx -> CommitInfo
272279
projectCommitInfo commitInfo = \case
280+
StateChanged.Checkpoint{state} -> case state of
281+
Initial InitialState{headId} -> NormalCommit headId
282+
Open OpenState{headId} -> IncrementalCommit headId
283+
_ -> CannotCommit
273284
StateChanged.HeadInitialized{headId} -> NormalCommit headId
274285
StateChanged.HeadOpened{headId} -> IncrementalCommit headId
275286
StateChanged.HeadAborted{} -> CannotCommit
276287
StateChanged.HeadClosed{} -> CannotCommit
277288
_other -> commitInfo
278-
279-
-- | Projection to obtain the 'HeadId' needed to draft a commit transaction.
280-
-- NOTE: We only want to project 'HeadId' when the Head is in the 'Initializing'
281-
-- state since this is when Head parties need to commit some funds.
282-
projectInitializingHeadId :: Maybe HeadId -> StateChanged.StateChanged tx -> Maybe HeadId
283-
projectInitializingHeadId mHeadId = \case
284-
StateChanged.HeadInitialized{headId} -> Just headId
285-
StateChanged.HeadOpened{} -> Nothing
286-
StateChanged.HeadAborted{} -> Nothing
287-
_other -> mHeadId

hydra-node/src/Hydra/API/WSServer.hs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,12 @@ import Hydra.Chain.ChainState (
4040
IsChainState,
4141
)
4242
import Hydra.Chain.Direct.State ()
43-
import Hydra.HeadLogic (ClosedState (ClosedState, readyToFanoutSent), HeadState, StateChanged)
43+
import Hydra.HeadLogic (ClosedState (ClosedState, readyToFanoutSent), HeadState, InitialState (..), OpenState (..), StateChanged)
4444
import Hydra.HeadLogic.State qualified as HeadState
4545
import Hydra.Logging (Tracer, traceWith)
4646
import Hydra.NetworkVersions qualified as NetworkVersions
4747
import Hydra.Node.Environment (Environment (..))
4848
import Hydra.Tx (Party)
49-
import Hydra.Tx.HeadId (HeadId (..))
5049
import Network.WebSockets (
5150
PendingConnection (pendingRequest),
5251
RequestHead (..),
@@ -68,13 +67,11 @@ wsApp ::
6867
(ClientInput tx -> IO ()) ->
6968
-- | Read model to enhance 'Greetings' messages with 'HeadStatus'.
7069
Projection STM.STM (StateChanged tx) (HeadState tx) ->
71-
-- | Read model to enhance 'Greetings' messages with 'HeadId'.
72-
Projection STM.STM (StateChanged tx) (Maybe HeadId) ->
7370
TChan (Either (TimedServerOutput tx) (ClientMessage tx)) ->
7471
ServerOutputFilter tx ->
7572
PendingConnection ->
7673
IO ()
77-
wsApp env party tracer history callback headStateP headIdP responseChannel ServerOutputFilter{txContainsAddr} pending = do
74+
wsApp env party tracer history callback headStateP responseChannel ServerOutputFilter{txContainsAddr} pending = do
7875
traceWith tracer NewAPIConnection
7976
let path = requestPath $ pendingRequest pending
8077
queryParams <- uriQuery <$> mkURIBs path
@@ -97,21 +94,19 @@ wsApp env party tracer history callback headStateP headIdP responseChannel Serve
9794
-- client.
9895
forwardGreetingOnly config con = do
9996
headState <- atomically getLatest
100-
hydraHeadId <- atomically getLatestHeadId
10197
sendTextData con $
10298
handleUtxoInclusion config (atKey "snapshotUtxo" .~ Nothing) $
10399
Aeson.encode
104100
Greetings
105101
{ me = party
106102
, headStatus = getHeadStatus headState
107-
, hydraHeadId
103+
, hydraHeadId = getHeadId headState
108104
, snapshotUtxo = getSnapshotUtxo headState
109105
, hydraNodeVersion = showVersion NetworkVersions.hydraNodeVersion
110106
, env
111107
}
112108

113109
Projection{getLatest} = headStateP
114-
Projection{getLatest = getLatestHeadId} = headIdP
115110

116111
mkServerOutputConfig :: [QueryParam] -> ServerOutputConfig
117112
mkServerOutputConfig qp =
@@ -185,12 +180,16 @@ wsApp env party tracer history callback headStateP headIdP responseChannel Serve
185180
WithAddressedTx addr -> txContainsAddr tx addr
186181
WithoutAddressedTx -> True
187182

188-
-- | Get the content of 'headStatus' field in 'Greetings' message from the full 'HeadState'.
189-
getHeadStatus :: HeadState tx -> HeadStatus
190-
getHeadStatus = \case
191-
HeadState.Idle{} -> Idle
192-
HeadState.Initial{} -> Initializing
193-
HeadState.Open{} -> Open
194-
HeadState.Closed ClosedState{readyToFanoutSent}
195-
| readyToFanoutSent -> FanoutPossible
196-
| otherwise -> Closed
183+
getHeadStatus = \case
184+
HeadState.Idle{} -> Idle
185+
HeadState.Initial{} -> Initializing
186+
HeadState.Open{} -> Open
187+
HeadState.Closed ClosedState{readyToFanoutSent}
188+
| readyToFanoutSent -> FanoutPossible
189+
| otherwise -> Closed
190+
191+
getHeadId = \case
192+
HeadState.Idle{} -> Nothing
193+
HeadState.Initial InitialState{headId} -> Just headId
194+
HeadState.Open OpenState{headId} -> Just headId
195+
HeadState.Closed ClosedState{headId} -> Just headId

0 commit comments

Comments
 (0)