Skip to content

Commit ee19ffd

Browse files
authored
Initialization time info (#2320)
fixes #2316 --- <!-- 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
2 parents fc4418c + 984ee07 commit ee19ffd

File tree

7 files changed

+136
-7
lines changed

7 files changed

+136
-7
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ As a minor extension, we also keep a semantic version for the `UNRELEASED`
99
changes.
1010

1111

12+
## [1.2.0] - UNRELEASED
13+
14+
- `hydra-node` has a new endpoint `GET /head-initialization` which serves the timestamp of the last Head initialization.
15+
16+
1217
## [1.1.0] - 2025-10-28
1318

1419
- **BREAKING** Partial assets depositing works a bit differently now so you should consult our [API reference](https://hydra.family/head-protocol/api-reference).

hydra-node/json-schemas/api.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,20 @@ channels:
454454
type: response
455455
method: POST
456456
bindingVersion: '0.1.0'
457+
/head-initialization:
458+
servers:
459+
- localhost-http
460+
subscribe:
461+
description: Get the last Head initialization time.
462+
operationId: getHeadInitialization
463+
message:
464+
summary: |
465+
Timestamp of the last seen Head initialization.
466+
bindings:
467+
http:
468+
type: response
469+
method: GET
470+
bindingVersion: '0.1.0'
457471

458472
components:
459473
messages:

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

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,31 @@ module Hydra.API.HTTPServer where
55
import Hydra.Prelude
66

77
import Cardano.Ledger.Core (PParams)
8+
import Conduit (
9+
ConduitT,
10+
MonadUnliftIO,
11+
concatC,
12+
linesUnboundedAsciiC,
13+
mapMC,
14+
runConduitRes,
15+
sinkList,
16+
sourceFileBS,
17+
(.|),
18+
)
819
import Control.Concurrent.STM (TChan, dupTChan, readTChan)
9-
import Data.Aeson (KeyValue ((.=)), object, withObject, (.:), (.:?))
20+
import Control.Lens ((^?))
21+
import Data.Aeson (KeyValue ((.=)), Value (String), object, withObject, (.:), (.:?))
1022
import Data.Aeson qualified as Aeson
23+
import Data.Aeson.Lens (key, _String)
1124
import Data.Aeson.Types (Parser)
1225
import Data.ByteString.Lazy qualified as LBS
1326
import Data.ByteString.Short ()
27+
import Data.List qualified as List
1428
import Data.Text (pack)
1529
import Hydra.API.APIServerLog (APIServerLog (..), Method (..), PathInfo (..))
1630
import Hydra.API.ClientInput (ClientInput (..))
1731
import Hydra.API.ServerOutput (ClientMessage (..), CommitInfo (..), ServerOutput (..), TimedServerOutput (..), getConfirmedSnapshot, getSeenSnapshot, getSnapshotUtxo)
18-
import Hydra.Cardano.Api (AddressInEra, LedgerEra, Tx)
32+
import Hydra.Cardano.Api (AddressInEra, LedgerEra, SlotNo, Tx)
1933
import Hydra.Chain (Chain (..), PostTxError (..), draftCommitTx)
2034
import Hydra.Chain.ChainState (IsChainState)
2135
import Hydra.Chain.Direct.State ()
@@ -28,6 +42,7 @@ import Hydra.Node.State (NodeState (..))
2842
import Hydra.Tx (CommitBlueprintTx (..), ConfirmedSnapshot, IsTx (..), Snapshot (..), UTxOType)
2943
import Network.HTTP.Types (ResponseHeaders, hContentType, status200, status202, status400, status404, status500)
3044
import Network.Wai (Application, Request (pathInfo, requestMethod), Response, consumeRequestBodyStrict, rawPathInfo, responseLBS)
45+
import System.Directory (doesFileExist)
3146

3247
newtype DraftCommitTxResponse tx = DraftCommitTxResponse
3348
{ commitTx :: tx
@@ -179,6 +194,13 @@ instance FromJSON SubmitL2TxResponse where
179194
instance Arbitrary SubmitL2TxResponse where
180195
arbitrary = genericArbitrary
181196

197+
data HeadInitializationDetails
198+
= HeadInitializationDetails
199+
{ time :: UTCTime
200+
, slot :: SlotNo
201+
}
202+
deriving (Eq, Show)
203+
182204
jsonContent :: ResponseHeaders
183205
jsonContent = [(hContentType, "application/json")]
184206

@@ -189,6 +211,7 @@ httpApp ::
189211
Tracer IO APIServerLog ->
190212
Chain tx IO ->
191213
Environment ->
214+
FilePath ->
192215
PParams LedgerEra ->
193216
-- | Get latest 'NodeState'.
194217
IO (NodeState tx) ->
@@ -203,7 +226,7 @@ httpApp ::
203226
-- | Channel to listen for events
204227
TChan (Either (TimedServerOutput tx) (ClientMessage tx)) ->
205228
Application
206-
httpApp tracer directChain env pparams getNodeState getCommitInfo getPendingDeposits putClientInput apiTransactionTimeout responseChannel request respond = do
229+
httpApp tracer directChain env stateFile pparams getNodeState getCommitInfo getPendingDeposits putClientInput apiTransactionTimeout responseChannel request respond = do
207230
traceWith tracer $
208231
APIHTTPRequestReceived
209232
{ method = Method $ requestMethod request
@@ -225,6 +248,9 @@ httpApp tracer directChain env pparams getNodeState getCommitInfo getPendingDepo
225248
("GET", ["snapshot", "last-seen"]) -> do
226249
hs <- headState <$> getNodeState
227250
respond . okJSON $ getSeenSnapshot hs
251+
("GET", ["head-initialization"]) ->
252+
handleHeadInitializationTime stateFile
253+
>>= respond
228254
("POST", ["snapshot"]) ->
229255
consumeRequestBodyStrict request
230256
>>= handleSideLoadSnapshot putClientInput apiTransactionTimeout responseChannel
@@ -530,6 +556,33 @@ handleSubmitL2Tx putClientInput apiTransactionTimeout responseChannel body = do
530556
_ -> go
531557
Right _ -> go
532558

559+
handleHeadInitializationTime :: MonadUnliftIO m => FilePath -> m Response
560+
handleHeadInitializationTime stateFile =
561+
liftIO (doesFileExist stateFile) >>= \case
562+
False -> pure $ responseLBS status400 jsonContent (Aeson.encode $ String $ "Could not read state file at path: " <> show stateFile)
563+
True -> do
564+
initializations <- runConduitRes $ sourceFileBS stateFile .| parseInitializingTime
565+
case initializations of
566+
[] -> pure $ responseLBS status400 jsonContent (Aeson.encode $ String "Unable to find Head initialization time in your state file.")
567+
as ->
568+
pure $ responseLBS status200 jsonContent (Aeson.encode $ List.last as)
569+
570+
parseInitializingTime :: MonadUnliftIO m => ConduitT ByteString Void m [Text]
571+
parseInitializingTime =
572+
linesUnboundedAsciiC
573+
.| mapMC maybeDecode
574+
.| concatC
575+
.| sinkList
576+
where
577+
maybeDecode :: Monad m => ByteString -> m (Maybe Text)
578+
maybeDecode bs =
579+
case bs ^? key "stateChanged" . key "tag" . _String of
580+
Nothing -> pure Nothing
581+
Just tag ->
582+
if tag == "HeadInitialized"
583+
then pure $ bs ^? key "time" . _String
584+
else pure Nothing
585+
533586
badRequest :: IsChainState tx => PostTxError tx -> Response
534587
badRequest = responseLBS status400 jsonContent . Aeson.encode . toJSON
535588

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ withAPIServer ::
8484
IsChainState tx =>
8585
APIServerConfig ->
8686
Environment ->
87+
FilePath ->
8788
Party ->
8889
EventSource (StateEvent tx) IO ->
8990
Tracer IO APIServerLog ->
@@ -93,7 +94,7 @@ withAPIServer ::
9394
(ClientInput tx -> IO ()) ->
9495
((EventSink (StateEvent tx) IO, Server tx IO) -> IO ()) ->
9596
IO ()
96-
withAPIServer config env party eventSource tracer chain pparams serverOutputFilter callback action =
97+
withAPIServer config env stateFile party eventSource tracer chain pparams serverOutputFilter callback action =
9798
handle onIOException $ do
9899
responseChannel <- newBroadcastTChanIO
99100
-- Initialize our read models from stored events
@@ -138,6 +139,7 @@ withAPIServer config env party eventSource tracer chain pparams serverOutputFilt
138139
tracer
139140
chain
140141
env
142+
stateFile
141143
pparams
142144
(atomically $ getLatest nodeStateP)
143145
(atomically $ getLatest commitInfoP)

hydra-node/src/Hydra/Node/Run.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ run opts = do
104104
withChain (chainStateHistory wetHydraNode) (wireChainInput wetHydraNode) $ \chain -> do
105105
-- API
106106
let apiServerConfig = APIServerConfig{host = apiHost, port = apiPort, tlsCertPath, tlsKeyPath, apiTransactionTimeout}
107-
withAPIServer apiServerConfig env party eventSource (contramap APIServer tracer) chain pparams serverOutputFilter (wireClientInput wetHydraNode) $ \(apiSink, server) -> do
107+
withAPIServer apiServerConfig env stateFile party eventSource (contramap APIServer tracer) chain pparams serverOutputFilter (wireClientInput wetHydraNode) $ \(apiSink, server) -> do
108108
-- Network
109109
let networkConfiguration =
110110
NetworkConfiguration

0 commit comments

Comments
 (0)