owned this note
owned this note
Published
Linked with GitHub
# Hydra Head Protocol - Plutus Walkthrough
# Introduction
In this article, we will cover how to build a Plutus smart contract that drives a simplified version of the Hydra head protocol. we will go over code examples, talk about the EUTxO model and also cover some of the current pitfalls or hard-to-grasp details we have discovered during our journey into Plutus. We hope that this article will give some interesting perspectives to DApp developers looking into Plutus by covering a real non-trivial use case.
# Hydra: Head Protocol, High-Level Overview
I'd like to start by giving a little overview of the (simplified) _Hydra Head protocol_ we'll be considering in this walk-through. We do not expect readers to have extensive knowledge of Hydra though we'd greatly recommend reading the paper if you're interested in the topic: [Hydra: Fast Isomorphic State Channels].
In a Hydra Head, multiple participants want to securely lock a set of UTxO they own to enable fast-paced transactions using dedicated communication channels, out of the main chain. In Hydra words, we say that the participants are **opening a head** and **committing UTxOs into a head**. With Hydra, we want to gives strong guarantees about security and in particular that:
- A head participant cannot lose funds he/she hasn't specifically agree to spend.
- While a head is running, the UTxOs committed by the participants are locked and unusable on the main chain.
- A head can be opened, closed or aborted by any of the participants.
We like to think of the Head as a _poker game_: several participants may decide to gather for a game, bringing their chips with them. Chips will change hands on the table as the game progresses and participants are free to leave at any time with their chips. The slight difference between hydra and poker is that if someone decides to leave the table, everyone must leave. Then is recorded on the mainchain only the final state and how many chips participants walk away with; not the detail and all transfers of the entire game.

<small><small><p align="right">Photo: Keenan Constance</p></small></small>
In this walkthrough, we'll mainly focus on the part that is establishing the head (opening the table). Once established, most of the communication happens outside of the main chain and it'll be a topic for another time. To open a head, participants must go through the following step:
- One participant must **initialize** the protocol by sending a transaction to the mainchain forging and assigning participation tokens to each participant.
- Each participant must then **commit** one or more UTxO to the head.
- Once all commits are done, one participant must then **collect** all commits and form the initial state of the head.
After the `initial` transaction, it should also be possible for any participant to _abort_ the head in which case commits should be refunded and participation tokens destroyed. Let's consider a scenario with two head participant, each committing a single UTxO to the head:

There are 4 transactions involved in the process, one `Init`, two `Commit` and one `CollectCom`. The tokens (⦿) created by the initial transaction entitles participants to redeem one output of the init transaction and commit UTxOs. One output of the initial transaction is also used to keep track of the head state, and the collectCom transaction evolves that state into a new one, which records the UTxOs that were committed in earlier transactions. The collectCom transaction enforces that all minted participation tokens are present.
We'll divide that contract into three on-chain validators:
- One initial validator checking that only targeted participant can commit a UTxO and redeem the participation token intended for them.
- One commit validator checks that a commit from a participant can only be redeemed by a collectCom transaction, in the context of a Hydra head.
- One hydra validator checks that the head is established according to a state machine with well-defined transitions.
> **Key takeaways**
>
> - A Hydra head follows a state-machine where transitions are materialized by on-chain transactions.
> - Participants must commit (and therefore lock) funds to a Hydra head to participate.
> - A Hydra head can be closed or aborted by any of the participants at any time.
# A practical view of the EUTxO model
Before diving in, we want to share how we visualize EUTxO, and why this helps me for writing smart contracts. So let's remind ourselves what a UTxO is. Fundamentally, transactions are made of **inputs** and **outputs**. An input is a reference to a previous unspent transaction output, which is what we call in short a **UTxO**. An output carries some **value** and a way to define who / what is allowed to spend it; we also call that an **address**. The address can be either referencing a public key, or a script / smart contract. The EUTxO model introduces two extra elements in the classic representation: a **datum** and a **redeemer**. The datum is provided alongside the address and value when the output is created whereas the redeemer must be provided only later when the output is being spent.
Conceptually, we like to think of the **datum** as a **configuration** or a setting for the smart contract. When sending to a smart-contract address, we specify in the output itself the code that will need to run (and validate!) for spending the output. This is quite rigid in itself because it means that changing the code also changes the address. Imagine for instance that you're writing a contract that says that an output can only be redeemed if spent after the 14th of July 2021. Surely, if you wanted to re-use the contract but with a different date, you should not need to change any of the logic; it'd be nice for the code to say roughly the same but, be configurable. This is where the datum kicks in because it allows you to specify a parameter to your contract when attaching it to an output. Therefore, the contract can be configured with different settings without the need to alter the code.
The **redeemer** however, is more like a **user input** or proof that you are entitled to redeem the output. Many contracts won't need a redeemer because they may be able to figure out from the transaction itself whether conditions are already met to allow spending. Indeed, the validator of the contract has access not only to the datum and redeemer but can inspect the entire transaction, its inputs, outputs, signatures and whatnot.
To some extent, a contract is like a locked chest. The size and shape of the keyhole are defined by the address and the datum; whereas the redeemer is the key itself that has to fit into the keyhole. Perhaps there's no keyhole (and therefore no key) and the chest may only open when certain conditions are met (for instance, if you flip it while wearing pink clothes) because the chest can observe its surrounding environment.

<small><small><p align="right">Photo: Immo Wegmann</p></small></small>
So, what can be somewhat hard to grasp when working with EUTxO at first is this whole validation dance which only occurs when an output is being spent. There's no global state, there's no contract instantiation. The conditions for spending an output are fully created with the output itself. This also means that it is very much possible to create an output that will be forever non-spendable if we aren't careful. On the plus side, it makes it also easier to reason about smart contracts in the EUTxO model because the context of validation solely depends on the transaction spending a UTxO.
> **Key takeaways**
>
> - Smart contracts are executed when spending an address they protect, and they can inspect the entire surrounding transaction for validation.
> - EUTxO refers to an unspent transaction output, made of an address (possibly owned by a smart contract), a value and a datum.
> - A datum allows for configuring a smart contract for better re-usability of logic.
> - A redeemer is a user input given to the contract for extra validation.
# About Writing Smart-Contracts
Throughout the rest of the article, we will follow a bottom-up approach, starting from the on-chain code and up to the off-chain client code. The reasons for doing this in my opinion are similar to what's TDD (Test-Driven Development) tries to prevent: do not influence yourself into writing validation which only complies with what you expect. As a matter of fact, remember that the off-chain code cannot be trusted; it may as well have been written by another malicious application. You can't expect that every transaction seen by your on-chain code will be structured according to your off-chain code. This is of the utmost importance: only the on-chain code matters when it comes to _guarantees_. That code must be a little bit paranoid and be picky about the validations it performs on a transaction although the ledger itself provides already some strong validations upfront that a contract may leverage.
Hence the bottom-up approach, writing validations first and what we expect a transaction to satisfy before actually writing the code that will construct such a transaction. In the end, when the validation code has been well written, constructing the transaction in the off-chain code feels a bit redundant. Doing the opposite can be misleading for we'll tend to only validate the bits we have thought about constructing. I hope this will get more concrete as we go through the on-chain code for Hydra.
In terms of code organization, we'll divide the source code in mainly two modules: `OnChain` vs `OffChain`. Although both codes are written in Haskell, the on-chain code is special in many aspects and we ought to treat it differently to avoid confusion. Firstly, remember that the on-chain code must include all the code necessary for its execution and has only access to a restricted set of language built-ins.

The Plutus framework comes with several primitives and data structures built on top of those primitives and although they've been named similarly to the common Haskell data structure found in the base prelude, they aren't the same ones. This is incidentally why almost every function used in the on-chain code has to be "inlineable" by the compiler; they must fit in the contract code itself! On the other hand, the off-chain code can be full-fledged Haskell and rely on common abstractions and data structures. Mixing up on-chain and off-chain code as part of the same module can be therefore awkward and lead to rather unfriendly error messages. Keeping them separated works well, if only for the mental model.
> **Key takeaways**
>
> - Don't assume, validate. Only on-chain code can be trusted.
> - On-chain code must include everything it needs in the contract; complex data structures and high-level abstractions are therefore costly.
> - Keep the separation between on-chain and off-chain code clear.
# The Monetary Policy
A Hydra head uses a monetary policy to issue participation tokens and to some extent authenticate each head participant with that token. It is therefore important for that tokens to have a few properties:
- Each token must be unique per participant, across all possible heads.
- Tokens must only be minted _all at once_ during the `init` transaction.
- Tokens must only be burned _all at once_ during the `fanout` transaction.
Said differently, the monetary policy can be used as an identifier for the head, and participation tokens as identifiers for each participant. In theory, we may imagine the same participants starting different heads so relying solely on the participant's public keys for creating the monetary policy (or identifying participants) will not work. Ideally, we need a good source of randomness and a global index that enforces that a chosen identifier is unique. Unfortunately, these sort of primitives do not exist in the on-chain code and would be expensive to build. There's however a nice source of unique one-time _things_ at reach: the Cardano blockchain. We can leverage the UTxO model and piggyback on the ledger which already enforces that UTxO is unique and can only be spent once.
Fundamentally, a Plutus monetary policy is like a smart contract with no datum and redeemer whatsoever. It must fully validate from the minting (resp. burning) transaction as context. Yet, since monetary policies are created on the fly, we can embed a certain UTxO reference into the policy's validator making it unique (for the UTxO reference itself is unique), and giving us an easy way to enforce that minting can only happen once:
<p align="right"><strong>OnChain</strong></p>
```hs
-- The validator is parameterized by the public keys of the participants, as well as a reference
-- to a particular UTxO. A minting transaction will be considered valid if it can spend
-- that UTxO (automatically ensuring that minting can only happen *once*).
--
-- What is not necessarily transparent here is that the on-chain code is made of the
-- curried function 'ScriptContext -> Bool', after the first parameter has been partially
-- applied. This is similar to what is called 'closures' in some languages. Fundamentally, the
-- parameter 'TxOutRef' is embedded within the policy and is part of the on-chain code itself!
validateMonetaryPolicy
:: TxOutRef
-> ScriptContext
-> Bool
validateMonetaryPolicy outRef ctx =
validateMinting || validateBurning
where
-- NOTE / TODO:
-- The scope of this walkthrough does not cover the final `fanout` of the Hydra head,
-- we therefore need not worry about this validation _yet_ and will simply leave it
-- to `False` which do not affect the semantic of the validator but still, leaves a
-- visual indicator about the validation for burning token.
validateBurning =
False
validateMinting =
let
constraints = mustSpendPubKeyOutput outRef
in
checkScriptContext constraints ctx
hydraMonetaryPolicy
:: TxOutRef
-> MonetaryPolicy
hydraMonetaryPolicy outRef =
mkMonetaryPolicyScript $
$$(PlutusTx.compile [|| Scripts.wrapMonetaryPolicy . validateMonetaryPolicy ||])
`PlutusTx.applyCode` PlutusTx.liftCode outRef
```
Note that, as extra conditions, we could require that the minting transaction is also initiating a Hydra head and sends money to the Hydra contract (see sections below), or, that minting indeed create exactly one token per participant of the head. This is however superfluous here. We said earlier that the on-chain validation code had to be quite paranoid, but we also do not have to make our lives more complicated than necessary. In this particular case, we are picking the UTxO, and since the policy requires spending it then necessarily we must fully own that UTxO. Remember that the complete policy is only disclosed during the init transaction that we are constructing ourselves. Since we are also choosing the UTxO at that moment we can also make sure the init transactions is well-constructed and send participation tokens to each participant.
Said differently, if someone tries to use a different UTxO and craft a transaction that would not be sending tokens to participants, then, necessarily, they will end up with a different policy id and none of that would be valid. What our on-chain code really ought to prevent and validate is that no other participation tokens can be minted whatsoever beyond the first init transaction. This is indeed the case because the same UTxO cannot be spent twice!
In brief, we _could_ add extra validation in the validator here to make sure that we are constructing the init transaction correctly; this would be merely a safety net to guard us against doing silly mistakes in our off-chain code but it doesn't provide any extra security guarantees. We prefer keeping the on-chain code simpler and easier to reason about, only to check for silly mistake through property testing. Let's now continue with the off-chain code corresponding to that policy but keep in mind that it'll be currently incomplete and only cover the creation and distribution of participation tokens, the rest will follow:
<p align="right"><strong>OffChain</strong></p>
```hs
-- An 'init' transaction must forge a participation token for each participant of Hydra head.
-- Tokens are uniquely identified by a monetary policy and each participant's public keys.
--
-- We do not worry yet about other constraints which we will handle later.
init
:: (AsContractError e)
=> [PubKeyHash]
-> MonetaryPolicy
-> Contract [] Schema e ()
init participants policy = do
endpoint @"init" @()
void $ submitTxConstraintsWith lookups constraints
where
policyId =
monetaryPolicyHash policy
-- Constraints are used to specify how the Plutus application backend should construct
-- a transaction. At this stage, we only require that a participation token is forged
-- for each participant, using the given policyId derived from our Hydra monetary policy.
constraints =
foldMap (\vk ->
let participationToken = mkParticipationToken policyId vk
in mustForgeValue participationToken
) participants
-- Lookups gives some context to the Plutus application backend to construct
-- a transaction and lookup information needed by the constraints. At the moment, we
-- only really need to specify the mapping between our policy and its corresponding
-- policyId.
lookups =
mempty
{ slMPS =
Map.singleton policyId policy
}
```
Finally, we can put everything together and define the off-chain contract interface foundation as such:
```hs
type Schema = BlockchainActions
.\/ Endpoint "init" ()
contract
:: (AsContractError e)
=> MonetaryPolicy
-> [PubKeyHash]
-> Contract [] Schema e ()
contract policy participants = forever endpoints
where
endpoints =
init participants policy
```
We'll add more endpoints in the next sections, but for now, this is sufficient to invoke our off-chain code and forge participation tokens for a list of participants. Great!
> **Key takeaways**
>
> - We can piggyback on the ledger's rules and re-use UTxO as a good source of uniqueness.
> - On-chain validation must look for adversarial behaviours and make sure that a contract cannot be misused if invoked outside of the context of our off-chain code.
# The `Init` Transaction
The init transaction has three goals:
1. Forging participation tokens
2. Initializing the Hydra head state-machine
3. Reaching out to head participants.
So far, we only have forged participation tokens but we aren't sending them anywhere. Also, we aren't initializing the Hydra on-chain state-machine which is driving the head establishment. Since we'll re-use the on-chain state machine for writing other validators, let's start with that.
Remember that we want the head instance to be unique and we achieved that by defining a custom monetary policy for each head instance. We also need to restrict the participation to the head to a set of known participants, identified by some public keys. We'll therefore define some `HeadParameters` embedded in each contract:
<p align="right"><strong>OnChain</strong></p>
```hs
data HeadParameters = HeadParameters
{ participants :: [PubKeyHash]
, policyId :: MonetaryPolicyHash
}
PlutusTx.makeLift ''HeadParameters
```
It is also time to define our simplified state machine. As stated in the introduction we currently only consider three states, `Initial`, `Open` and `Final` with two transitions from `Initial` to `Open` (`CollectCom`) and from `Initial` to `Final` (`Abort`).
<p align="right"><strong>OnChain</strong></p>
```hs
data State
= Initial
| Open [TxOut]
| Final
deriving stock (Generic, Show)
deriving anyclass (ToJSON)
PlutusTx.makeLift ''State
PlutusTx.unstableMakeIsData ''State
data Transition
= CollectCom
| Abort
deriving (Generic, Show)
PlutusTx.makeLift ''Transition
PlutusTx.unstableMakeIsData ''Transition
data Hydra
instance Scripts.ScriptType Hydra where
type DatumType Hydra = State
type RedeemerType Hydra = Transition
```
And, we can already define the skeleton for the Hydra state-machine validator. We'll add transitions as we need in the sections below so for now it's pretty empty and always `False` by default.
<p align="right"><strong>OnChain</strong></p>
```hs
hydraValidator
:: HeadParameters
-> State
-> Transition
-> ScriptContext
-> Bool
hydraValidator HeadParameters{participants, policyId} s i ctx =
case (s, i) of
(_, _) ->
False
```
We want to distribute participation tokens to every head participants, and make sure that they can only be redeemed under one of the two scenarios:
- By an abort transaction, burning all participation tokens and reimbursing participants who have already committed.
- By a commit transaction effectively locking some UTxO to the head.
This is much more complex than what the ledger offers by default, hence the need for a smart-contract validator to capture this logic. We will call this initial validator using a grain of creativity: `initialValidator`. This validator will have to run ideally for each of the participants, but, we don't want the validator for Alice to also apply to Bob. Said differently, only Alice should be able to redeem her participation token to commit, and so does Bob, Carol and any other participant. Yet, we still want to allow any of them to abort as a collective event.
<p align="right"><strong>OnChain</strong></p>
```hs
data Initial
instance Scripts.ScriptType Initial where
type DatumType Initial = PubKeyHash
type RedeemerType Initial = TxOutRef
```
We will therefore parameterize the `initialValidator` with a public key, one for each participant. This will allow us to identify, within the validator, which participant is trying to redeem which participation token. Also, since we don't know upfront what UTxO are participants going to commit, we'll need to make the committed output part of the redeemer for this validator so that we can control that it is indeed spent on the Hydra contract!
Finally, since this validator needs to reference other validators of our Hydra smart contract, we will provide this information as hard-coded parameters. These parameters will indeed be embedded in the contract code itself. That is indeed necessary because there's no way for our chain code to dynamically compute the address of another contract (that would require compiling it, directly on-chain! That's a little too much to ask of a mere contract). So instead, we directly inject those addresses when compiling the contract. Thus, don't get fooled by the type-signature of the validator below, the first 3 arguments are injected into the validator during compilation and the validator itself remains a function from `DatumType a -> RedeemerType a -> ScriptContext -> Bool`!
<p align="right"><strong>OnChain</strong></p>
```hs
-- | The Validator 'initial' ensures that the input is consumed by a commit or
-- an abort transaction.
initialValidator
:: HeadParameters
-> ValidatorHash
-> ValidatorHash
-> PubKeyHash -- Datum
-> TxOutRef -- Redeemer
-> ScriptContext
-> Bool
initialValidator HeadParameters{policyId} hydraScript commitScript vk ref ctx =
consumedByCommit || consumedByAbort
where
-- A commit transaction, identified by:
-- (a) A signature that verifies as valid with verification key defined as datum
-- (b) Spending a UTxO also referenced as redeemer.
-- (c) Having the commit validator in its only output, with a valid
-- participation token for the associated key, and the total value of the
-- committed UTxO.
consumedByCommit =
case findUtxo ref ctx of
Nothing ->
False
Just utxo ->
let commitDatum = asDatum @(DatumType Commit) (snd utxo)
commitValue = txOutValue (snd utxo) <> mkParticipationToken policyId vk
in checkScriptContext @(RedeemerType Initial) @(DatumType Initial)
( mconcat
[ mustBeSignedBy vk
, mustSpendPubKeyOutput (fst utxo)
, mustPayToOtherScript commitScript commitDatum commitValue
]
)
ctx
consumedByAbort =
mustRunContract hydraScript Abort ctx
```
We've purposely left some of the helpers (e.g. `mkParticipationToken`) out of this except to keep the excerpt short(er). You can however find them in the annexes. Note that, since validators are predicate functions, we like to structure them as logical disjunctions (`||`) of cases, and then write validators for each case. The `consumedByCommit` is rather straightforward and directly ensue from the Hydra Head paper. You'll see that we extensively make use of the `TxConstraints` API from Plutus which we find quite elegant to read and compose. Let's look a bit more in details at `mustRunContract` which deserves some explanations.
The name being quite explicit, it rather speaks for itself: these constraints off-load some of the validation logic to another contract! This is a quite handy trick for re-using logic across various contracts. Note that it isn't always possible to do so since you need to be able to produce a valid redeemer. Here, however, as we will see a bit later, we can fully produce a redeemer for our Hydra state machine. The nice thing about this approach which stems from the EUTxO model tself is that we can be 100% sure about the validation logic that will be executed! How so? Well, remember that scripts are fully identified by their hashes; if one changes the code, it changes the hash. So by requiring that an output locked by a script a certain hash is spent, then necessarily, we have the guarantee that the validator code being executed is exactly the one which produced that hash in the first place, neat!
<p align="right"><strong>OnChain</strong></p>
```hs
mustRunContract
:: forall redeemer. (IsData redeemer)
=> ValidatorHash
-> redeemer
-> ScriptContext
-> Bool
mustRunContract script redeemer ctx =
case findContractInput script ctx of
Nothing ->
False
Just contractRef ->
checkScriptContext @() @()
( mconcat
[ mustSpendScriptOutput contractRef (asRedeemer redeemer)
]
) ctx
{-# INLINEABLE mustRunContract #-}
```
So here, we are really saying that it is possible to abort if and only if, we are also spending the special Hydra output that is also created by the initial transaction. If no such UTxO is present in the transaction, the validation fails. If the UTxO is spent with a different redeemer from the one we expect, it fails. If the validator code is a tiny bit different from what we expect, it fails. Of course, this relies on our Hydra validator to do the right thing! That is, it should check that all participation tokens are burnt and that the abort is executed by one of the head participants. Let's make sure it does:
<p align="right"><strong>OnChain</strong></p>
```hs
hydraValidator
[...]
case (s, i) of
(Initial, Abort) ->
and
[ mustBeSignedByOneOf participants ctx
, all (mustBurnParticipationToken ctx policyId) participants
, checkScriptContext @(RedeemerType Hydra) @(DatumType Hydra)
(mustPayToTheScript Final (lovelaceValueOf 0))
ctx
]
[...]
```
The contract instance itself is mostly boilerplate stitching things together. As mentioned earlier, we inject during compilation our head parameters and, the addresses of the other contracts necessary for the `initialValidator` to operate. Don't bother about the `hydraValidatorHash` and `commitValidatorHash` which are helpers doing exactly what their names suggest and are referenced in the annexes.
<p align="right"><strong>OnChain</strong></p>
```hs
initialScriptInstance
:: HeadParameters
-> Scripts.ScriptInstance Initial
initialScriptInstance policyId = Scripts.validator @Initial
($$(PlutusTx.compile [|| initialValidator ||])
`PlutusTx.applyCode` PlutusTx.liftCode policyId
`PlutusTx.applyCode` PlutusTx.liftCode (hydraValidatorHash policyId)
`PlutusTx.applyCode` PlutusTx.liftCode (commitValidatorHash policyId)
)
$$(PlutusTx.compile [|| wrap ||])
where
wrap = Scripts.wrapValidator @(DatumType Initial) @(RedeemerType Initial)
```
Now it's time to extend the `init` endpoint we've previously written, to make sure it is indeed distributing participation to participants, locking them with the `initialValidator` and also initialize the Hydra Head state-machine to its initial state. The main difference comes from the transaction constraints, which we wrote as:
<p align="right"><strong>OffChain</strong></p>
```hs
constraints =
foldMap (\vk ->
let participationToken = mkParticipationToken policyId vk
in mustForgeValue participationToken
) participants
```
and now become:
<p align="right"><strong>OffChain</strong></p>
```hs
constraints = mconcat
-- For each participant, we create two constraints on the transaction:
-- (a) We forge a participation token for this participant
-- (b) We send this token to a script address, locked by an `initialValidator` contract.
-- This validator ensures that the token can only be redeemed by a commit transaction
-- or a general abort of the head.
--
-- And finally, we also initialize the head by creating an artifical output locked by the
-- Hydra state-machine contract.
[ foldMap (\vk ->
let participationToken = mkParticipationToken policyId vk
in mconcat
[ mustForgeValue participationToken
, mustPayToOtherScript
(Scripts.scriptHash $ initialScriptInstance params)
(asDatum @(DatumType OnChain.Initial) vk)
participationToken
]
) participants
, mustPayToTheScript OnChain.Initial (lovelaceValueOf 0)
]
```
> **Key takeaways**
>
> - The EUTxO model is well-suited for state-machines, using datum as states and redeemers as transitions.
> - Validators can be structured as conjunction of cases to make it easier to reason about them.
> - Contracts can be parameterized at compilation-time; the configuration is effectively embedded into the contract. A different configuration yields a different contract address.
> - Existing contracts for which we can construct redeemers can be re-used for validation.
# The `Commit` Transaction(s)
We are now able to initialize a head and distribute participation tokens. Great. The big piece is yet to come so stay well sit. In the `initialValidator`, we have allowed assets to be redeemed if the transaction was also consuming an extra UTxO passed as redeemer and locking the assets (plus the participation token) to a commit validator. The role of this commit validator is to ensure that funds can only be redeemed by a collectCom transaction such that, all commits are collected at the same time.
This is an interesting validator to write because, to be valid, it expects not only the UTxO it locks to be present but also all other commits! Writing such a validator wouldn't be possible if the validator was only able to operate on the UTxO it locks. Fortunately, validators are given the entire transaction as context and they can therefore inspect other inputs of the transaction!
If you were careful when we wrote the `initialValidator`, you might have noticed that we've given a specific datum to the `commitValidator`. This is necessary because, after a UTxO has been committed, it no longer exists. Besides, UTxOs are indeed committed when the `initialValidator` is executed (this can be hard to wrap your head around, but keep in mind that validator is executed when _spending_ a UTxO, not when _sending_ to an address). So naturally, we end up with the following validator signature:
<p align="right"><strong>OnChain</strong></p>
```hs
data Commit
instance Scripts.ScriptType Commit where
type DatumType Commit = TxOut
type RedeemerType Commit = ()
```
There's indeed no redeemer needed to run the `commitValidator` because, the validity of the collectCom transaction can fully be inferred from the transaction itself. The `commitValidator` itself is made pretty simple by re-using our trick from before:
<p align="right"><strong>OnChain</strong></p>
```hs
commitValidator
:: ValidatorHash
-> TxOut -- Datum
-> () -- Redeemer
-> ScriptContext
-> Bool
commitValidator hydraScript committedOut () ctx =
consumedByCollectCom || consumedByAbort
where
consumedByCollectCom =
mustRunContract hydraScript CollectCom ctx
consumedByAbort = and
[ mustRunContract hydraScript Abort ctx
, mustReimburse committedOut ctx
]
```
Here we have a similar structure as for the `initialValidator`. A UTxO locked by a commit script can be consumed either by an abort, or a collectCom. Unlike `initialValidator` there is however an additional constraint for the abort case: it must also reimburse the commit to the head participant! For the collectCom validation, We've decided to keep the logic inside the Hydra state-machine contract itself for we find it less confusing that way. Since the collectCom inspect the entire transaction and perform a sort of horizontal validation across all commits, it's more easily thought of in the context of the Hydra contract.
Finally, to close the loop, we must write the corresponding off-chain code that is constructing a commit transaction. This is, unfortunately, a bit more involved than the validation so let's break it down, starting with the outer skeleton:
<p align="right"><strong>OffChain</strong></p>
```hs
commit
:: (AsContractError e)
=> HeadParameters
-> Contract [] Schema e ()
commit params@HeadParameters{policy, policyId} = do
(vk, toCommit) <- endpoint @"commit" @(PubKeyHash, (TxOutRef, TxOutTx))
initial <- utxoAtWithDatum
(Scripts.scriptAddress $ initialScriptInstance params)
(asDatum @(DatumType OnChain.Initial) vk)
void $ submitTxConstraintsWith
(lookups vk toCommit initial)
(constraints vk toCommit initial)
```
Notice that this time, the endpoint has some arguments! Indeed, since we expect this endpoint to work for any of the participants, it must know who is committing, and what is being committed. Note that, since the initial script input can be inferred from the participant verification key, we can find the UTxO that is carrying around the participation token of that participant pretty easily (the function `utxoAtWithDatum` is given in annexes). Then, a commit is made of a single transaction with some specific constraints:
<p align="right"><strong>OffChain</strong></p>
```hs
constraints vk (ref, txOut) initial =
let amount = txOutValue (txOutTxOut txOut) <> OnChain.mkParticipationToken policyId vk
in mconcat
[ -- A commit must originate from a head participant
mustBeSignedBy vk
, -- NOTE: using a 'foldMap' here but that 'initial' utxo really has only one
-- element! This effectively consumes an the initial UTxO sent to that participant
-- including its participation token.
foldMap
(`mustSpendScriptOutput` asRedeemer @(RedeemerType OnChain.Initial) ref)
(Map.keys initial)
-- What follows really is, the commit. Not only should the ref UTxO be spent,
-- but it should lock the amount to a commit validator / contract.
, mustSpendPubKeyOutput ref
, mustPayToOtherScript
(Scripts.scriptHash $ commitScriptInstance params)
(asDatum @(DatumType OnChain.Commit) (txOutTxOut txOut))
amount
]
```
Here, we are in fact re-stating what we expect of the validations (which is great in that direction! Doing it the other way around is a recipe for disaster). One thing you may have noticed from the beginning, I tend to write most of the datums and redeemers using explicit type annotations. For example:
```hs
asRedeemer @(RedeemerType OnChain.Initial)
```
This is because most functions working with datums and redeemers in the Plutus framework are agnostic to contracts and must therefore work for any contract. As a consequence, the interface is very much opaque/dynamic and we can't have the compiler backing us. So, instead of implicit type conversions, we chose to force a particular type using a type annotation to make sure that, if we get it wrong, the compiler will yell at me. Note also that we do not use any concrete data type (e.g. `asDatum @TxOutRef`) because this will go completely unnoticed in case of refactoring. The code would most likely compile, only to lead to very puzzling and mind-boggling errors later on. So, save yourself some debugging time and avoid implicit type coercion!
> **Key takeaways**
>
> - Datums can be used to record some previous state, useful to construct states of next validators.
> - Avoid implicit type coercion and leverage the data-families `DatumType` and `RedeemerType` to be explicit when dealing with datums and redeemers.
# The `CollectCom` Transaction
Now, the fun part. So far, we haven't opened a head but rather, prepared the ground for it. The collectCom transaction is effectively the one gathering all commits and putting the Hydra state-machine in the `Open` state from which transactions can now flow off-chain through payment channels. As always, let's start with the on-chain code.
The collectCom transaction must ensures that:
- It is made by a head participant.
- It collects all the commits from all participants.
- It moves the Hydra state-machine into the `Open` state.
For the second goal we can leverage once more participation tokens. Since the `hydraValidator` is parameterized by the participants, the validator knows exactly how many tokens it expects and even, what are the tokens it expects (since the `assetName` of each token is fully determined by the participant verification keys).
<p align="right"><strong>OnChain</strong></p>
```hs
hydraValidator HeadParameters{participants, policyId} s i ctx =
[...]
case (s, i) of
(Initial, CollectCom) ->
-- We use participation tokens here to identify which outputs are
-- effectively the commits to collect. We have to keep in mind that
-- the transaction may also contain other inputs, for instance, to pay
-- for transaction fees or because, several contracts are grouped
-- within one transaction; it'd be wrong to assume that all inputs are
-- commits.
let collectComUtxos =
snd <$> filterInputs (hasParticipationToken policyId) ctx
-- Committed outputs are referenced as the datum of each commit validator. So we need
-- to effectively look them up and, there are in principle no guarantees that they
-- are even TxOutRef.
committedOutputs =
mapMaybe decodeCommit collectComUtxos
newState =
Open committedOutputs
amountPaid =
foldMap txOutValue collectComUtxos
in and
[ mustBeSignedByOneOf participants ctx
, all (mustForwardParticipationToken ctx policyId) participants
, checkScriptContext @(RedeemerType Hydra) @(DatumType Hydra)
(mustPayToTheScript newState amountPaid)
ctx
]
[...]
where
decodeCommit = txOutDatumHash
>=> (`findDatum` scriptContextTxInfo ctx)
>=> (fromData @(DatumType Commit) . getDatum)
```
Note that, a lot of the security guarantees stem from the participation tokens which are passed from contracts to contracts. Since the monetary policy guarantees that **these tokens can only be forged once** and in a very specific context, they protect from many adversarial behaviours and comes in handy to assess that we necessarily went through a very particular contract flow. Said differently, if we can get all participation tokens at this stage, then the contract must have gone through the other init and commit steps.
Like the `commit` endpoint, the `collectCom` endpoint is a bit more involved than the validator. Indeed, in order to collect all commits, we must first gather all UTxO which are locked by a commit validator and in principle, the UTxO that were committed (but no longer exists on the blockchain!).
Although _it would be possible_ to figure out which outputs were committed, with respect to the **single-responsibility principle**, We've kept this concern out of this endpoint and expect the caller to pass the UTxO that were committed/locked. This implicitly assumes that the caller is watching the chain by some mean, which could be for instance via the Plutus-Application-Backend using another endpoint specifically crafted for that purpose.
Note that it also makes testing this endpoint easier since one can now pass any UTxO to collect and observe the behavior of the validator.
<p align="right"><strong>OffChain</strong></p>
```hs
collectCom
:: (AsContractError e)
=> HeadParameters
-> Contract [] Schema e ()
collectCom params@HeadParameters{participants, policy, policyId} = do
(headMember, lockedOutputs) <- endpoint @"collectCom" @(PubKeyHash, [TxOut])
commits <- utxoAt (Scripts.scriptAddress $ commitScriptInstance params)
stateMachine <- utxoAt (Scripts.scriptAddress $ hydraScriptInstance params)
tx <- submitTxConstraintsWith @OnChain.Hydra
(lookups commits stateMachine headMember)
(constraints commits stateMachine headMember lockedOutputs)
awaitTxConfirmed (txId tx)
```
The collectCom endpoint then consists of a single transaction satisfying the following constraints:
<p align="right"><strong>OffChain</strong></p>
```hs
constraints commits stateMachine headMember lockedOutputs =
let amount = foldMap (txOutValue . snd) $ flattenUtxo (commits <> stateMachine)
in mconcat
[ -- A collectCom must originate from a head participant
mustBeSignedBy headMember
-- Must move the Hydra state-machine from Init to Open, recording all the
-- locked outputs in the contract's state.
, foldMap
(\ref -> mustSpendScriptOutput ref $ asRedeemer @(RedeemerType OnChain.Hydra) OnChain.CollectCom)
(Map.keys stateMachine)
, mustPayToTheScript (OnChain.Open $ reverse lockedOutputs) amount
-- Make sure that every UTxO locked by a commit validator is spent. Note that we
-- must explicitly include commits datum here to make sure they can be used in the
-- Hydra validator.
, foldMap
(\(_, ref) -> mustSpendScriptOutput ref $ asRedeemer @(RedeemerType OnChain.Commit) ())
(zipOnParticipationToken policyId participants commits)
, foldMap
(mustIncludeDatum . asDatum @(DatumType OnChain.Commit))
lockedOutputs
]
```
Re-using other validators for validation may be a bit confusing at first. In particular, it requires a bit more of manual plumbing when constructing transactions. For example here, since the collectCom involves in fact two validators: `commitValidator` and `hydraValidator`, we must explicitly specify datums for the `commitValidator` (because our endpoint is written from the perspective of the `hydraValidator`). When using a single validator this is mostly handled by the Plutus framework.
> **Key takeaways**
>
> - While we can theoretically serialize data in on-chain contracts directly, it is often cumbersome. Rather, datums can be used for passing data to serialize.
> - Endpoints aren't necessarily tight to a single contract / validator, but using several requires more manual plumbing.
> - Using tokens to keep track of a contract flow is handy and reliable (provided that the policy is so itself).
> - Keep endpoints focused on one single responsibility. Create more endpoints for different use-cases.
# Conclusion
To be written.
# Annexes
### `ValidatorHash` Helpers
```hs
commitValidatorHash :: HeadParameters -> ValidatorHash
commitValidatorHash = Scripts.scriptHash . commitScriptInstance
{-# INLINEABLE commitValidatorHash #-}
hydraValidatorHash :: HeadParameters -> ValidatorHash
hydraValidatorHash = Scripts.scriptHash . hydraScriptInstance
{-# INLINEABLE hydraValidatorHash #-}
```
### Participation token helpers
```hs
mkParticipationToken
:: MonetaryPolicyHash
-> PubKeyHash
-> Value
mkParticipationToken policyId vk =
Value.singleton (Value.mpsSymbol policyId) (mkParticipationTokenName vk) 1
{-# INLINEABLE mkParticipationToken #-}
mkParticipationTokenName
:: PubKeyHash
-> TokenName
mkParticipationTokenName =
TokenName . getPubKeyHash
{-# INLINEABLE mkParticipationTokenName #-}
hasParticipationToken
:: MonetaryPolicyHash
-> TxInInfo
-> Bool
hasParticipationToken policyId input =
let currency = Value.mpsSymbol policyId
in currency `elem` symbols (txOutValue $ txInInfoResolved input)
{-# INLINEABLE hasParticipationToken #-}
mustBurnParticipationToken
:: ScriptContext
-> MonetaryPolicyHash
-> PubKeyHash
-> Bool
mustBurnParticipationToken ctx policyId vk =
let assetName = mkParticipationTokenName vk
in checkScriptContext @() @() (mustForgeCurrency policyId assetName (-1)) ctx
{-# INLINEABLE mustBurnParticipationToken #-}
```
### Miscellaneous On-Chain helpers
```hs
mustBeSignedByOneOf
:: [PubKeyHash]
-> ScriptContext
-> Bool
mustBeSignedByOneOf vks ctx =
or ((`checkScriptContext` ctx) . mustBeSignedBy @() @() <$> vks)
{-# INLINEABLE mustBeSignedByOneOf #-}
mustReimburse
:: TxOut
-> ScriptContext
-> Bool
mustReimburse txOut =
elem txOut . txInfoOutputs . scriptContextTxInfo
{-# INLINEABLE mustReimburse #-}
```
### Miscellaneous Off-Chain helpers
```hs
utxoAtWithDatum
:: forall w s e. (AsContractError e, HasUtxoAt s)
=> Address
-> Datum
-> Contract w s e UtxoMap
utxoAtWithDatum addr datum = do
utxo <- utxoAt addr
pure $ Map.filter matchDatum utxo
where
matchDatum txOut =
txOutDatumHash (txOutTxOut txOut) == Just (datumHash datum)
```
[Hydra: Fast Isomorphic State Channels]: TODO