Skip to main content

Open a head on testnet

This tutorial will show you how to use hydra-node on the preprod Cardano network to open a layer-two state channel between two actors using the Hydra Head protocol. We will also use Mithril to bootstrap our nodes for a speedy setup.

This setup is also known as the Basic Hydra Head topology and we will be creating the "green" Hydra Head between X and Y as shown below:

What you will need

  • Terminal access to a machine that can connect to and can be reached from the internet.
  • Either
    • someone else following this tutorial as well to connect to (recommended), or
    • two such machines (or you can run it on one machine).
  • 100 tADA in a wallet on preprod (per participant)

Step 0: Installation

Required tools, this tutorial assumes to be available on your system:

We will start with downloading pre-built binaries of the involved software components of the Cardano ecosystem, putting them in a bin/ directory:

mkdir -p bin
curl -L -O${version}/hydra-x86_64-linux-${version}.zip
unzip -d bin hydra-x86_64-linux-${version}.zip
curl -L -o - \
| tar xz ./bin/cardano-node ./bin/cardano-cli
curl -L -o - \
| tar xz -C bin mithril-client
chmod +x bin/*

We also need to define various environment variables that will simplify our commands. Make sure each terminal you'll be opening to run those commands has those environment variables defined.

export PATH=$(pwd)/bin:$PATH
export GENESIS_VERIFICATION_KEY=$(curl 2> /dev/null)
export CARDANO_NODE_SOCKET_PATH=$(pwd)/node.socket
Other installation options

There are other ways to acquire and run the Cardano, Mithril, and Hydra nodes which might be better suited depending on your environment:

  • Docker containers are published regularly,
  • Some projects provide system-level packages for installation and/or pre-built binaries for various platforms,
  • Building from source is always an option.

Please check-out each project's GitHub pages for more options.

Step 1: Connect to Cardano

The Hydra Head protocol a connection to the Cardano layer one network to verify and post protocol transactions in a trustless way. Hence, the first step is to set up a cardano-node on a public testnet. Using Mithril, we can skip synchronizing the whole history and get started quickly.

We will be using the mithril-client configured to download from preprod network to download the latest blockchain snapshot:

mithril-client cardano-db download latest
NixOS workaround

The dynamically linked mithril-client binary does not work out-of-the-box on NixOS. You can workaround this by emulating a common linux FHS environment:

alias mithril-client="steam-run mithril-client"

If you have a better solution or want to contribute static binaries to the mithril CI, PRs are welcome!

Then we can run a cardano-node, first downloading some configuration files, with:

curl -O
curl -O
curl -O
curl -O
curl -O
curl -O

cardano-node run \
--config config.json \
--topology topology.json \
--socket-path ./node.socket \
--database-path db

To interact with the cardano-node we will be using the cardano-cli with cardano-cli we can now check the synchronization status. You will need to open another terminal window as running the cardano-node in the foreground prevents you from running other commands:

cardano-cli query tip

This should show something like:

"block": 1275938,
"epoch": 88,
"era": "Babbage",
"hash": "7d22ae918f3ffd35e18c5a7859af27dbcbd29fe08f274b76c284c00042044a2e",
"slot": 36501000,
"slotInEpoch": 126600,
"slotsToEpochEnd": 305400,
"syncProgress": "100.00"
Bash auto-completion

If you are using bash, you can get auto-completion of cardano-cli using:

source <(cardano-cli --bash-completion-script cardano-cli)

Detailed steps on bootstrapping a cardano-node using Mithril with more explanations can be found here

Step 2: Prepare keys and funding

As introduced before, the tutorial considers a minimal setup of two participants that together want to open a Hydra head. We will call them alice and bob going forward. Depending on whether you do this tutorial with a friend or alone, decide who is who or execute the commands on your two distinct setups.

With the cardano-cli we first generate Cardano key pairs and addresses to identify the hydra-node and hold funds on the layer one:

mkdir -p credentials

cardano-cli address key-gen \
--verification-key-file credentials/alice-node.vk \
--signing-key-file credentials/

cardano-cli address build \
--verification-key-file credentials/alice-node.vk \
--out-file credentials/alice-node.addr

cardano-cli address key-gen \
--verification-key-file credentials/alice-funds.vk \
--signing-key-file credentials/

cardano-cli address build \
--verification-key-file credentials/alice-funds.vk \
--out-file credentials/alice-funds.addr

Next we need to send some funds to the node and funding keys. If you have a wallet on preprod, you can send some tADA directly to these addresses shown after executing:

echo "Send at least 30 tADA to alice-node:"
echo $(cat credentials/alice-node.addr)"\n"

echo "Send any amount of tADA or assets to alice-funds:"
echo $(cat credentials/alice-funds.addr)"\n"
Where to get funds

In case you have no tADA on preprod, you can use the Testnet Faucet to seed your wallet or the addresses above. Note that due to rate limiting, it's better to request a large sums for a single address and then dispatch to other addresses.

You can check the balance of your addresses via:

echo "# UTxO of alice-node"
cardano-cli query utxo --address $(cat credentials/alice-node.addr) --out-file /dev/stdout | jq

echo "# UTxO of alice-funds"
cardano-cli query utxo --address $(cat credentials/alice-funds.addr) --out-file /dev/stdout | jq

Besides the Cardano keys, we now also need to generate Hydra key pairs which will be used on the layer two by the hydra-node. For this, we will use the hydra-tools to generate the keys for alice and/or bob respectively:

hydra-node gen-hydra-key --output-file credentials/alice-hydra

If you are doing this tutorial with a friend, now is the time to exchange the verification (public) keys: {alice,bob}-node.vk and {alice,bob}-hydra.vk. You can use any authenticated communication channel for this where you can be sure your peer cannot be easily impersonated.

Besides keys, we also want to communicate each other's connectivity information. That is, an IP address / hostname + port where we will be reachable for our layer two network using hydra-node. For the purpose of this tutorial we are assuming an IP address and port for alice and bob which works on a single machine, but please replace usages below with your respective addresses:



We still need one thing, before we can spin up the hydra-node, that is the protocol parameters that the ledger in our Hydra head will use. We can use the same parameters as on the Cardano layer one, but we tweak them for this tutorial such that there are no fees! This will fetch the parameters and sets fees + prices to zero:

cardano-cli query protocol-parameters \
| jq '.txFeeFixed = 0 |.txFeePerByte = 0 | .executionUnitPrices.priceMemory = 0 | .executionUnitPrices.priceSteps = 0' \
> protocol-parameters.json

In summary, the Hydra head participants exchanged and agreed on:

  • IP addresses + port on which their hydra-node will run.
  • A Hydra verification key to identify them in the head.
  • A Cardano verification key to identify them on the blockchain.
  • The protocol parameters that they want to use in the Hydra head.
  • A contestation period for the head closing (we will use the default here).

Step 3: Start the Hydra node

With all these parameters defined, we now pick a HYDRA_VERSION of the Head protocol we want to use. This is defined by the hydra-node --HYDRA_VERSION itself and the --hydra-scripts-tx-id which point to scripts published on-chain.

For all released HYDRA_VERSIONs of the hydra-node and common Cardano networks, the scripts do get pre-published and we can just use them. See the user manual for more information how to publish scripts yourself.

Let's start the hydra-node with all these parameters now:

hydra-node \
--node-id "alice-node" \
--persistence-dir persistence-alice \
--cardano-signing-key credentials/ \
--hydra-signing-key credentials/ \
--hydra-scripts-tx-id $(curl | jq -r ".preprod.\"${version}\"") \
--ledger-protocol-parameters protocol-parameters.json \
--testnet-magic 1 \
--node-socket node.socket \
--api-port 4001 \
--host \
--api-host \
--port 5001 \
--peer \
--hydra-verification-key credentials/bob-hydra.vk \
--cardano-verification-key credentials/bob-node.vk

And we can check whether it is running by opening a Websocket connection to the API port:

websocat ws:// | jq

This opens a duplex connection and we should see something like:

"peer": "bob-node",
"seq": 0,
"tag": "PeerConnected",
"timestamp": "2023-08-17T18:25:02.903974459Z"
"headStatus": "Idle",
"hydraNodeVersion": "0.12.0-54db2265c257c755df98773c64754c9854d879e8",
"me": {
"vkey": "ab159b29b87b498fa060f6045cccf84ecd20cf623f7820ed130ffc849633a120"
"seq": 1,
"tag": "Greetings",
"timestamp": "2023-08-17T18:32:29.092329511Z"

Before continuing, make sure that you see a PeerConnected message for each of the configured other hydra-node. If this is not showing up, double-check network configuration and connectivity.

Step 4: Open a Hydra head

Using the jq enhanced websocat session, we can now communicate with the hydra-node through its Websocket API on the terminal. This is a duplex connection and we can just insert commands directly.

Send this command to initialize a head through the Websocket connection:

Websocket API
{ "tag": "Init" }

Depending on the network connection, this might take a bit as the node does submit a transaction on-chain. Eventually, both Hydra nodes and connected clients should see HeadIsInitializing with a list of parties that need to commit now.

Committing funds to the head means that we pick which UTxO we want to have available on the layer two. We use the HTTP API of hydra-node to commit all funds given to {alice,bob}-funds.vk beforehand:

cardano-cli query utxo \
--address $(cat credentials/alice-funds.addr) \
--out-file alice-commit-utxo.json

curl -X POST \
--data @alice-commit-utxo.json \
> alice-commit-tx.json

cardano-cli transaction sign \
--tx-file alice-commit-tx.json \
--signing-key-file credentials/ \
--out-file alice-commit-tx-signed.json

cardano-cli transaction submit --tx-file alice-commit-tx-signed.json
Alternative: Not commit anything

If you don't want to commit any funds, for example only receive things on the layer two, you can just request an empty commit transaction like this (example for bob):

curl -X POST --data "{}" > bob-commit-tx.json
cardano-cli transaction submit --tx-file bob-commit-tx.json

This does find all UTxO owned by the funds key, request a commit transaction draft from the hydra-node, sign it with the funds key and submit the transaction to the Cardano layer one.

Once this transaction was seen by the hydra-node, you should see a Committed message on the Websocket connection.

When both parties, alice and bob, have committed, the head will automatically open and you will see a HeadIsOpen on the Websocket session. This message also includes the starting balance utxo. Notice that the entries correspond exactly the ones which were committed to the Head (even the Tx hash and index are the same). The head is now open and ready to be used!

Step 5: Using the Hydra head

We want to make a basic transaction between alice and bob. Since Hydra Head is an isomorphic protocol, all things that work on the layer one also work in the head. This means that constructing transactions is no different than on Cardano. This is great since it allows us to use already existing tools like the cardano-cli or frameworks to create transactions!

In this example, we will send 10₳ from alice to bob, hence you may need to change the values depending on what you (and your partner) committed to the head.

First, we need to select a UTxO to spend. We can do this either by looking at the utxo field of the last HeadIsOpen or SnapshotConfirmed message, or query the API for the current UTxO set:

curl -s | jq

From the response, we would need to select a UTxO that is owned by alice to spend:

curl -s \
| jq "with_entries(select(.value.address == \"$(cat credentials/alice-funds.addr)\"))" \
> utxo.json

Then, just like on the Cardano layer one, we can construct a transaction via the cardano-cli that spends this UTxO and send it to an address. If you have not yet, enquire the address of your partner to send something to (here credentials/bob-funds.addr which alice would not have automatically):

cardano-cli transaction build-raw \
--tx-in $(jq -r 'to_entries[0].key' < utxo.json) \
--tx-out $(cat credentials/bob-funds.addr)+${LOVELACE} \
--tx-out $(cat credentials/alice-funds.addr)+$(jq "to_entries[0].value.value.lovelace - ${LOVELACE}" < utxo.json) \
--fee 0 \
--out-file tx.json

Note that we need to use the build-raw version, since the client cannot (yet?) index the Hydra head directly and would not find the UTxO to spend. This means we need to also create a change output with the right amount. Also, because we have set the protocol parameters of the head to have zero fees, we can use the --fee 0 option.

Before submission, we need to sign the transaction to authorize spending alice's funds:

cardano-cli transaction sign \
--tx-body-file tx.json \
--signing-key-file credentials/ \
--out-file tx-signed.json

To submit the transaction we can use our websocket session again. This command will print the NewTx command to copy paste into an already open websocket connection:

cat tx-signed.json | jq -c '{tag: "NewTx", transaction: .}'

The transation will be validated by both hydra-nodes and either result in a TxInvalid message with a reason, or a TxValid message and a SnapshotConfirmed with the new UTxO available in the head shortly after.

🎉 Congratulations, you just processed your first Cardano transaction off-chain in a Hydra head!

At this stage you can continue experimenting with constructing & submitting transactions to the head as you wish. Proceed in the tutorial once you're done and want to realize the exchanged funds from the Hydra head back to the Cardano layer one.

Step 6: Closing the Hydra head

Each participant of the head can close it at any point in time. To do this, we can use the websocket API and submit this command:

Websocket API
{ "tag": "Close" }

This will have the hydra-node submit a protocol transaction to the Cardano network with the last known snapshot. A smart contract on the layer one will check the snapshot signatures and confirm the head closed. When this close transaction is observed, the websocket API sends a HeadIsClosed message (this can also happen if any other hydra-node closes the head).

Known bug

You might need to submit the Close command multiple times if the head is not getting closed within ~30s.

See #1039 for details.

Included in the message will be a contestationDeadline which gets set using the configurable --contestation-period. Until this deadline, the closing snapshot can be contested with a more recent, multi-signed snapshot. Your hydra-node would contest automatically for you if the closed snapshot is not the last known one.

We need to wait now until the deadline has passed, which will be notified by the hydra-node through the websocket API with a ReadyToFanout message.

At this point any head member can issue distribution of funds on the layer one. You can do this through the websocket API one last time:

Websocket API
{ "tag": "Fanout" }

This will again submit a transaction to the layer one and once successful is indicated by a HeadIsFinalized message which includes the distributed utxo.

To confirm, you can query the funds of both, alice and bob, on the layer one:

echo "# UTxO of alice"
cardano-cli query utxo --address $(cat credentials/alice-funds.addr) --out-file /dev/stdout | jq

echo "# UTxO of bob"
cardano-cli query utxo --address $(cat credentials/bob-funds.addr) --out-file /dev/stdout | jq

That's it. That's the full life-cycle of a Hydra head.

Bonus: Be a good citizen

As we have taken our funds from the testnet faucet and we do not need them anymore, we can return all the remaining tADA of alice and bob back to the faucet (before we throw away the keys):

cardano-cli query utxo \
--address $(cat credentials/alice-node.addr) \
--address $(cat credentials/alice-funds.addr) \
--out-file alice-return-utxo.json

cardano-cli transaction build \
$(cat alice-return-utxo.json | jq -j 'to_entries[].key | "--tx-in ", ., " "') \
--change-address addr_test1qqr585tvlc7ylnqvz8pyqwauzrdu0mxag3m7q56grgmgu7sxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknswgndm3 \
--out-file alice-return-tx.json

cardano-cli transaction sign \
--tx-file alice-return-tx.json \
--signing-key-file credentials/ \
--signing-key-file credentials/ \
--out-file alice-return-tx-signed.json

cardano-cli transaction submit --tx-file alice-return-tx-signed.json