Tutorial
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:
- Linux x86-64
- Mac OS aarch64
mkdir -p bin
curl -L -O https://github.com/input-output-hk/hydra/releases/download/0.12.0/hydra-x86_64-unknown-linux-musl.zip
unzip -d bin hydra-x86_64-unknown-linux-musl.zip
curl -L -o - https://github.com/input-output-hk/cardano-node/releases/download/8.1.2/cardano-node-8.1.2-linux.tar.gz \
| tar xz -C bin ./cardano-node ./cardano-cli
curl -L -o - https://github.com/input-output-hk/mithril/releases/download/2331.1/mithril-2331.1-linux-x64.tar.gz \
| tar xz -C bin mithril-client
chmod +x bin/*
mkdir -p bin
curl -L -o - https://github.com/input-output-hk/hydra/releases/download/0.12.0/tutorial-binaries-aarch64-darwin.tar.gz \
| tar xz -C 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.
- Linux x86-64
- Mac OS aarch64
export PATH=$(pwd)/bin:$PATH
export GENESIS_VERIFICATION_KEY=$(curl https://raw.githubusercontent.com/input-output-hk/mithril/main/mithril-infra/configuration/release-preprod/genesis.vkey 2> /dev/null)
export AGGREGATOR_ENDPOINT=https://aggregator.release-preprod.api.mithril.network/aggregator
export CARDANO_NODE_SOCKET_PATH=$(pwd)/node.socket
export CARDANO_NODE_NETWORK_ID=1
export PATH=$(pwd)/bin:$PATH
export GENESIS_VERIFICATION_KEY=$(curl https://raw.githubusercontent.com/input-output-hk/mithril/main/mithril-infra/configuration/release-preprod/genesis.vkey 2> /dev/null)
export AGGREGATOR_ENDPOINT=https://aggregator.release-preprod.api.mithril.network/aggregator
export CARDANO_NODE_SOCKET_PATH=$(pwd)/node.socket
export CARDANO_NODE_NETWORK_ID=1
export DYLD_FALLBACK_LIBRARY_PATH=$(pwd)/bin
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:
SNAPSHOT_DIGEST=$(mithril-client snapshot list --json | jq -r '.[0].digest')
mithril-client snapshot download $SNAPSHOT_DIGEST
Then we can run a cardano-node
, first downloading some configuration files, with:
curl -O https://book.world.dev.cardano.org/environments/preprod/config.json
curl -O https://book.world.dev.cardano.org/environments/preprod/topology.json
curl -O https://book.world.dev.cardano.org/environments/preprod/byron-genesis.json
curl -O https://book.world.dev.cardano.org/environments/preprod/shelley-genesis.json
curl -O https://book.world.dev.cardano.org/environments/preprod/alonzo-genesis.json
curl -O https://book.world.dev.cardano.org/environments/preprod/conway-genesis.json
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:
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:
- Alice
- Bob
mkdir -p credentials
cardano-cli address key-gen \
--verification-key-file credentials/alice-node.vk \
--signing-key-file credentials/alice-node.sk
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/alice-funds.sk
cardano-cli address build \
--verification-key-file credentials/alice-funds.vk \
--out-file credentials/alice-funds.addr
mkdir -p credentials
cardano-cli address key-gen \
--verification-key-file credentials/bob-node.vk \
--signing-key-file credentials/bob-node.sk
cardano-cli address build \
--verification-key-file credentials/bob-node.vk \
--out-file credentials/bob-node.addr
cardano-cli address key-gen \
--verification-key-file credentials/bob-funds.vk \
--signing-key-file credentials/bob-funds.sk
cardano-cli address build \
--verification-key-file credentials/bob-funds.vk \
--out-file credentials/bob-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:
- Alice
- Bob
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"
echo "Send at least 30 tADA to bob-node:"
echo $(cat credentials/bob-node.addr)"\n"
echo "Send any amount of tADA or assets to bob-funds:"
echo $(cat credentials/bob-funds.addr)"\n"
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:
- Alice
- Bob
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
echo "# UTxO of bob-node"
cardano-cli query utxo --address $(cat credentials/bob-node.addr) --out-file /dev/stdout | jq
echo "# UTxO of bob-funds"
cardano-cli query utxo --address $(cat credentials/bob-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:
- Alice
- Bob
hydra-tools gen-hydra-key --output-file credentials/alice-hydra
hydra-tools gen-hydra-key --output-file credentials/bob-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:
Alice: 127.0.0.1:5001
Bob: 127.0.0.1:5001
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 version of the Head protocol we
want to use. This is defined by the hydra-node --version
itself and the
--hydra-scripts-tx-id
which point to scripts published on-chain.
For all released 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:
- Alice
- Bob
hydra-node \
--node-id "alice-node" \
--persistence-dir persistence-alice \
--cardano-signing-key credentials/alice-node.sk \
--hydra-signing-key credentials/alice-hydra.sk \
--hydra-scripts-tx-id e5eb53b913e274e4003692d7302f22355af43f839f7aa73cb5eb53510f564496 \
--ledger-protocol-parameters protocol-parameters.json \
--testnet-magic 1 \
--node-socket node.socket \
--api-port 4001 \
--host 0.0.0.0 \
--port 5001 \
--peer 127.0.0.1:5002 \
--hydra-verification-key credentials/bob-hydra.vk \
--cardano-verification-key credentials/bob-node.vk
hydra-node \
--node-id "bob-node" \
--persistence-dir persistence-bob \
--cardano-signing-key credentials/bob-node.sk \
--hydra-signing-key credentials/bob-hydra.sk \
--hydra-scripts-tx-id e5eb53b913e274e4003692d7302f22355af43f839f7aa73cb5eb53510f564496 \
--ledger-protocol-parameters protocol-parameters.json \
--testnet-magic 1 \
--node-socket node.socket \
--api-port 4002 \
--host 0.0.0.0 \
--port 5002 \
--peer 127.0.0.1:5001 \
--hydra-verification-key credentials/alice-hydra.vk \
--cardano-verification-key credentials/alice-node.vk
And we can check whether it is running by opening a Websocket connection to the API port:
- Alice
- Bob
websocat ws://127.0.0.1:4001 | jq
websocat ws://127.0.0.1:4002 | 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:
{ "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:
- Alice
- Bob
cardano-cli query utxo \
--address $(cat credentials/alice-funds.addr) \
--out-file alice-commit-utxo.json
curl -X POST 127.0.0.1:4001/commit \
--data @alice-commit-utxo.json \
> alice-commit-tx.json
cardano-cli transaction sign \
--tx-file alice-commit-tx.json \
--signing-key-file credentials/alice-funds.sk \
--out-file alice-commit-tx-signed.json
cardano-cli transaction submit --tx-file alice-commit-tx-signed.json
cardano-cli query utxo \
--address $(cat credentials/bob-funds.addr) \
--out-file bob-commit-utxo.json
curl -X POST 127.0.0.1:4002/commit \
--data @bob-commit-utxo.json \
> bob-commit-tx.json
cardano-cli transaction sign \
--tx-file bob-commit-tx.json \
--signing-key-file credentials/bob-funds.sk \
--out-file bob-commit-tx-signed.json
cardano-cli transaction submit --tx-file bob-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 127.0.0.1:4002/commit --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 through the websocket session:
{ "tag": "GetUTxO" }
From the response, we would need to select a UTxO that is owned by alice
to
spend. We can do that also via the snapshotUtxo
field in the Greetings
message and using this websocat
and jq
invocation:
websocat -U "ws://0.0.0.0:4001?history=no" \
| jq "select(.tag == \"Greetings\") \
| .snapshotUtxo \
| 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):
LOVELACE=1000000
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/alice-funds.sk \
--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: .cborHex}'
The transation will be validated by both hydra-node
s 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:
{ "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).
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:
{ "tag": "Fanout" }
This will again submit a transactin 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):
- Alice
- Bob
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/alice-node.sk \
--signing-key-file credentials/alice-funds.sk \
--out-file alice-return-tx-signed.json
cardano-cli transaction submit --tx-file alice-return-tx-signed.json
cardano-cli query utxo \
--address $(cat credentials/bob-node.addr) \
--address $(cat credentials/bob-funds.addr) \
--out-file bob-return-utxo.json
cardano-cli transaction build \
$(cat bob-return-utxo.json | jq -j 'to_entries[].key | "--tx-in ", ., " "') \
--change-address addr_test1qqr585tvlc7ylnqvz8pyqwauzrdu0mxag3m7q56grgmgu7sxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknswgndm3 \
--out-file bob-return-tx.json
cardano-cli transaction sign \
--tx-file bob-return-tx.json \
--signing-key-file credentials/bob-node.sk \
--signing-key-file credentials/bob-funds.sk \
--out-file bob-return-tx-signed.json
cardano-cli transaction submit --tx-file bob-return-tx-signed.json