Skip to main content

5 posts tagged with "Draft"

View All Tags

· 4 min read

Status

Draft

Context

  • Hydra-node currently requires a whole slew of command-line arguments to configure properly its networking layer: --peer to connect to each peer, --cardano-verification-key and --hydra-verification-key to identify the peer on the L1 and L2 respectively.
  • This poses significant challenges for operating a cluster of Hydra nodes as one needs to know beforehand everything about the cluster, then pass a large number of arguments to some program or docker-compose file, before any node can be started
    • This is a pain that's been felt first-hand for benchmarking and testing purpose
  • Having static network configuration is probably not sustainable in the long run, even if we don't add any fancy multihead capabilities to the node, as it would make it significantly harder to have automated creation of Heads.
  • There's been an attempt at providing a file-based network configuration but this was deemed unconvincing
  • Hydra paper (sec. 4, p. 13) explicitly assumes the existence of a setup phase
    • This setup is currently left aside, e.g. exchange of keys for setting up multisig and identifying peers. The hydra-node executable is statically configured and those things are assumed to be known beforehand

Decision

  • Hydra-node exposes an Administrative API to enable configuration of the Hydra network using "standard" tools
    • API is exposed as a set of HTTP endpoints on some port, consuming and producing JSON data,
    • It is documented as part of the User's Guide for Hydra Head
  • This API provides commands and queries to:
    • Add/remove peers providing their address and keys,
    • List currently known peers and their connectivity status,
    • Start/stop/reset the Hydra network
  • This API is implemented by a new component accessible through a network port separate from current Client API, that configures the Network component

The following picture sketches the proposed architectural change:

Architecture change

Q&A

  • Why a REST interface?
    • This API is an interface over a specific resource controlled by the Hydra node, namely its knowledge of other peers with which new Head_s can be opened. As such a proper REST interface (_not RPC-in-disguise) seems to make sense here, rather than stream/event-based duplex communication channels
    • We can easily extend such an API with WebSockets to provide notifications (e.g. peers connectivity, setup events...)
  • Why a separate component?
    • We could imagine extending the existing APIServer interface with new messages related to this network configuration, however this seems to conflate different responsibilities in a single place: Configuring and managing the Hydra node itself, and configuring, managing, and interacting with the Head itself
    • "Physical" separation of endpoints makes it easier to secure a very sensitive part of the node, namely its administration, e.g by ensuring this can only be accessed through a specific network interface, without relying on application level authentication mechanisms

Consequences

  • It's easy to deploy Hydra nodes with some standard configuration, then dynamically configure them, thus reducing the hassle of defining and configuring the Hydra network
  • It makes it possible to reconfigure a Hydra node with different peers
  • The Client API should reflect the state of the network and disable Initing a head if the network layer is not started
    • In the long run, it should also have its scope reduced to represent only the possible interactions with a Head, moving things related to network connectivity and setup to the Admin API
    • In a Managed Head scenario it would even make sense to have another layer of separation between the API to manage the life-cycle of the Head and the API to make transactions within the Head
  • Operational tools could be built easily on top of the API, for command-line or Web-based configuration

· 2 min read

Status

Draft

Context

Current Hydra networking layer is based on Ouroboros network framework networking stack which, among other features, provides:

  1. An abstraction of stream-based duplex communication channels called a Snocket,
  2. A Multiplexing connection manager that manages a set of equivalent peers, maintains connectivity, and ensures diffusion of messages to/from all peers,
  3. Typed protocols for expressing the logic of message exchanges as a form of state machine.

While it's been working mostly fine so far, the abstractions and facilities provided by this network layer are not well suited for Hydra Head networking. Some of the questions and shortcomings are discussed in a document on Networking Requirements, and as the Hydra Head matures it seems time is ripe for overhauling current network implementation to better suite current and future Hydra Head networks needs.

Decision

  • Hydra Head nodes communicate by sending messages to other nodes using UDP protocol

Details

  • How do nodes know each other?: This is unspecified by this ADR and left for future work, it is assumed that a Hydra node operator knows the IP:Port address of its peers before opening a Head with them
  • Are messages encrypted?: This should probably be the case in order to ensure Heads' privacy but is also left for future work
  • How are nodes identified?: At the moment they are identified by their IP:Port pair. As we implement more of the setup process from section 4 of the Hydra Head paper, we should identify nodes by some public key(hash) and resolve the actual IP:Port pair using some other mechanism

Consequences

  • Node's HeadLogic handles lost, duplicates, and out-of-order messages using retry and timeout mechanisms
  • Messages should carry a unique identifier, eg. source node and index
  • Protocol, eg. messages format, is documented

· 5 min read

Status

Draft

Context

  • ADR-3 concluded that a full-duplex communication channels are desirable to interact with a reactive system.

  • The Client API communicates several types of messages to clients. Currently this ranges from node-level PeerConnected, over head-specific HeadIsOpen to messages about transactions like TxValid. These messages are all of type StateChanged.

  • Current capabilities of the API:

    • Clients can retrieve the whole history of StateChanged messages or opt-out using a query parameter - all or nothing.

    • There is a welcome message called Greetings which is always sent, that contains the last headStatus.

    • There exists a GetUTxO query-like ClientInput, which will respond with a GetUTxOResponse containing the confirmed UTxO set in an open head, or (!) the currently committed UTxO set when the head is initializing.

    • While overall json encoded, clients can choose choose between json or binary (cbor) output of transaction fields in several of these using a query parameter.

  • Many of these features have been added in a "quick and dirty" way, by monkey patching the encoded JSON.

  • The current capabalities even do not satisfy all user needs:

    • Need to wade through lots of events to know the latest state (except the very basic headStatus from the Greetings).

    • Need to poll GetUTxO or aggregate confirmed transactions on client side to know the latest UTxO set for constructing transactions.

    • Inclusion of the whole UTxO set in the head is not always desirable and filtering by address would be beneficial. (not addressed in this ADR though, relevant discussion #797)

    • As ADR-15 also proposes, some clients may not need (or should not have) access to administrative information.

  • It is often a good idea to separate the responsibilities of Commands and Queries (CQRS), as well as the model they use.

Decision

  • Drop GetUTxO and GetUTxOResponse messages as they advocate a request/response way of querying.

  • Realize that ClientInput data is actually a ClientCommand (renaming them) and that ServerOutput are just projections of the internal event stream (see ADR-24) into read models on the API layer.

  • Compose a versioned (/v1) API out of resource models, which compartmentalize the domain into topics on the API layer.

    • A resource has a model type and the latest value is the result of a pure projection folded over the StateChanged event stream, i.e. project :: model -> StateChanged -> model.

    • Each resource is available at some HTTP path, also called "endpoint":

      • GET requests must respond with the latest state in a single response.

      • GET requests with Upgrade: websocket headers must start a websocket connection, push the latest state as first message and any resource state updates after.

      • Other HTTP verbs may be accepted by a resource handler, i.e. to issue resource-specific commands. Any commands accepted must also be available via the corresponding websocket connection.

    • Accept request headers can be used to configure the Content-Type of the response

      • All resources must provide application/json responses

      • Some resources might support more content types (e.g. CBOR-encoded binary)

    • Query parameters may be used to further configure responses of some resources. For example, ?address=<bech32> could be used to filter UTxO by some address.

  • Keep the semantics of /, which accepts websocket upgrade connections and sends direct/raw output of ServerOutput events on /, while accepting all ClientCommand messages.

    • Define ServerOutput also in terms of the StateChanged event stream

Example resources

Example resource paths + HTTP verbs mapped to existing things to demonstrate the effects of the decision points above. The mappings may change and are to be documented by an API specification instead.

PathGETPOSTPATCHDELETE
/v1/head/statusHeadStatus(..)---
/v1/head/snapshot/utxolast confirmed snapshot utxo---
/v1/head/snapshot/transactionsconfirmed snapshot txsNewTx + responses--
/v1/head/ledger/utxolocalUTxO---
/v1/head/ledger/transactionslocalTxsNewTx + responses--
/v1/head/commit-Chain{draftCommitTx}--
/v1/headall /v1/head/* dataInitCloseFanout / Abort
/v1/protocol-parameterscurrent protocol parameters
/v1/cardano-transaction-Chain{submitTx}--
/v1/peersa list of peers---
/v1/node-versionnode version as in Greetings---
/v1/all /v1/* data---

Multiple heads are out of scope now and hence paths are not including a <headId> variable section.

Consequences

  • Clear separation of what types are used for querying and gets subscribed to by clients and we have dedicated types for sending data to clients

  • Changes on the querying side of the API are separated from the business logic.

  • Clients do not need to aggregate data that is already available on the server side without coupling the API to internal state representation.

  • Separation of Head operation and Head usage, e.g. some HTTP endpoints can be operated with authentication.

  • Clients have a fine-grained control over what to subscribe to and what to query.

  • Versioned API allows clients to detect incompatibility easily.

  • Need to rewrite how the hydra-tui is implemented.

· 4 min read

Status

Draft

Context

  • ADR 18 merged both headState and chainState into one single state in the Hydra node, giving the chain layer a way to fetch and update the chainState when observing a chain event.

  • ADR 23 outlined the need for a local chain state in the chain layer again to correctly handle correct observation of multiple relevant transactions and the resulting chainState updates.

  • The ChainStateType tx for our "actual" Cardano chain layer is currently:

    data ChainStateAt = ChainStateAt
    { chainState :: ChainState
    , recordedAt :: Maybe ChainPoint
    }

    data ChainState
    = Idle
    | Initial InitialState
    | Open OpenState
    | Closed ClosedState

    where InitialState, OpenState and ClosedState hold elaborate information about the currently tracked Hydra head.

  • We face difficulties to provide sufficient user feedback when an initTx was observed but (for example) keys do not match our expectation.

    • Core problem is, that observeInit is required to take a decision whether it wants to "adopt" the Head by returning an InitialState or not.
    • This makes it impossible to provide user feedback through the HeadLogic and API layers.
  • We want to build a Hydra head explorer, which should be able to keep track and discover Hydra heads and their state changes even when the heads were initialized before starting the explorer.

Decision

  • We supersede ADR 18 with the current ADR.

Changes internal to Direct chain layer

  • Introduce a ResolvedTx type that has its inputs resolved. Where a normal Tx will only contain TxIn information of its inputs, a ResolvedTx also includes the TxOut for each input.

  • Change ChainSyncHandler signature to onRollForward :: BlockHeader -> [ResolvedTx] -> m ()

  • Change observing function signature to observeSomeTx :: ChainContext -> ResolvedTx -> Maybe (OnChainTx Tx). Notably there is no ChainState involved.

  • Do not guard observation by HeadId in the chain layer and instead do it in the HeadLogic layer.

  • Define a SpendableUTxO type that is a UTxO with potentially needed datums included.

    • TBD: instead we could decide to use inline datums and rely on UTxO containing them
  • Change transaction creation functions initialize, commit, abort, collect, close, contest and fanout in Hydra.Direct.Chain.State to take SpendableUTxO and HeadId/HeadParameters as needed.

  • Extend IsChainState type class to enforce that it can be updated by concurrent transactions update :: ChainStateType tx -> [tx] -> ChainStateType tx.

    • While this is not strictly needed "outside" of the chain layer, it will have us not fall into the same pit again.
  • Change ChainStateAt to only hold a spendableUTxO and the recordedAt.

  • Update the LocalChainState in onRollForward by using update and pushing a new ChainStateAt generically.

TBD:

  • Impact on generators

Chain interface changes

  • Add HeadId and HeadParameters to PostChainTx.

  • Add HeadId to all OnChainTx constructors.

  • Extend OnInitTx with observed chain participants.

    • TBD: How are cardano verification keys generically represented in HeadLogic?
  • Extend OnContestTx with new deadline and a list of contesters.

  • Move off-chain checks for what makes a "proper head" to HeadLogic

TBD:

  • Merge HeadSeed and HeadId? How to abstract?

Consequences

  • All logic is kept in the logic layer and no protocol decisions (i.e. whether to adopt or ignore a head initialization) are taken in the chain layer.

    • The HeadLogic gets informed of any proper initTx and can log that it is ignored and for what reason.
  • The transaction observation and construction functions can be moved into a dedicated package that is cardano-specific, but not requires special state knowledge of the "direct chain following" and can be re-used as a library.

  • All transaction observation functions used by observeSomeTx will need to be able to identify a Hydra Head transaction from only the ResolvedTx and the ChainContext

  • Any Chain Tx implementation wanting to re-use existing transaction observation functions must be able to resolve transaction inputs (against some ledger state) and produce ResolvedTx.

    • A chain-following implementation (as Hydra.Chain.Direct) can keep previous transactions around.
    • A chain indexer on "interesting" protocol addresses can be used to efficiently query most inputs.
  • We can get rid of the Hydra.Chain.Direct.State glue code altogether.

  • While this does not directly supersede ADR23, it paves the way to remove LocalChainState again as the ChainStateAt is now combinable from multiple transactions (see update above) and we can keep the state (again) only in the HeadState aggregate. Note that this would shift the rollback handling back into the logic layer.

· 4 min read
Elaine Cardenas
Pi Lanningham
Sebastian Nagel

Status

Draft

Context

  • The Hydra node represents a significant engineering asset, providing layer 1 monitoring, peer to peer consensus, durable persistence, and an isomorphic Cardano ledger. Because of this, it is being eyed as a key building block not just in Hydra based applications, but other protocols as well.

  • Currently the hydra-node uses a very basic persistence mechanism for it's internal HeadState, that is saving StateChanged events to file on disk and reading them back to load and re-aggregate the HeadState upon startup.

    • Some production setups would benefit from storing these events to a service like Amazon Kinesis data stream instead of local files.
  • The hydra-node websocket-based API is the only available event stream right now and might not fit all purposes.

    • See also ADR 3 and 25
    • Internally, this is realized as a single Server handle which can sendOutput :: ServerOutput tx -> m ()
    • These ServerOutputs closely relate to StateChanged events and ClientEffects are yielded by the logic layer often together with the StateChanged. For example:
    onInitialChainAbortTx newChainState committed headId =
    StateChanged HeadAborted{chainState = newChainState}
    <> Effects [ClientEffect $ ServerOutput.HeadIsAborted{headId, utxo = fold committed}]
  • Users of hydra-node are interested to add alternative implementations for storing, loading and consuming events of the Hydra protocol.

Decision

  • We create two new interfaces in the hydra-node architecture:

    • data EventSource e m = EventSource { getEvents :: m [e] }
    • data EventSink e m = EventSink { putEvent :: e -> m () }
  • We realize our current PersistenceIncremental used for persisting StateChanged events is both an EventSource and an EventSink

  • We drop the persistence from the main handle HydraNode tx m, add one EventSource and allow many EventSinks

data HydraNode tx m = HydraNode
{ -- ...
, eventSource :: EventSource (StateChanged tx) m
, eventSinks :: [EventSink (StateChanged tx) m]
}
  • The hydra-node will load events and __hydra_te its HeadState using getEvents of the single eventSource.

  • The stepHydraNode main loop does call putEvent on all eventSinks in sequence. Any failure will make the hydra-node process terminate and require a restart.

  • When loading events from eventSource on hydra-node startup, it will also re-submit events via putEvent to all eventSinks.

  • The default hydra-node main loop does use the file-based EventSource and a single file-based EventSink (using the same file).

  • We realize that the EventSource and EventSink handles, as well as their aggregation in HydraNode are used as an API by forks of the hydra-node and try to minimize changes to it.

Consequences

  • The default operation of the hyda-node remains unchanged.

  • There are other things called Event and EventQueue(putEvent) right now in the hydra-node. This is getting confusing and when we implement this, we should also rename several things first (tidying).

  • Interface first: Implementations of EventSink should specify their format in a non-ambiguous and versioned way, especially when a corresponding EventSource exists.

  • The API Server can be modelled and refactored as an EventSink.

  • Projects forking the hydra node have dedicated extension points for producing and consuming events.

  • Sundae Labs can build a "Save transaction batches to S3" proof of concept EventSink.

  • Sundae Labs can build a "Scrolls source" EventSink.

  • Sundae Labs can build a "Amazon Kinesis" EventSource and EventSink.

Out of scope / future work

  • Available implementations for EventSource and EventSink could be

    • configured upon hydra-node startup using for example URIs: --event-source file://state or --event-sink s3://some-bucket
    • dynamically loaded as plugins without having to fork hydra-node.
  • The Network and Chain parts qualify as EventSinks as well or shall those be triggered by Effects still?