Skip to content

Commit

Permalink
Remap ServerOutput to StateChanged
Browse files Browse the repository at this point in the history
Signed-off-by: Sasha Bogicevic <[email protected]>
  • Loading branch information
v0d1ch committed Feb 11, 2025
1 parent 3fe87c8 commit 0f792c2
Show file tree
Hide file tree
Showing 17 changed files with 428 additions and 486 deletions.
5 changes: 2 additions & 3 deletions hydra-node/src/Hydra/API/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import Hydra.API.HTTPServer (httpApp)
import Hydra.API.Projection (Projection (..), mkProjection)
import Hydra.API.ServerOutput (
CommitInfo (CannotCommit),
HeadStatus (Idle),
ServerOutput,
TimedServerOutput (..),
projectCommitInfo,
projectHeadStatus,
Expand All @@ -34,6 +32,7 @@ import Hydra.Cardano.Api (LedgerEra)
import Hydra.Chain (Chain (..))
import Hydra.Chain.ChainState (IsChainState)
import Hydra.Chain.Direct.State ()
import Hydra.HeadLogic.Outcome (HeadStatus (Idle), StateChanged)
import Hydra.Logging (Tracer, traceWith)
import Hydra.Network (IP, PortNumber)
import Hydra.Persistence (PersistenceIncremental (..))
Expand All @@ -59,7 +58,7 @@ import Network.WebSockets (

-- | Handle to provide a means for sending server outputs to clients.
newtype Server tx m = Server
{ sendOutput :: ServerOutput tx -> m ()
{ sendOutput :: StateChanged tx -> m ()
-- ^ Send some output to all connected clients.
}

Expand Down
220 changes: 28 additions & 192 deletions hydra-node/src/Hydra/API/ServerOutput.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,37 @@
module Hydra.API.ServerOutput where

import Control.Lens ((.~))
import Data.Aeson (Value (..), defaultOptions, encode, genericParseJSON, genericToJSON, omitNothingFields, withObject, (.:))
import Data.Aeson (Value (..), encode, withObject, (.:))
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Lens (atKey, key)
import Data.ByteString.Lazy qualified as LBS
import Hydra.API.ClientInput (ClientInput (..))
import Hydra.Chain (PostChainTx, PostTxError)
import Hydra.Chain.ChainState (IsChainState)
import Hydra.HeadLogic.State (HeadState)
import Hydra.Ledger (ValidationError)
import Hydra.Network (Host, NodeId)
import Data.Map.Strict qualified as Map
import Hydra.Chain.ChainState (IsChainState (ChainStateType))
import Hydra.HeadLogic.Outcome (StateChanged (..), HeadStatus (..))
import Hydra.Prelude hiding (seq)
import Hydra.Tx (
HeadId,
Party,
Snapshot,
SnapshotNumber,
IsTx,
)
import Hydra.Tx qualified as Tx
import Hydra.Tx.ContestationPeriod (ContestationPeriod)
import Hydra.Tx.Crypto (MultiSignature)
import Hydra.Tx.IsTx (ArbitraryIsTx, IsTx (..))
import Hydra.Tx.OnChainId (OnChainId)
import Test.QuickCheck.Arbitrary.ADT (ToADTArbitrary)
import Hydra.Tx.IsTx (IsTx (..))

-- | The type of messages sent to clients by the 'Hydra.API.Server'.
data TimedServerOutput tx = TimedServerOutput
{ output :: ServerOutput tx
{ output :: StateChanged tx
, seq :: Natural
, time :: UTCTime
}
deriving stock (Eq, Show, Generic)
deriving stock (Generic)

deriving stock instance IsChainState tx => Eq (TimedServerOutput tx)
deriving stock instance IsChainState tx => Show (TimedServerOutput tx)

instance Arbitrary (ServerOutput tx) => Arbitrary (TimedServerOutput tx) where
instance Arbitrary (StateChanged tx) => Arbitrary (TimedServerOutput tx) where
arbitrary = genericArbitrary

-- | Generate a random timed server output given a normal server output.
genTimedServerOutput :: ServerOutput tx -> Gen (TimedServerOutput tx)
genTimedServerOutput :: StateChanged tx -> Gen (TimedServerOutput tx)
genTimedServerOutput o =
TimedServerOutput o <$> arbitrary <*> arbitrary

Expand All @@ -55,157 +49,6 @@ instance IsChainState tx => FromJSON (TimedServerOutput tx) where
parseJSON v = flip (withObject "TimedServerOutput") v $ \o ->
TimedServerOutput <$> parseJSON v <*> o .: "seq" <*> o .: "timestamp"

data DecommitInvalidReason tx
= DecommitTxInvalid {localUTxO :: UTxOType tx, validationError :: ValidationError}
| DecommitAlreadyInFlight {otherDecommitTxId :: TxIdType tx}
deriving stock (Generic)

deriving stock instance (Eq (TxIdType tx), Eq (UTxOType tx)) => Eq (DecommitInvalidReason tx)
deriving stock instance (Show (TxIdType tx), Show (UTxOType tx)) => Show (DecommitInvalidReason tx)

instance (ToJSON (TxIdType tx), ToJSON (UTxOType tx)) => ToJSON (DecommitInvalidReason tx) where
toJSON = genericToJSON defaultOptions

instance (FromJSON (TxIdType tx), FromJSON (UTxOType tx)) => FromJSON (DecommitInvalidReason tx) where
parseJSON = genericParseJSON defaultOptions

instance ArbitraryIsTx tx => Arbitrary (DecommitInvalidReason tx) where
arbitrary = genericArbitrary

-- | Individual server output messages as produced by the 'Hydra.HeadLogic' in
-- the 'ClientEffect'.
data ServerOutput tx
= PeerConnected {peer :: NodeId}
| PeerDisconnected {peer :: NodeId}
| PeerHandshakeFailure
{ remoteHost :: Host
, ourVersion :: Natural
, theirVersions :: [Natural]
}
| HeadIsInitializing {headId :: HeadId, parties :: [Party]}
| Committed {headId :: HeadId, party :: Party, utxo :: UTxOType tx}
| HeadIsOpen {headId :: HeadId, utxo :: UTxOType tx}
| HeadIsClosed
{ headId :: HeadId
, snapshotNumber :: SnapshotNumber
, contestationDeadline :: UTCTime
-- ^ Nominal deadline until which contest can be submitted and after
-- which fanout is possible. NOTE: Use this only for informational
-- purpose and wait for 'ReadyToFanout' instead before sending 'Fanout'
-- as the ledger of our cardano-node might not have progressed
-- sufficiently in time yet and we do not re-submit transactions (yet).
}
| HeadIsContested {headId :: HeadId, snapshotNumber :: SnapshotNumber, contestationDeadline :: UTCTime}
| ReadyToFanout {headId :: HeadId}
| HeadIsAborted {headId :: HeadId, utxo :: UTxOType tx}
| HeadIsFinalized {headId :: HeadId, utxo :: UTxOType tx}
| CommandFailed {clientInput :: ClientInput tx, state :: HeadState tx}
| -- | Given transaction has been seen as valid in the Head. It is expected to
-- eventually be part of a 'SnapshotConfirmed'.
TxValid {headId :: HeadId, transactionId :: TxIdType tx, transaction :: tx}
| -- | Given transaction was not not applicable to the given UTxO in time and
-- has been dropped.
TxInvalid {headId :: HeadId, utxo :: UTxOType tx, transaction :: tx, validationError :: ValidationError}
| -- | Given snapshot was confirmed and included transactions can be
-- considered final.
SnapshotConfirmed
{ headId :: HeadId
, snapshot :: Snapshot tx
, signatures :: MultiSignature (Snapshot tx)
}
| GetUTxOResponse {headId :: HeadId, utxo :: UTxOType tx}
| InvalidInput {reason :: String, input :: Text}
| -- | A friendly welcome message which tells a client something about the
-- node. Currently used for knowing what signing key the server uses (it
-- only knows one), 'HeadStatus' and optionally (if 'HeadIsOpen' or
-- 'SnapshotConfirmed' message is emitted) UTxO's present in the Hydra Head.
Greetings
{ me :: Party
, headStatus :: HeadStatus
, hydraHeadId :: Maybe HeadId
, snapshotUtxo :: Maybe (UTxOType tx)
, hydraNodeVersion :: String
}
| PostTxOnChainFailed {postChainTx :: PostChainTx tx, postTxError :: PostTxError tx}
| IgnoredHeadInitializing
{ headId :: HeadId
, contestationPeriod :: ContestationPeriod
, parties :: [Party]
, participants :: [OnChainId]
}
| 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}
| CommitRecorded {headId :: HeadId, utxoToCommit :: UTxOType tx, pendingDeposit :: TxIdType tx, deadline :: UTCTime}
| CommitApproved {headId :: HeadId, utxoToCommit :: UTxOType tx}
| CommitFinalized {headId :: HeadId, theDeposit :: TxIdType tx}
| CommitRecovered {headId :: HeadId, recoveredUTxO :: UTxOType tx, recoveredTxId :: TxIdType tx}
| CommitIgnored {headId :: HeadId, depositUTxO :: [UTxOType tx], snapshotUTxO :: Maybe (UTxOType tx)}
deriving stock (Generic)

deriving stock instance IsChainState tx => Eq (ServerOutput tx)
deriving stock instance IsChainState tx => Show (ServerOutput tx)

instance IsChainState tx => ToJSON (ServerOutput tx) where
toJSON =
genericToJSON
defaultOptions
{ omitNothingFields = True
}

instance IsChainState tx => FromJSON (ServerOutput tx) where
parseJSON =
genericParseJSON
defaultOptions
{ omitNothingFields = True
}

instance (ArbitraryIsTx tx, IsChainState tx) => Arbitrary (ServerOutput tx) where
arbitrary = genericArbitrary

-- NOTE: Somehow, can't use 'genericShrink' here as GHC is complaining about
-- Overlapping instances with 'UTxOType tx' even though for a fixed `tx`, there
-- should be only one 'UTxOType tx'
shrink = \case
PeerConnected p -> PeerConnected <$> shrink p
PeerDisconnected p -> PeerDisconnected <$> shrink p
PeerHandshakeFailure rh ov tv -> PeerHandshakeFailure <$> shrink rh <*> shrink ov <*> shrink tv
HeadIsInitializing headId xs -> HeadIsInitializing <$> shrink headId <*> shrink xs
Committed headId p u -> Committed <$> shrink headId <*> shrink p <*> shrink u
HeadIsOpen headId u -> HeadIsOpen <$> shrink headId <*> shrink u
HeadIsClosed headId s t -> HeadIsClosed <$> shrink headId <*> shrink s <*> shrink t
HeadIsContested headId sn dl -> HeadIsContested <$> shrink headId <*> shrink sn <*> shrink dl
ReadyToFanout headId -> ReadyToFanout <$> shrink headId
HeadIsFinalized headId u -> HeadIsFinalized <$> shrink headId <*> shrink u
HeadIsAborted headId u -> HeadIsAborted <$> shrink headId <*> shrink u
CommandFailed i s -> CommandFailed <$> shrink i <*> shrink s
TxValid headId i tx -> TxValid <$> shrink headId <*> shrink i <*> shrink tx
TxInvalid headId u tx err -> TxInvalid <$> shrink headId <*> shrink u <*> shrink tx <*> shrink err
SnapshotConfirmed headId s ms -> SnapshotConfirmed <$> shrink headId <*> shrink s <*> shrink ms
GetUTxOResponse headId u -> GetUTxOResponse <$> shrink headId <*> shrink u
InvalidInput r i -> InvalidInput <$> shrink r <*> shrink i
Greetings me headStatus hydraHeadId snapshotUtxo hydraNodeVersion ->
Greetings
<$> shrink me
<*> shrink headStatus
<*> shrink hydraHeadId
<*> shrink snapshotUtxo
<*> shrink hydraNodeVersion
PostTxOnChainFailed p e -> PostTxOnChainFailed <$> shrink p <*> shrink e
IgnoredHeadInitializing{} -> []
DecommitRequested headId txid u -> DecommitRequested headId txid <$> shrink u
DecommitInvalid headId decommitTx decommitInvalidReason -> DecommitInvalid headId <$> shrink decommitTx <*> shrink decommitInvalidReason
DecommitApproved headId txid u -> DecommitApproved headId txid <$> shrink u
DecommitFinalized headId decommitTxId -> DecommitFinalized headId <$> shrink decommitTxId
CommitRecorded headId u i d -> CommitRecorded headId <$> shrink u <*> shrink i <*> shrink d
CommitApproved headId u -> CommitApproved headId <$> shrink u
CommitRecovered headId u rid -> CommitRecovered headId <$> shrink u <*> shrink rid
CommitFinalized headId theDeposit -> CommitFinalized headId <$> shrink theDeposit
CommitIgnored headId depositUTxO snapshotUTxO -> CommitIgnored headId <$> shrink depositUTxO <*> shrink snapshotUTxO

instance (ArbitraryIsTx tx, IsChainState tx) => ToADTArbitrary (ServerOutput tx)

-- | Whether or not to include full UTxO in server outputs.
data WithUTxO = WithUTxO | WithoutUTxO
deriving stock (Eq, Show)
Expand Down Expand Up @@ -249,6 +92,13 @@ prepareServerOutput ServerOutputConfig{utxoInSnapshot} response =
CommandFailed{} -> encodedResponse
TxValid{} -> encodedResponse
TxInvalid{} -> encodedResponse
TransactionReceived{} -> encodedResponse
DecommitRecorded{} -> encodedResponse
SnapshotRequestDecided{} -> encodedResponse
SnapshotRequested{} -> encodedResponse
PartySignedSnapshot{} -> encodedResponse
ChainRolledBack{} -> encodedResponse
TickObserved{} -> encodedResponse
SnapshotConfirmed{} ->
handleUtxoInclusion (key "snapshot" . atKey "utxo" .~ Nothing) encodedResponse
GetUTxOResponse{} -> encodedResponse
Expand All @@ -273,20 +123,6 @@ prepareServerOutput ServerOutputConfig{utxoInSnapshot} response =

encodedResponse = encode response

-- | All possible Hydra states displayed in the API server outputs.
data HeadStatus
= Idle
| Initializing
| Open
| Closed
| FanoutPossible
| Final
deriving stock (Eq, Show, Generic)
deriving anyclass (ToJSON, FromJSON)

instance Arbitrary HeadStatus where
arbitrary = genericArbitrary

-- | All information needed to distinguish behavior of the commit endpoint.
data CommitInfo
= CannotCommit
Expand All @@ -296,17 +132,17 @@ data CommitInfo
--

-- | Projection to obtain the list of pending deposits.
projectPendingDeposits :: IsTx tx => [TxIdType tx] -> ServerOutput tx -> [TxIdType tx]
projectPendingDeposits :: IsTx tx => [TxIdType tx] -> StateChanged tx -> [TxIdType tx]
projectPendingDeposits txIds = \case
CommitRecorded{pendingDeposit} -> pendingDeposit : txIds
CommitRecorded{pendingDeposit} -> Map.keys pendingDeposit <> txIds
CommitRecovered{recoveredTxId} -> filter (/= recoveredTxId) txIds
CommitFinalized{theDeposit} -> filter (/= theDeposit) txIds
CommitFinalized{depositTxId} -> filter (/= depositTxId) txIds
_other -> txIds

-- | Projection to obtain 'CommitInfo' needed to draft commit transactions.
-- NOTE: We only want to project 'HeadId' when the Head is in the 'Initializing'
-- state since this is when Head parties need to commit some funds.
projectCommitInfo :: CommitInfo -> ServerOutput tx -> CommitInfo
projectCommitInfo :: CommitInfo -> StateChanged tx -> CommitInfo
projectCommitInfo commitInfo = \case
HeadIsInitializing{headId} -> NormalCommit headId
HeadIsOpen{headId} -> IncrementalCommit headId
Expand All @@ -317,15 +153,15 @@ projectCommitInfo commitInfo = \case
-- | Projection to obtain the 'HeadId' needed to draft a commit transaction.
-- NOTE: We only want to project 'HeadId' when the Head is in the 'Initializing'
-- state since this is when Head parties need to commit some funds.
projectInitializingHeadId :: Maybe HeadId -> ServerOutput tx -> Maybe HeadId
projectInitializingHeadId :: Maybe HeadId -> StateChanged tx -> Maybe HeadId
projectInitializingHeadId mHeadId = \case
HeadIsInitializing{headId} -> Just headId
HeadIsOpen{} -> Nothing
HeadIsAborted{} -> Nothing
_other -> mHeadId

-- | Projection function related to 'headStatus' field in 'Greetings' message.
projectHeadStatus :: HeadStatus -> ServerOutput tx -> HeadStatus
projectHeadStatus :: HeadStatus -> StateChanged tx -> HeadStatus
projectHeadStatus headStatus = \case
HeadIsInitializing{} -> Initializing
HeadIsOpen{} -> Open
Expand All @@ -335,8 +171,8 @@ projectHeadStatus headStatus = \case
_other -> headStatus

-- | Projection of latest confirmed snapshot UTxO.
projectSnapshotUtxo :: Maybe (UTxOType tx) -> ServerOutput tx -> Maybe (UTxOType tx)
projectSnapshotUtxo :: Maybe (UTxOType tx) -> StateChanged tx -> Maybe (UTxOType tx)
projectSnapshotUtxo snapshotUtxo = \case
SnapshotConfirmed _ snapshot _ -> Just $ Tx.utxo snapshot
HeadIsOpen _ utxos -> Just utxos
HeadIsOpen _ _ utxos -> Just utxos
_other -> snapshotUtxo
5 changes: 3 additions & 2 deletions hydra-node/src/Hydra/API/ServerOutputFilter.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Hydra.API.ServerOutputFilter where

import Hydra.API.ServerOutput (ServerOutput (..), TimedServerOutput, output)
import Hydra.API.ServerOutput (TimedServerOutput, output)
import Hydra.Cardano.Api (
Tx,
serialiseToBech32,
Expand All @@ -12,6 +12,7 @@ import Hydra.Prelude hiding (seq)
import Hydra.Tx (
Snapshot (..),
)
import Hydra.HeadLogic (StateChanged(..))

newtype ServerOutputFilter tx = ServerOutputFilter
{ txContainsAddr :: TimedServerOutput tx -> Text -> Bool
Expand All @@ -22,7 +23,7 @@ serverOutputFilter :: ServerOutputFilter Tx =
ServerOutputFilter
{ txContainsAddr = \response address ->
case output response of
TxValid{transaction} -> matchingAddr address transaction
TxValid{tx} -> matchingAddr address tx
TxInvalid{transaction} -> matchingAddr address transaction
SnapshotConfirmed{snapshot = Snapshot{confirmed}} -> any (matchingAddr address) confirmed
_ -> True
Expand Down
Loading

0 comments on commit 0f792c2

Please sign in to comment.