Set up a Cardano ITN staking pool

From Organic Design wiki

To run a staking pool you'll need a reasonable server that is on a reliable high-bandwidth connection. First you need to install the node software, then create the cryptographically signed staking pool parameters associated with a funded pledge address, and then finally register your pool so it shows up in the staking wallets. This section is mainly based on the instructions at Stake Pool Operators How-To with a few differences.

General packages

apt install build-essential pkg-config htop curl git jq libwww-perl libjson-perl

Install Rust

This isn't needed for normal operation running from the official binaries, but sometimes you want to compile the source of an alpha release, or test out someone's fork or pull request, so it's useful to have the environment ready for compiling just in case.

curl --proto '=https' --tlsv1.2 -sSf | sh
source $HOME/.cargo/env
rustup install stable
rustup default stable

Install and configure Chrony

It's a good idea to install chrony and add a pool closest to your server - and it's also good to check what all the output actually means. Accurate time means less likelihood of rejected blocks. My config which is working pretty well (for Germany) is:

server iburst minpoll 1 maxpoll 1
server iburst minpoll 1 maxpoll 1
server iburst minpoll 1 maxpoll 1
server iburst minpoll 1 maxpoll 1
server iburst minpoll 1 maxpoll 1
server iburst minpoll 1 maxpoll 1
server iburst minpoll 1 maxpoll 1
server iburst minpoll 1 maxpoll 1
server iburst minpoll 1 maxpoll 1
pool iburst minpoll 1 maxpoll 1

keyfile /etc/chrony/chrony.keys
driftfile /var/lib/chrony/chrony.drift
logdir /var/log/chrony

maxupdateskew 10.0
makestep 0.1 3
leapsectz right/UTC
local stratum 10

The chrony sources should show sources that are very low latency such as the following:

210 Number of sources = 12
MS Name/IP address         Stratum Poll Reach LastRx Last sample               
^?     1   1     0  292m   -634us[ +181us] +/- 3108us
^?     1   1     0    4d    -62ms[ +187us] +/- 3168us
^*     1   1   377     1    +46us[  +47us] +/- 2241us
^?     1   1     0   29h    -13ms[ +291us] +/- 3208us
^+ rustime01.rus.uni-stuttg>     1   1   377     1   -352us[ -351us] +/- 3172us
^+ rustime02.rus.uni-stuttg>     1   1   377     2    -40us[  -39us] +/- 3423us
^-             2   1   377     1  +3029us[+3030us] +/-   39ms
^?               1   1     0   73m    +30us[ +161us] +/- 3709us
^+               1   1   377     2    +74us[  +75us] +/- 3798us
^-          2   1   377     0   +904us[ +904us] +/-   49ms
^-                  2   1   377     1    -72us[  -71us] +/-   45ms
^-                2   1   377     2   -459us[ -459us] +/- 9377us

The chrony tracking info should show something like the following:

Reference ID    : 83BC03DE (
Stratum         : 2
Ref time (UTC)  : Thu Jan 23 13:58:10 2020
System time     : 0.000008567 seconds fast of NTP time
Last offset     : -0.000000037 seconds
RMS offset      : 0.000001337 seconds
Frequency       : 15.345 ppm slow
Residual freq   : +0.000 ppm
Skew            : 0.155 ppm
Root delay      : 0.004153801 seconds
Root dispersion : 0.000038784 seconds
Update interval : 2.2 seconds
Leap status     : Normal

Install and configure Jormungandr

We start by installing the latest release (not a pre release) of Jormungandr from the official repo (it's a good idea to subscribe to the repo's feed so you can know as soon as new stable releases are available). I found installing from source pretty straight forward using their instructions too, the only issue was that I needed to install the pkg-config package with apt in addition to their listed prerequisites, Note that you need to log out and back in again for the Rust paths to take effect, and the final binaries are located in ~/.cargo/bin.

One important difference from their configuration procedure is that we need to use the itn_rewards_v1 configuration rather than the beta configuration. A couple of differences from their procedure too: first I changed the port to 3000 as this seems to be what the vast majority of nodes on the network are running, with 3100 for the internal REST interface. I also changed the logging output to stdout, and had to add a storage location to make the chain data persistent. The first few sections of your config file should look something like this:

  "log": [
      "format": "plain",
      "level": "info",
      "output": "stdout"
  "storage": "./storage/",
  "p2p": {
    "listen_address": "/ip4/",
    "public_address": "/ip4/",
    "topics_of_interest": {
      "blocks": "high",
      "messages": "high"
    . . .

Note: here is a sample of our current Jormungandr configuration.

Another issue is that I was not able to find any genesis hash in the configuration as it says there should be, I had to obtain it myself from the last page of slots for epoch 0 which yields this (later I found this parameter and others here). It's best to put this genesis has into a file called genesis-hash.txt so that it can be referred to easily from other programs when needed.

I created a script called to run it with the correct parameters in the background and redirected its output to a log file. It also calls another script I made called which rebuilds the config file with the current list of good peers available from, you'll need to download this into your pool's directory as well if you want to use it.

nohup ./jormungandr --config itn_rewards_v1-config.yaml --genesis-block-hash `cat genesis-hash.txt` >> debug.log &

If you see no errors in the log and the daemon keeps running, you can check the sync progress by running the node stats command and checking that the lastBlockDate matches the current epoch and slot shown in the Shelley explorer.

./jcli rest v0 node stats get --host ""
blockRecvCnt: 41
lastBlockContentSize: 0
lastBlockDate: 7.35623
lastBlockFees: 0
lastBlockHash: "f78c64c030383899ebb1b25dac7ae9d360d222d0b80320323375dc51762651d2"
lastBlockHeight: 26342
lastBlockSum: 0
lastBlockTime: "2019-12-21T15:15:20+00:00"
lastBlockTx: 0
state: "Running"
txRecvCnt: 45
uptime: 886
version: "jormungandr 0.8.3-8f276c0"

To shut the node down gracefully use:

./jcli rest v0 shutdown get --host ""

Create and fund a reward address

Now we need to create three files for our reward account, a public/private key-pair and it's corresponding ADA address which I did by following the instructions in how to register your stake pool on the chain.

./jcli key generate --type ed25519 | tee owner.prv | ./jcli key to-public >
./jcli address account --testing --prefix addr `cat` > owner.addr

You can then send funds (absolute minimum 500.3 ADA after fees) to the address in the owner.addr file from Daedalus or Yoroi, and then check the balance:

./jcli rest v0 account get `cat owner.addr` -h
counter: 0
  pools: []
  epoch: 0
  reward: 0
value: 550000000

Create the stake pool and publish to the blockchain

Finally we need to create the stake pool itself which can be done by calling the handy and scripts. You only need to run the former script which calls the latter, make sure both are executable first. The script takes four parameters, the listening port, the fixed tax (in lovelace), the percentage as a fraction and the private key of your reward address that you put in the owner.prv file above. For example:

./ 3100 1000000 5/100 OWNER_PRIV_KEY | tee results.txt

This will create a pool that takes 1 ADA (1M Lovelaces) fixed rate, and 5%. Note that the instructions say you need another tax_limit parameter, but this must have been removed at some point. This script returns two important values that you need to keep, the Pool ID and Pool Owner, but by appending the tee command, all the output is also captured in results.txt. It also creates the important node_secret.yaml file that is used when starting jormungandr from now on. Check the output for errors and successful signing and sending of the new pool registration transaction, you should see something like this in your output:

## 10. Encode and send the transaction
 ## 11. Remove the temporary files
 ## Waiting for new block to be created (timeout = 200 blocks = 400s)
New block was created - 8fe7ac108640778ca53ce4d38ed8b7b6092454770d4aaf04a38ec548cc66b330

Note: If anything goes wrong in this process, you're best creating a new pledge address before trying again, because if you end up with more than one pool operating on the same pledge address, only the last one will work.

Organising files, versions and backing up data

IMPORTANT: As soon as you've created an address and node secret, create a directory for it using the stake id, or its first few characters, as the name and copy all the specific files into it so you have them in case you need them later. For example you need them if you want to retire the pool, or sign any messages as that pool owner, even if it's just a dummy run and you're sure you'll never need to refer to them again, do it anyway! The files are:

  • node_secret.yaml
  • owner.prv
  • owner.addr
  • results.txt

If you ever need to rebuild your pool, for example if you need to move server, then simply put these files into the directory after you've put all the program files and scripts in place and then when you run jormungandr it will start as that pool and retrieving the block chain data.

Also, the storage data should be regularly backed up (I do it nightly) so that you can go back to a prior storage if you're having extra bad bootstrapping problems. You don't need to keep these backups for more than a few days to a week though.

Over time you will want to update Jormungandr, but also sometimes new versions turn out to perform badly and you want to be able to go back to a previous version. You may also like to experiment with forks and pull requests, so it's a good idea to organise the file structure so that it's very easy to switch between versions. I do it by having all the versions in their own sub-directory such as v0.8.9 which contains the source or the zip and the jcli and jormungandr binaries. Then I have a symlink called current which links to the sub-directory of the version I want to be using. The jcli and jormungandr files in the main directory that are called by are symlinks to current/jcli and current/jormungandr. This way I can just change the current symlink to change the version that's currently in use. Here's an example structure that allows me to quickly switch between v0.8.6, v0.8.7 and v0.8.9:

├── current -> v0.8.9
├── debug.log
├── genesis-hash.txt
├── itn_rewards_v1-config.yaml
├── jcli -> current/jcli
├── jormungandr -> current/jormungandr
├── node_secret.yaml
├── owner.addr
├── owner.prv
├── storage
│   ├── blocks.sqlite
│   ├── blocks.sqlite-shm
│   └── blocks.sqlite-wal
├── v0.8.6
│   ├── jcli
│   ├── jormungandr
│   └── jormungandr-v0.8.6-x86_64-unknown-linux-gnu.tar.gz
├── v0.8.7
│   ├── jcli
│   ├── jormungandr
│   └── jormungandr-v0.8.7-x86_64-unknown-linux-gnu.tar.gz
└── v0.8.9
    ├── jcli
    ├── jormungandr
    └── jormungandr-v0.8.9-x86_64-unknown-linux-gnu.tar.gz

Start your pool!

Now you're ready to shut your node down and restart it with you secret key parameter to start it as a pool!

./jcli rest v0 shutdown get --host ""
nohup ./jormungandr --config itn_rewards_v1-config.yaml --secret node_secret.yaml --genesis-block-hash `cat genesis-hash.txt` >> debug.log &

Note: Remember to add the --secret node_secret.yaml parameter to the command in your script.

Register your pool in the official registry

To allow people to delegate their stake to your pool in a supporting wallet, you need to add your pool to the public registry. This is done by creating a JSON file containing your pool's details, and a file signing the JSON content with the owner's private key, and committing these files to the registry's Github repo.

The name of the pool is your owner public key from the file appended with a .json file extension. The content of the file is as follows. The "owner" field is the same key as used in the filename, and the "pledge_address" field is the owner address from the owner.addr file.

  "owner": "OWNER_PUBKEY",
  "name": "Pudim o gatinho com fome",
  "description": "All your stake are belong with PUDIM!",
  "ticker": "PUDIM",
  "homepage": "",
  "pledge_address": "OWNER_ADDR"

Note: This file must be valid JSON (e.g. you must use double quotes) otherwise the pull request will fail.

Then to sign this JSON file with the owner's private key, you use jcli as follows:

./jcli key sign --secret-key owner.prv --output `cat`.sig `cat`.json

To verify the signature (or any other registration signature), use the key verfiy command, e.g.

./jcli key verify --public-key --signature `cat`.sig `cat`.json

You then need to fork the Cardano foundation's incentivized-testnet-stakepool-registry Github repo, clone your new fork of it, add your two files into the registry directory, add, commit and push them and then create a pull request on the Github site in your forked repo page. Note that it's always best to create a new branch for a pull request, because all commits even after you've made the request are automatically included in the pull request at the upstream repo. Note that the following example is assuming that you've cloned the repo in your pool directory, if you haven't adjust the path to your keys as necessary.

git clone git clone
git checkout -b PUDIM
cd incentivized-testnet-stakepool-registry/registry
cp ../../OWNER_PUBKEY.* ./
git add *
git commit -m "PUDIM"
git push --set-upstream origin PUDIM

The pull request will verify your that your JSON is valid and your signature verifies, and if so the team should approve it for inclusion in the registry shortly after, and your stake pool will be listed in the delegation interface of Daedalus!

The image below shows two pull requests to the official registry repo, our PUDIM registration has passed, meaning that the JSON syntax is all correct and the signature has been successfully verified, but the MORON pool registration has not been successful. Even after a successful pull request, manual validation is required by the Cardano team, ours was accepted the next day.


Note: If you want to remove your pool from the register or change it's details, see these details which involve creating two pull requests, one for a signed "voiding" of the old metadata file, and another to add the new metadata and signature files.

Register with

As a pool operator, it's a good idea to sign up with and claim your pool on the site, i.e. find your pool in the list after you have an account and claim it to associate it with yourself as the owner. This is a great site for seeing clear statistics about your pool in real-time, and how it compared to other pools.

Users who are signed up and run a pool can send their current pool block height to the site, which allows the site to know the current maximum height of the network. This allows the PoolTool site to show you clearly if you node is up to date. Using the information, the site can also display information about the approximate percentage of nodes that are synchronised.


Also you can supply your PoolTool user ID as the forth parameter to the sentinel script, and it will take care of sharing your node's block height with the PoolTool site so you can see easily if it's up to date on the PoolTool site, as shown below in the green bubble to the right. If it's green it means the node is less than ten blocks behind the maximum which is considered as synchronised, if a node gets ten or more blocks behind the bubble becomes orange and then red.


By providing your block height to PoolTool, they will return the current maximum height across all shared heights they've received, which allows your sentinel to show how far behind your node is. This is shown as a negative integer appended to the block number in the log as shown in the example below, most of the blocks should be appended with "-0". Sometimes you'll see block heights appended with "--" which means the request to PoolTool failed for example due to taking longer than the 1s timeout limit imposed by the sentinel script.

[1577918246/109] Epoch:19 Slot:6085 Block:62588-2 Hash:7f0ed4a88a80104aea8e9162fe618b6f8d3d480773dd94eb3b66729c9bdd4c7b

Note: If you are behind a block or two regularly then you may find that you have cpu overloading issues, check your cpu usage and if a single CPU is peaking often at 100% you'll need to reduce your max_connections setting or change your hardware, because many of your blocks will be produced too late to be accepted by the network in that state.

Troubleshooting & questions

This has now been moved in a dedicated article at Cardano staking pool FAQ.

Manual pages

Testnet info sites and tools

See also