Skip to main content

18. Single state in Hydra.Node.

· 4 min read

Status

Accepted

Context

  • Currently the hydra-node maintains two pieces of state during the life-cycle of a Hydra Head:
    1. A HeadState tx provided by the HydraHead tx m handle interface and part of the Hydra.Node module. It provides the basis for the main hydra-node business logic in Hydra.Node.processNextEvent and Hydra.HeadLogic.updateCreation, Usage
    2. SomeOnChainHeadState is kept in the Hydra.Chain.Direct to keep track of the latest known head state, including notable transaction outputs and information how to spend it (e.g. scripts and datums) Code, Usage 1, Usage 2, Usage 3 (There are other unrelated things kept in memory like the event history in the API server or a peer map in the network heartbeat component.)
  • The interface between the Hydra.Node and a Hydra.Chain component consists of
    • constructing certain Head protocol transactions given a description of it (PostChainTx tx):
      postTx :: MonadThrow m => PostChainTx tx -> m ()
    • a callback function when the Hydra.Chain component observed a new Head protocol transaction described by OnChainTx tx:
      type ChainCallback tx m = OnChainTx tx -> m ()
  • Given by the usage sites above, the Hydra.Chain.Direct module requires additional info to do both, construct protocol transactions with postTx as well as observe potential OnChainTx (here). Hence we see that, operation of the Hydra.Chain.Direct component (and likely any implementing the interface fully) is inherently stateful.
  • We are looking at upcoming features to handle rollbacks and dealing with persisting the head state.
    • Both could benefit from the idea, that the HeadState is just a result of pure Event processing (a.k.a event sourcing).
    • Right now the HeadState kept in Hydra.Node alone, is not enough to fully describe the state of the hydra-node. Hence it would not be enough to just persist all the Events and replaying them to achieve persistence, nor resetting to some previous HeadState in the presence of a rollback.

Decision

  • We define and keep a "blackbox" ChainStateType tx in the HeadState tx

    • It shall not be introspectable to the business logic in HeadLogic
    • It shall contain chain-specific information about the current Hydra Head, which will naturally need to evolve once we have multiple Heads in our feature scope
    • For example:
    data HeadState tx
    = IdleState
    | InitialState
    { chainState :: ChainStateType tx
    -- ...
    }
    | OpenState
    { chainState :: ChainStateType tx
    -- ...
    }
    | ClosedState
    { chainState :: ChainStateType tx
    -- ...
    }
  • We provide the latest ChainStateType tx to postTx:

    postTx :: ChainStateType tx -> PostChainTx tx -> m ()
  • We change the ChainEvent tx data type and callback interface of Chain to:

    data ChainEvent tx
    = Observation
    { observedTx :: OnChainTx tx
    , newChainState :: ChainStateType tx
    }
    | Rollback ChainSlot
    | Tick UTCTime

    type ChainCallback tx m = (ChainStateType tx -> Maybe (ChainEvent tx)) -> m ()

    with the meaning, that invocation of the callback indicates receival of a transaction which is Maybe observing a relevant ChainEvent tx, where an Observation may include a newChainState.

  • We also decide to extend OnChainEffect with a ChainState tx to explicitly thread the used chainState in the Hydra.HeadLogic.

Consequences

  • We need to change the construction of Chain handles and the call sites of postTx
  • We need to extract the state handling (similar to the event queue) out of the HydraNode handle and shuffle the main of hydra-node a bit to be able to provide the latest ChainState to the chain callback as a continuation.
  • We need to make the ChainState serializable (ToJSON, FromJSON) as it will be part of the HeadState.
  • We can drop the TVar of keeping OnChainHeadState in the Hydra.Chain.Direct module.
  • We need to update DirectChainSpec and BehaviorSpec test suites to mock/implement the callback & state handling.
  • We might be able to simplify the ChainState tx to be just a UTxOType tx later.
  • As OnChainEffect and Observation values will contain a ChainStateType tx, traces will automatically include the full ChainState, which might be helpful but also possible big.

Alternative

  • We could extend PostChainTx (like Observation) with ChainState and keep the signatures:
postTx :: MonadThrow m => PostChainTx tx -> m ()
type ChainCallback tx m = (ChainState tx -> Maybe (ChainEvent tx) -> m ()
  • Not implemented as it is less clear on the need for a ChainState in the signatures.