Make Ada Pool
Devices Needed
Server Hardware Minimum Requirements
Where to Get a Server or VPS?
Install 2FA App on Your Phone
Learn How to Secure Your Critical Key Files
Enable 2FA on Your Hosting Provider Account
About SSH
Log in to Your Server via SSH
Update Ubuntu on Server
Enable Live Patch
Create New User
Generate RSA Key Pair
Copy Public Key to Ubuntu Server
Disable Root User
Disable Password
Change SSH Port
Enable 2FA: Install Google PAM
Enable 2FA: Edit PAM SSHD Config
Enable 2FA: Edit SSHD Config
Secure Shared Memory
Disable IPv6
Install Fail2Ban
Install Dependencies
Install Chrony
Install Cabal & GHC with GHCup
Install Libsodium
Install cardano-node
Setup Bash Variables
Setup Config Files
Block-producing Node vs Relay Node
Setup Another Node
Setup Topology
Setup Firewall
Setup Auto-starting Node on Server Boot
Install Live View
About Air-gapped Computer
Install Ubuntu on Air-gapped Computer
How to Download and Upload Files with scp
Copy cardano-cli to Air-gapped Computer
About KES (Key Evolving Signature)
Get KES Period
Generate Keys & Certificates
Update Startup Scripts
Get Protocol Parameters
Fund Your payment-with-stake.addr
Understanding UTxO and How Transactions Work
Register Stake Address
Prepare to Create Stake Pool Registration Certificate
Determine --pool-pledge
Determine --pool-cost
Determine --pool-margin
Prepare Metadata
Create poolExtendedMetadata.json
How to Upload poolExtendedMetadata.json to Github
Create poolMetadata.json
Upload poolMetadata.json
Four Ways to Register Your Relay Addresses
Create Pool Registration Certificate
Generate Delegation Certificate to Honor Your Pledge
Install Topology Updater
Install Grafana & Prometheus
Setup Grafana
Setup Telegram to Receive Important Notifications
Setup Grafana to Send Alerts to Telegram
Install OSSEC
Get Alerts When Server is Down
Pool Setup Checklist
Important Metrics to Watch in Grafana
🎉🥳 Congratulations





All-in-one Guide to Making a Secure Cardano Stake Pool   
To many of us, the learning curve of creating a secure stake pool is steep. This guide aims to be an all-in-one place that helps save your research time as much as possible.
✅ Updated for cardano-node 1.35.0
❤️ Big thanks to the excellent guide from CoinCashew, which part of this guide took reference from. I really learnt a lot from them.
To make it easy to follow and work smoothly, I had to work extremely hard to refine the details and test every single command. Any support is greatly appreciated 🙏 Thanks to your supports, I can continue updating this guide.
❤️ Stake with my pool, ticker: STAY
❤️ Donate to my address: addr1qyxd2rpa3fwxpj739zcddwac5knke4s2casn0czfr32v5zf6g5qmpfv40fal70xtuqc4wx4h708h26l78eajgmhuvntqhkhr4z
Devices Needed
Aside from your current computer, you will need:
  • A computer that has no internet connection (air-gapped) to create and manage critical key files. It has to be a separated device and not a virtual machine. It doesn't need to be powerful though. You can use your old laptop or get a cheap one.
  • At least two separated servers. One for Block-producing, the other for relay.
Server Hardware Minimum Requirements
  • An Intel or AMD x86 processor with two or more cores, at 2GHz or faster.
  • 16GB of RAM
  • 100GB of free disk space
  • Runs Ubuntu 20.04
  • Static IPv4
Where to Get a Server or VPS?
Big cloud brands like AWS, Google, DigitalOcean, Contabo or Azure are good, but your local hosting providers may be more cost-efficient and are also better for decentralization. Ideally, your provider should use green, renewable energy sources.
⚠️ Make sure your provider supports 2FA log in.
Install 2FA App on Your Phone
2FA (2-Factor Authentication) allows you to add another layer of security when logging in your accounts or servers. You do this by installing an app that gives you an OTP (One-Time Password) which expires after a short while.
For iOS, I highly recommend this app. It's a very well-thought app with fast and simple iCloud syncing. (I have no affiliation)
For Android, if the same app isn't available, the best choice IMHO is Microsoft Authenticator.
Learn How to Secure Your Critical Key Files
Basically, you will have to encrypt your critical key files on your air-gapped computer, then back up the encrypted files on various places. You definitely should watch this awesome video from Charles Hoskinson.
Enable 2FA on Your Hosting Provider Account
How to do this really depends on your hosting provider. In general, log in to your hosting account and look for the "2FA" or "OTP" section, contact support if needed.
This step is critically important because anyone has access to your hosting account can control your servers.
About SSH
SSH (Secure Shell) allows you to securely connect to and operate your servers from your local computer.
If your computer is running Linux or macOS, it already has SSH installed.
SSH is not natively supported on Windows. Although you can install PuTTY to use SSH, it's much more complicated IMHO. I would recommend you to just install Ubuntu OS and dual boot. You'll be able to use the commands in this guide. A plus point: Since it's a clean OS install, it's gonna be more secure than your day-to-day OS. How to install Ubuntu.
Log in to Your Server via SSH
  • Open a new terminal window
📒 How to open terminal
macOS:
  • Press command + spacebar to open Spotlight Search.
  • Type "terminal"
  • Click on the terminal app.
Ubuntu
  • Click on "Show Applications" (Bottom-left corner)
  • Type "terminal"
  • Click on the terminal app.
  • Replace ip_of_server with the IPv4 of your server given by your hosting provider.
  • Paste into terminal and hit Enter/Return
ssh root@ip_of_server
If this is the first time you log in, the output may look like this:
The authenticity of host '...' can't be established.
ECDSA key fingerprint is ...
Are you sure you want to continue connecting (yes/no/[fingerprint])?
  • Type "yes" then Enter/Return
  • You will be asked for a password, use the password for "root" user given by your hosting provider.
As you type the password, nothing will appear. Just type it and press Enter/Return.
Update Ubuntu on Server
  • Check Ubuntu version
lsb_release -a
Output should look like this
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
  • Verify that Description is Ubuntu 20.04.x LTS (LTS means Long-term support.)
  • If not, upgrade Ubuntu
sudo do-release-upgrade
  • Just follow the instructions to upgrade.
  • Check for minor updates
sudo apt update
sudo apt-get update -y && sudo apt-get upgrade -y
sudo apt-get autoremove
sudo apt-get autoclean
Enable Live Patch
Live patch allows security updates for Ubuntu to be installed without restarting your server.
  • Follow instructions to get the live patch key. You will need to create an Ubuntu account.
  • Install snap
sudo apt install snapd
  • Install Live Patch
sudo snap install canonical-livepatch
  • Enable live patch, replacing livepatch_key with the given key.
sudo canonical-livepatch enable livepatch_key
Output:
Successfully enabled device. Using machine-token: ...
Create New User
For security purpose, this new user is going to replace root user for all tasks.
  • Log in to your server as root
ssh root@ip_of_server
  • Replace not_root with your unique user name.
sudo adduser not_root
  • Create a new password for not_root in your password manager.
Output:
Adding user `not_root' ...
Adding new group `not_root' (1000) ...
Adding new user `not_root' (1000) with group `not_root' ...
Creating home directory `/home/not_root' ...
Copying files from `/etc/skel' ...
Enter new UNIX password:
  • Enter the password you've just created
As you type the password, nothing will appear. Just type it and press Enter/Return.
Output:
Retype new UNIX password:
passwd: password updated successfully
Changing the user information for not_root
Enter the new value, or press ENTER for the default
Full Name []: NotRootUser
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n] y
  • Add new user to sudo group
usermod -aG sudo not_root
  • Switch to the new user account
su - not_root
  • Verify superuser privileges
sudo ls -la /root
Output may look like this:
[sudo] password for not_root:
total 24
drwx------  4 root root 4096 May 28 09:43 .
drwxr-xr-x 19 root root 4096 May 28 09:42 ..
-rw-r--r--  1 root root 3340 May 28 05:05 .bashrc
drwx------  2 root root 4096 May 28 05:06 .cache
-rw-r--r--  1 root root  161 Dec  5  2019 .profile
drwxr-xr-x  3 root root 4096 May 28 09:43 snap
Generate RSA Key Pair
💻 On local computer
  • Open a new terminal window and run:
ssh-keygen
Output:
Generating public/private rsa key pair.
Enter file in which to save the key (/your_home/.ssh/id_rsa):
  • Press Enter/Return
If you're asked:
/your_home/.ssh/id_rsa already exists.
Overwrite (y/n)?
  • ⚠️ Make sure you have backed up the current key in .ssh folder. If you don't, it will be overwritten, and you will lose access to anywhere that requires it.
  • Type y then Enter/Return
📒 Where to find the ".ssh" folder
macOS:
  • Open Finder. Press command + shift + H to go to "Home".
  • Press command + shift + . (period) to show hidden files.
Ubuntu:
  • Open Files (Top app on the left column)
  • In Files, click "Home" on the left column.
  • Press ctrl + H to show hidden files.
Output:
Enter passphrase (empty for no passphrase):
  • Create a password for SSH key in your password manager.
  • Type password and press Enter/Return.
As you type the password, nothing will appear. Just type it and press Enter/Return.
Output:
Your identification has been saved in /your_home/.ssh/id_rsa.
Your public key has been saved in /your_home/.ssh/id_rsa.pub.
The key fingerprint is:
a9:49:2e:2a:5e:33:3e:a9:de:4e:77:11:58:b6:90:26 username@localcomputer
The key's randomart image is:
+---[RSA 3072]----+
|          o++o=o |
|        . *=*=+..|
|       . *oX+*..+|
|        +.Bo= o*o|
|        S=.=.. .*|
|        . o..o ..|
|       . .  . .  |
|       E.        |
|       ..        |
+----[SHA256]-----+
  • Limit the permission of private key file (id_rsa). If you don't, SSH won't use it.
💡 Tip: Drag and drop a file into terminal to get its path.
chmod 400 drag_and_drop_PRIVATE_key_(id_rsa)_here
Example: chmod 400 /path/to/id_rsa
  • Move public and private SSH keys to a proper place.
  • Encrypt and back up the encrypted SSH keys.
Copy Public Key to Ubuntu Server
💻 On local computer
  • Drag-and-drop public key and run this command
ssh-copy-id -i drag_and_drop_PUBLIC_key_(id_rsa.pub)_here not_root@ip_of_server
Example: ssh-copy-id -i /path/to/id_rsa.pub [email protected]
Output:
Source of key(s) to be installed: "path to local public key (id_rsa.pub)"
attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
not_root@ip_of_server's password:
  • Type in not_root's password (NOT SSH key's password)
Output:
Number of key(s) added:        1
Now try logging into the machine, with:   "ssh 'not_root@ip_of_server'"
and check to make sure that only the key(s) you wanted were added.
At this point, public key (id_rsa.pub) has been uploaded to the server.
  • Try to log in server with SSH key
ssh -i drag_and_drop_PRIVATE_key_(id_rsa)_here not_root@ip_of_server
Output:
Enter passphrase for key 'path to private key (id_rsa)':
  • Type in password for SSH key (NOT not_root's password) then press Enter/Return.
  • Confirm if logged in successfully.
💡 Tip: Save the customized command to a text file for logging in later.
  • Verify that this is the only key can be used to log in.
cat $HOME/.ssh/authorized_keys
Output should match the content of "id_rsa.pub" file, for example:
ssh-rsa
AAAAqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WqrR0WpOTCcRWyUW0WW04c [email protected]
Disable Root User
This will strengthen your server's security.
  • Log in to your server as not_root user using SSH key
⚠️ Make sure you are logged in as not_root user. This verifies that you have another account to log in other than root.
  • Edit SSH daemon config
sudo nano /etc/ssh/sshd_config
This will open SSH config file in "nano" editor
  • Search for a line contains PermitRootLogin
  • If it is commented (has # at the beginning of the line), uncomment it by deleting the # character.
  • Change the value to no
That part will look like this
PermitRootLogin no
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Validate the syntax of SSH config.
sudo sshd -t
  • If there is no error, restart SSH
sudo service ssh restart
  • Open a new terminal and try logging in with root user to see if that's still possible.
ssh root@ip_of_server
That shouldn't be possible by now.
Disable Password
Disabling password protects your server against brute-force attacks.
  • Log in as not_root user using your SSH key. (Use the command you saved earlier)
⚠️ Make sure you're logged in using SSH key and not password. This verifies that you have another way to log in other than your password.
  • Edit SSH config
sudo nano /etc/ssh/sshd_config
  • Search for a line contains PasswordAuthentication
  • If it is commented (has # at the beginning of the line), uncomment it by deleting the # character.
  • Change the value to no
  • Also set PermitEmptyPasswords to no (Uncomment if needed)
That part may look like this:
# To disable tunneled clear text passwords, change to no here!
PasswordAuthentication no
PermitEmptyPasswords no
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Validate the syntax of SSH config.
sudo sshd -t
  • If there is no error, restart SSH
sudo service ssh restart
  • Open a new terminal and try logging in with password to see if that's still possible.
ssh not_root@ip_of_server
That shouldn't be possible by now:
Permission denied (publickey).
Change SSH Port
Changing SSH port makes it difficult to attack your server because the attackers will have to know the port first.
  • Log in as not_root.
  • Edit SSH config
sudo nano /etc/ssh/sshd_config
  • Look for the Port 22 line and change the number to a random 4-digit number.
⚠️ DON'T make it easy to guess, like "1234" or "1111", "2222"...
That line look like this:
Port xxxx
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Validate the syntax of SSH config.
sudo sshd -t
  • If there is no error, restart SSH
sudo service ssh restart
  • Open a new terminal window and try logging in with the new port number.
ssh -p port_number -i drag_and_drop_PRIVATE_key_(id_rsa)_here not_root@ip_of_server
Example: ssp -p 1234 -i /path/to/id_rsa [email protected]
💡 Tip: Save the customized command to a text file for logging in later.
  • Open a new terminal window and try logging in with -p 22 or no -p to see if that's still possible
ssh -i drag_and_drop_PRIVATE_key_(id_rsa)_here not_root@ip_of_server
That shouldn't be possible by now:
ssh: connect to host ... port ...: Connection refused
Enable 2FA: Install Google PAM
  • Log in to your server.
⚠️ DO NOT CLOSE TERMINAL WINDOW UNTIL AFTER YOU'VE FINISHED ENABLING 2FA AND SUCCESSFULLY LOGGED IN WITH 2FA IN ANOTHER TERMINAL WINDOW. IF YOU CLOSE THE CURRENT TERMINAL WINDOW AND FAIL TO LOG IN WITH 2FA, YOU WILL BE LOCKED OUT OF YOUR SERVER.
  • Install Google PAM (Pluggable Authentication Module)
sudo apt-get install libpam-google-authenticator
Output:
After this operation, 190 kB of additional disk space will be used.
Do you want to continue? [Y/n] y
  • Type y then Enter/Return
  • Run the initialization app
google-authenticator
Output:
Do you want authentication tokens to be time-based (y/n) y
  • Type y then Enter/Return
A giant QR code will appear.
  • Use the OTP app on your phone to scan it. You may have to expand the terminal window to fullscreen to see the whole QR code.
  • If the QR code is larger than your screen, scroll up. There's a link on the top of the giant code. Paste it to your browser to get a smaller one.
Emergency scratch codes are used when you lose access to the OTP app. Each code can only be used once.
Your new secret key is: XXXXXXXXXXXXXXXXXX
Your verification code is XXXXXX
Your emergency scratch codes are:
  XXXXXXXX
  XXXXXXXX
  XXXXXXXX
  XXXXXXXX
  XXXXXXXX
⚠️ Save the information above in a safe place.
Do you want me to update your "/home/not_root/.google_authenticator" file? (y/n) y
  • Type y then Enter/Return
Do you want to disallow multiple uses of the same authentication token? This restricts you to one login about every 30s, but it increases your chances to notice or even prevent man-in-the-middle attacks (y/n) y
  • Type y then Enter/Return
By default, a new token is generated every 30 seconds by the mobile app. In order to compensate for possible time-skew between the client and the server, we allow an extra token before and after the current time. This allows for a
time skew of up to 30 seconds between the authentication server and client. Suppose you experience problems with poor time synchronization. In that case, you can increase the window from its default size of 3 permitted codes (one previous code, the current code, the next code) to 17 permitted codes (the eight previous codes, the current
code, and the eight next codes). This will permit a time skew of up to 4 minutes between client and server.
Do you want to do so? (y/n) n
  • ⚠️ Type n, then Enter/Return
If the computer that you are logging into isn't hardened against brute-force login attempts, you can enable rate-limiting for the authentication module. By default, this limits attackers to no more than 3 login attempts every 30s.
Do you want to enable rate-limiting? (y/n) y
  • Type y then Enter/Return
Enable 2FA: Edit PAM SSHD Config
  • Open the file
sudo nano /etc/pam.d/sshd
By default, PAM will ask for a password. But since password was disabled earlier, it causes a conflict and will prevent you from logging in. You need to tell PAM not to ask for a password.
  • Find the line @include common-auth (possibly near the beginning of the file)
  • Add # to the beginning of the line to disable it.
That part should looks like this
# Standard Un*x authentication.
#@include common-auth
Tell PAM to use Google Authenticator
  • Add this line to the end of the file
auth required pam_google_authenticator.so
That part may look like this:
# Standard Un*x password updating.
@include common-password
auth required pam_google_authenticator.so
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Validate the syntax of SSH config.
sudo sshd -t
Enable 2FA: Edit SSHD Config
  • Open the file
sudo nano /etc/ssh/sshd_config
  • Find ChallengeResponseAuthentication and set to "yes"
That part may look like this:
# Change to yes to enable challenge-response passwords (beware issues>
# some PAM modules and threads)
ChallengeResponseAuthentication yes
  • Add this line to the bottom of the file.
AuthenticationMethods publickey,keyboard-interactive
The line above tell SSH that in order to log in, the user need:
  • publickey: The SSH key.
  • keyboard-interactive: The one-time password (OTP)
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Validate the syntax of your new SSH configuration.
sudo sshd -t
  • Restart SSH for the changes to take effect.
sudo systemctl restart sshd.service
⚠️ Do NOT close the current terminal window.
  • Open a new terminal window and try logging in again as usual.
Now after entering SSH key password, you will be asked for "Verification code", which is the OTP. Open the OTP app to get the code.
Example
ssp -p 1234 -i /path/to/id_rsa [email protected]
Enter passphrase for key '/path/to/id_rsa':
Verification code:
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-67-generic x86_64)
Secure Shared Memory
  • Edit fstab
sudo nano /etc/fstab
  • Insert the following line to the bottom of the file
none /run/shm tmpfs defaults,ro 0 0
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Validate the syntax of fstab
sudo mount -a
  • If there is no error, reboot server
sudo reboot
Disable IPv6
IPv6 if left unused will create an extra attack surface.
  • Edit grub
sudo nano /etc/default/grub
  • Add ipv6.disable=1 into GRUB_CMDLINE_LINUX_DEFAULT and GRUB_CMDLINE_LINUX, separated from other commands with a space.
That part may look like this:
GRUB_CMDLINE_LINUX_DEFAULT="nomodeset ipv6.disable=1"
GRUB_CMDLINE_LINUX="net.ifnames=0 biosdevname=0 ipv6.disable=1"
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Update grub for changes to take effect
sudo update-grub2
  • Check to see if IPV6 is current enabled
test -f /proc/net/if_inet6 && echo "IPv6 is enabled." || echo "IPv6 is disabled."
It may be enabled at the moment
IPv6 is enabled.
  • Reboot the system
sudo reboot
  • Log back in
  • Check again
test -f /proc/net/if_inet6 && echo "IPv6 is enabled." || echo "IPv6 is disabled."
This time IPv6 is disabled
IPv6 is disabled.
Install Fail2Ban
Ban an IP from accessing your server after x failed log in attempts.
  • Install Fail2Ban
sudo apt-get install fail2ban -y
  • Edit fail2ban config file
sudo nano /etc/fail2ban/jail.local
  • Add the following lines to the bottom of the file, replacing your_random_SSH_port_number with the right value.
[sshd]
enabled = true
port = your_random_SSH_port_number
filter = sshd
logpath = /var/log/auth.log
maxretry = 10
# whitelisted IP addresses
#ignoreip = 192.168.1.0/24 127.0.0.1/8
Set maxretry relatively high to avoid locking yourself out.
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Restart fail2ban
sudo systemctl restart fail2ban
Install Dependencies
  • Paste into terminal one line at a time
sudo apt-get update -y
sudo apt-get upgrade -y
sudo apt-get install net-tools build-essential pkg-config libffi-dev libgmp-dev -y
sudo apt-get install libssl-dev libtinfo-dev libsystemd-dev zlib1g-dev -y
sudo apt-get install make g++ tmux git jq wget libncursesw5 libtool autoconf -y
sudo apt-get install llvm-12 numactl libnuma-dev automake libpq-dev libsodium-dev
  • Paste into terminal one line at a time
mkdir -p $HOME/src
cd $HOME/src
git clone https://github.com/bitcoin-core/secp256k1
cd secp256k1
git checkout ac83be33
./autogen.sh
./configure --enable-module-schnorrsig --enable-experimental
make
sudo make install
sudo ln -s /usr/local/lib/libsecp256k1.so.0 /usr/lib/libsecp256k1.so.0
Install Chrony
Chrony helps keep your server's time synchronized with Network Time Protocol (NTP), which is critical in running a blockchain.
  • Install chrony
sudo apt-get install chrony -y
  • Create chrony.conf
cat > $HOME/chrony.conf << EOF
pool time.google.com       iburst minpoll 1 maxpoll 2 maxsources 3
pool ntp.ubuntu.com        iburst minpoll 1 maxpoll 2 maxsources 3
pool us.pool.ntp.org     iburst minpoll 1 maxpoll 2 maxsources 3
# This directive specify the location of the file containing ID/key pairs for
# NTP authentication.
keyfile /etc/chrony/chrony.keys
# This directive specify the file into which chronyd will store the rate
# information.
driftfile /var/lib/chrony/chrony.drift
# Uncomment the following line to turn logging on.
#log tracking measurements statistics
# Log files location.
logdir /var/log/chrony
# Stop bad estimates upsetting machine clock.
maxupdateskew 5.0
# This directive enables kernel synchronisation (every 11 minutes) of the
# real-time clock. Note that it can’t be used along with the 'rtcfile' directive.
rtcsync
# Step the system clock instead of slewing it if the adjustment is larger than
# one second, but only in the first three clock updates.
makestep 0.1 -1
EOF
  • Move file to /etc/chrony
sudo mv $HOME/chrony.conf /etc/chrony/chrony.conf
  • Restart chrony service
sudo systemctl restart chronyd
  • Verify chrony is running
sudo systemctl status chronyd
Output should look like this
🟢 chrony.service - chrony, an NTP client/server
     Loaded: loaded (/lib/systemd/system/chrony.service; enabled; vendor preset: enabled)
     Active: active (running)
  • Press Ctrl-C to exit.
Install Cabal & GHC with GHCup
  • Use this command line
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
  • Source your .bashrc file for changes to take effect
source $HOME/.bashrc
  • Install the specific versions
ghcup install ghc 8.10.7
ghcup install cabal 3.6.2.0
ghcup set ghc 8.10.7
ghcup set cabal 3.6.2.0
  • Verify that ~/.local/bin and ~/.ghcup/bin are in your PATH
echo $PATH
Output may contains this:
:~/.local/bin:~/.ghcup/bin:
  • If not, open your .bashrc file with nano text editor
nano $HOME/.bashrc
  • Go to the bottom of the file and add the following lines
export PATH="~/.local/bin:~/.ghcup/bin:$PATH"
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Source your .bashrc file for changes to take effect
source $HOME/.bashrc
  • Verify cabal version
cabal --version
It should be 3.6.2.0
  • Check if GHC is in the correct version
ghc --version
It should be 8.10.7
Install Libsodium
  • Download Libsodium
mkdir -p $HOME/src
cd $HOME/src
git clone https://github.com/input-output-hk/libsodium
  • Install Libsodium
cd libsodium
git checkout 66f017f1
./autogen.sh
./configure
make
sudo make install
  • Open .bashrc
sudo nano $HOME/.bashrc
  • Go to the bottom of the file and add the following lines
export LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH"
export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH"
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Source .bashrc for changes to take effect
source $HOME/.bashrc
Install cardano-node
  • Download cardano-node source
mkdir -p $HOME/src
cd $HOME/src
git clone https://github.com/input-output-hk/cardano-node.git
  • Checkout with the lastest cardano-node release.
cd $HOME/src/cardano-node
git fetch --all --recurse-submodules --tags
git checkout tags/1.35.0
  • We explicitly use the GHC version that we installed earlier. This avoids defaulting to a system version of GHC that might be older than the one you have installed
cabal configure --with-compiler=ghc-8.10.7
🕚 It may take a while.
  • Update the local project file to use the VRF library that you installed earlier.
echo "package cardano-crypto-praos" >>  cabal.project.local
echo "  flags: -external-libsodium-vrf" >>  cabal.project.local
  • Clean and update Cabal
cabal clean
cabal update
🕚 It may take a while.
  • Built Cardano node with cabal
cabal build all
🕚 It may take up to a few hours.
Wait for the build to finish to continue.
  • Install the newly built cardano-node and cardano-cli
mkdir -p ~/.local/bin
cp -p "$(./scripts/bin-path.sh cardano-node)" ~/.local/bin/
cp -p "$(./scripts/bin-path.sh cardano-cli)" ~/.local/bin/
🕚 It may take a while.
Note, we avoid using cabal install because that method prevents the installed binaries from reporting the git revision with the --version switch.
  • Check the version that has been installed
echo ;\
cardano-cli --version ;\
echo ;\
cardano-node version
The versions should be 1.35.0
Setup Bash Variables
These variables will help you run other commands more accurately.
  • Open .bashrc
sudo nano $HOME/.bashrc
  • Add these lines to the end of file
  • Replace 0.0.0.0 with your node's IPv4
  • Replace 1234 with your node's SSH Port (the random SSH port you created earlier)
export NODE_IP="0.0.0.0"
export NODE_SSH_PORT="1234"
export POOL_RELAY_PORT="6000"
export NODE_HOME="$HOME/cardano"
export NODE_DB_PATH="${NODE_HOME}/db"
export NODE_CONFIG="${NODE_HOME}/mainnet-config.json"
export NODE_TOPOLOGY="${NODE_HOME}/mainnet-topology.json"
# Required by cardano-node
export CARDANO_NODE_SOCKET_PATH="${NODE_HOME}/db/socket"
alias editbash="sudo nano $HOME/.bashrc"
alias sourcebash="source $HOME/.bashrc"
alias nodehome="cd $NODE_HOME"
alias tip="cardano-cli query tip --mainnet | jq -r '.slot'"
alias liveview="$NODE_HOME/simpleLiveView/./liveview.sh"
alias startnode="sudo systemctl start cardano-node"
alias stopnode="sudo systemctl stop cardano-node"
alias restartnode="sudo systemctl restart cardano-node"
alias nodelog="sudo journalctl --unit=cardano-node --follow"
alias nodestatus="sudo systemctl status cardano-node"
alias metrics="curl localhost:12798/metrics"
alias editconfig="sudo nano $NODE_HOME/mainnet-config.json"
alias topo="cat $NODE_HOME/mainnet-topology.json"
alias peerin="echo ;\echo \"PEERS IN:\" ;\echo \"CONNECTIONS   IP\" ;\netstat -tn 2 | grep ${NODE_IP}:${POOL_RELAY_PORT} | awk '{print \$5}' | cut -d: -f1 | sort | uniq -c | sort -nr"
alias restartchrony="sudo systemctl restart chronyd"
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Source .bashrc for changes to take effect
source $HOME/.bashrc
💡 You may notice some aliases, these are really convenient for frequently used commands. For example, instead of typing sudo nano $HOME/.bashrc, you just need to type editbash
Setup Config Files
  • Make node home folder
mkdir $NODE_HOME
cd $NODE_HOME
  • Download config files templates
cd $NODE_HOME
wget https://hydra.iohk.io/job/Cardano/cardano-node/cardano-deployment/latest-finished/download/1/mainnet-config.json
wget https://hydra.iohk.io/job/Cardano/cardano-node/cardano-deployment/latest-finished/download/1/mainnet-byron-genesis.json
wget https://hydra.iohk.io/job/Cardano/cardano-node/cardano-deployment/latest-finished/download/1/mainnet-shelley-genesis.json
wget https://hydra.iohk.io/job/Cardano/cardano-node/cardano-deployment/latest-finished/download/1/mainnet-alonzo-genesis.json
wget https://hydra.iohk.io/job/Cardano/cardano-node/cardano-deployment/latest-finished/download/1/mainnet-topology.json
  • Update TraceBlockFetchDecisions in mainnet-config.json to true to enable live view.
sed -i mainnet-config.json \
    -e "s/TraceBlockFetchDecisions\": false/TraceBlockFetchDecisions\": true/g"
Block-producing Node vs Relay Node
A stake pool needs:
🔷 One Block-producing Node:
  • Focus on producing blocks
  • Only connects to your relay node(s)
  • Only allow connections from your relay node(s) to go through POOL_RELAY_PORT
✳️ At least one Relay Node:
  • Handle connections with other pools' relay nodes
  • Connect to Block-producing Node
Each node must be an independent server.
Setup Another Node
  • Get another server and repeat all the steps from the beginning to this point.

Now you have three working spaces:

🔷  Block-producing Node
✳️  Relay Node
💻  Local Computer
  • Open 3 terminal windows
Window 1 logged in to 🔷 Block-producing Node
Window 2 logged in to ✳️ Relay Node
Window 3 is for 💻 Local commands
✳️ On Relay Node
Save block-producing node's IPv4 to a file for later reference
  • Replace 0.0.0.0 with the IPv4 of your 🔷 Block-producing node
echo -n 0.0.0.0 > $NODE_HOME/bp-node-ip.txt
Setup Topology
🔷 On Block-producing Node
  • Replace 0.0.0.0 with the IPv4 of your ✳️ Relay node
cat > $NODE_HOME/mainnet-topology.json << EOF
{
    "Producers": [
      {
        "addr": "0.0.0.0",
        "port": ${POOL_RELAY_PORT},
        "valency": 1
      }
    ]
  }
EOF
✳️ On Relay Node
  • Create temporary topology file
cat > $NODE_HOME/mainnet-topology.json << EOF
{
    "Producers": [
      {
        "addr": "$(cat $NODE_HOME/bp-node-ip.txt)",
        "port": ${POOL_RELAY_PORT},
        "valency": 1
      },
      {
        "addr": "relays-new.cardano-mainnet.iohk.io",
        "port": 3001,
        "valency": 2
      }
    ]
  }
EOF
Valency is the number of connections to an address at the same time. A valency greater than 1 is only applied when the address is a domain, which can represent multiple IPs.
Setup Firewall
🔷 On Block-producing Node
  • Allow ssh port number.
sudo ufw allow ${NODE_SSH_PORT}/tcp
🔷 On Block-producing Node
Only allow your Relay Node(s) to access your Block-producing Node
  • Replace 1.2.3.4 with your ✳️ Relay node's IPv4.
sudo ufw allow proto tcp from 1.2.3.4 to any port ${POOL_RELAY_PORT}
🔷 On Block-producing Node
  • By default, deny all incoming connections and allow all outgoing connections
sudo ufw default deny incoming
sudo ufw default allow outgoing
🔷 On Block-producing Node
  • Enable firewall
sudo ufw enable
Output:
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
  • Type y then hit Enter/Return
🔷 On Block-producing Node
  • Check firewall status
sudo ufw status numbered
Output should look like this
Status: active
     To                         Action      From
     --                         ------      ----
[ 1] POOL_RELAY_PORT/tcp        ALLOW IN    RELAY_NODE_IP
[ 2] SSH_port/tcp               ALLOW IN    Anywhere
✳️ On Relay Node
  • Setup firewall
sudo ufw allow ${NODE_SSH_PORT}/tcp
sudo ufw allow ${POOL_RELAY_PORT}/tcp
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable
Output:
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
  • Type y then hit Enter/Return
  • Check firewall status
sudo ufw status numbered
Output should look like this
Status: active
     To                         Action      From
     --                         ------      ----
[ 1] SSH_port/tcp               ALLOW IN    Anywhere
[ 2] POOL_RELAY_PORT/tcp        ALLOW IN    Anywhere
Setup Auto-starting Node on Server Boot
🔷✳️ On All Nodes
  • Create a script file that starts Cadano Node
cat > $NODE_HOME/startCardanoNode.sh << EOF
#!/bin/bash
$HOME/.local/bin/cardano-node +RTS -N -RTS run \
--topology ${NODE_TOPOLOGY} \
--database-path ${NODE_DB_PATH} \
--socket-path ${CARDANO_NODE_SOCKET_PATH} \
--host-addr ${NODE_IP} \
--port ${POOL_RELAY_PORT} \
--config ${NODE_CONFIG}
EOF
🔷✳️ On All Nodes
  • Give execution permission
chmod +x $NODE_HOME/startCardanoNode.sh
🔷✳️ On All Nodes
  • Create systemd service
cat > $NODE_HOME/cardano-node.service << EOF
[Unit]
Description = Cardano Node systemd Service
Wants = network-online.target
After = network-online.target
[Service]
Type = simple
User = ${USER}
WorkingDirectory = ${NODE_HOME}
ExecStart = /bin/bash -c '${NODE_HOME}/startCardanoNode.sh'
KillSignal = SIGINT
RestartKillSignal = SIGINT
TimeoutStopSec = 5
LimitNOFILE = 32768
Restart = always
RestartSec = 5
[Install]
WantedBy = multi-user.target
EOF
🔷✳️ On All Nodes
  • Move the unit file to /etc/systemd/system and give it permissions.
sudo mv $NODE_HOME/cardano-node.service /etc/systemd/system/cardano-node.service
sudo chmod 644 /etc/systemd/system/cardano-node.service
🔷✳️ On All Nodes
  • Enable service
sudo systemctl daemon-reload
sudo systemctl enable cardano-node
🔷✳️ On All Nodes
  • Run Libsodium config to prevent "error while loading shared libraries"
sudo ldconfig
🔷✳️ On All Nodes
  • Start running Cardano Node
sudo systemctl start cardano-node
or just type startnode
🔷✳️ On All Nodes
  • View systemd status
sudo systemctl status cardano-node
or just type nodestatus
Output should look like this
🟢 cardano-node.service - Cardano Node systemd Service
     Loaded: loaded (/etc/systemd/system/cardano-node.service; enabled; vendor preset: en>
     Active: active (running) since ...
  • Press q to quit
🔷✳️ On All Nodes
  • Watch cardano-node's log
journalctl --unit=cardano-node --follow
or just type nodelog
Press Ctrl-C to stop watching (not stopping the service)
After a while, you will start to see
Chain extended, new tip: xxx at slot xxx
That means the node is syncing with the blockchain.
To stop service
⚠️ just for reference. Don't do it now.
sudo systemctl stop cardano-node
or just type stopnode
To restart service
⚠️ just for reference. Don't do it now.
sudo systemctl restart cardano-node
or just type restartnode
Install Live View
🔷✳️ On All Nodes
  • Install SimpleLiveView
cd $NODE_HOME
git clone https://github.com/crypto2099/simpleLiveView
🔷✳️ On All Nodes
  • Edit liveview.sh
chmod +x $NODE_HOME/simpleLiveView/liveview.sh
sed -i $NODE_HOME/simpleLiveView/liveview.sh \
-e "s/cardanoport=3001/cardanoport=\${POOL_RELAY_PORT}/g"
🔷✳️ On All Nodes
  • Run simpleLiveView
$NODE_HOME/simpleLiveView/./liveview.sh
or just type liveview

Let the nodes sync with the blockchain

🕚 It may take days to complete. Check progress periodically using Live View. While you wait, let's set up 🔒 Airgapped Computer.
About Air-gapped Computer
An 🔒 Airgapped Computer is dedicated to creating and managing critically sensitive information.
🔒 Air-gapped computer requirements:
  • Must be a separate device, not a Virtual Machine.
  • No internet or network connection of any kind.
  • The only way to communicate with other devices is via a clean USB stick.
  • Disk encryption enabled.
  • Has a fresh install of Ubuntu 20.04 to make sure the evironment is clean.
  • Only used for the sole purpose of creating and managing critically sensitive information.
  • It doesn't need to be powerful. Just use your old laptop or get a cheap one.
Install Ubuntu on Air-gapped Computer
🔒 Create a bootable USB stick to install Ubuntu on:
⚠️ Some notices when installing:
  • Do NOT connect to wifi
  • Choose "Minimal installation"
  • Uncheck the box "Download updates while installing Ubuntu"
  • Enable Disk Encryption when installing:
  • After installation, turn Wifi OFF, and turn Airplane Mode ON:
How to Download and Upload Files with scp
⬇️ Download files from $NODE_HOME to local computer
scp -r -P SSH_port -i /path/to/id_rsa username@server_ip:~/cardano/file_to_download /path/to/Downloads/folder
💡 Tip: Drag and drop a file into terminal to get its path.
Example:
scp -r -P 1234 -i /Users/Charles/RSA/id_rsa charles@12.34.56.78:~/cardano/tx.raw /Users/Charles/Downloads
💡 Tip: Most of the times you just need to change file_to_download to download a file.
⬆️ Upload files from local computer to $NODE_HOME
scp -r -P SSH_port -i /path/to/id_rsa /path/to/file_to_upload username@server_ip:~/cardano
💡 Tip: Drag and drop a file into terminal to get its path.
Example:
scp -r -P 1234 -i /Users/Charles/RSA/id_rsa /Users/Charles/Uploads/file_to_upload charles@12.34.56.78:~/cardano
💡 Tip: If you put files in the same "Uploads" folder, most of the time you just need to change file_to_upload to upload a file.
Copy cardano-cli to Air-gapped Computer
💻 On local computer
  • Download cardano-cli from any node to local computer
scp -r -P SSH_port -i /path/to/id_rsa username@ip_of_node:.local/bin/cardano-cli /path/to/Downloads/folder
💡 Tip: Drag and drop a file into terminal to get its path.
Example:
scp -r -P 1234 -i /Users/Charles/RSA/id_rsa charles@12.34.56.78:.local/bin/cardano-cli /Users/Charles/Downloads
  • Save the customized command to a text file to use when you update cardano-cli later.
  • Copy cardano-cli to a USB stick (It's best to format the USB first)
🔒 ON YOUR AIR-GAPPED COMPUTER
💡 You may want to save this web page as a HTML file and copy it to your 🔒 Airgapped Computer so you can copy and paste the commands.
  • Open "Files" application
  • Go to "Home" folder (on the left column)
  • Create a folder named "cardano" (in lower case)
  • Copy cardano-cli from USB stick to "cardano" folder.
🔒 ON YOUR AIR-GAPPED COMPUTER
  • Open a new terminal window
📒 How to open terminal
  • Click on "Show Applications" (Bottom-left corner)
  • Type "terminal"
  • Click on the terminal app.
  • Verify cardano-cli is copied
ls $HOME/cardano
Output should contain the file cardano-cli
🔒 ON YOUR AIR-GAPPED COMPUTER
  • Give execution permission
sudo chmod +x $HOME/cardano/cardano-cli
🔒 ON YOUR AIR-GAPPED COMPUTER
  • Open .bashrc
sudo nano $HOME/.bashrc
  • Go to the bottom of the file and add the following lines
alias cardano-cli="$HOME/cardano/cardano-cli"
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Source .bashrc for changes to take effect
source $HOME/.bashrc
🔒 ON YOUR AIR-GAPPED COMPUTER
  • Test if cardano-cli can run
cardano-cli
Output should show cardano-cli help
cardano-cli - utility to support a variety of key operations (genesis
generation, migration, pretty-printing..) for different system generations.
...
About KES (Key Evolving Signature)
From official documentation:
To create an operational certificate for a block-producing node, you need a KES key pair.
Here “KES” stands for Key Evolving Signature, which means that after a certain period, the key will evolve to a new key and discard its old version. This is useful, because it means that even if an attacker compromises the key and gets access to the signing key, he can only use that to sign blocks from now on, but not blocks dating from earlier periods, making it impossible for the attacker to rewrite history.
A KES key can only evolve for a certain number of periods and becomes useless afterwards. This means that before that number of periods has passed, the node operator has to generate a new KES key pair, issue a new operational node certificate with that new key pair and restart the node with the new certificate.
Get KES Period
📒 Before continuing, you must wait for your nodes to sync to the most current block. If they are not, you won't be able to correctly calculate KES period. Compare your "block" number in Live View with the lastest block number at https://explorer.cardano.org
🔷 On Block-producing Node
  • Get current KES period
slotsPerKESPeriod=$(cat $NODE_HOME/mainnet-shelley-genesis.json | jq -r '.slotsPerKESPeriod') ;\
slotNo=$(cardano-cli query tip --mainnet | jq -r '.slot') ;\
kesPeriod=$((${slotNo} / ${slotsPerKESPeriod})) ;\
echo ;\
echo ● slotsPerKESPeriod: ${slotsPerKESPeriod} ;\
echo ● slotNo: ${slotNo} ;\
echo ● kesPeriod: ${kesPeriod}
  • Take note of the kesPeriod
Generate Keys & Certificates
🔴 CRITICAL SECURITY NOTICE 🔴
🔴 YOU ARE ABOUT TO GENERATE COLD KEYS.
🔴 MAKE SURE YOU ARE DOING IT ON YOUR 🔒 AIR-GAPPED COMPUTER.
🔴 Any file that has the word "cold" in its name should never leave your 🔒 Air-gapped computer unencrypted.
🔴 Exposing any of those keys to the internet can cause lost of fund.
🔴 If one of those keys ever exposed to the internet, it is of your best interest to just start over and create new keys.
🔒 On Air-gapped Computer
  • Generate cold keys and counter
mkdir $HOME/cardano/cold-keys
cardano-cli node key-gen \
    --cold-verification-key-file $HOME/cardano/cold-keys/cold-pool.vkey \
    --cold-signing-key-file $HOME/cardano/cold-keys/cold-pool.skey \
    --operational-certificate-issue-counter $HOME/cardano/cold-keys/cold-op-cert-issue.counter
🔒 On Air-gapped Computer
  • Create new payment key pair
cardano-cli address key-gen \
    --verification-key-file $HOME/cardano/cold-keys/cold-payment.vkey \
    --signing-key-file $HOME/cardano/cold-keys/cold-payment.skey
🔒 On Air-gapped Computer
  • Create a new stake key pair
cardano-cli stake-address key-gen \
    --verification-key-file $HOME/cardano/cold-keys/cold-stake.vkey \
    --signing-key-file $HOME/cardano/cold-keys/cold-stake.skey
🔒 On Air-gapped Computer
  • Create VRF key pair
cardano-cli node key-gen-VRF \
    --verification-key-file $HOME/cardano/cold-keys/cold-vrf.vkey \
    --signing-key-file $HOME/cardano/vrf.skey
🔒 On Air-gapped Computer
  • Create KES key pair
cardano-cli node key-gen-KES \
  --verification-key-file $HOME/cardano/cold-keys/cold-kes.vkey \
  --signing-key-file $HOME/cardano/kes.skey
🔒 On Air-gapped Computer
  • Encrypt
  1. cold-pool.vkey
  2. cold-pool.skey
  3. cold-op-cert-issue.counter
  4. cold-payment.vkey
  5. cold-payment.skey
  6. cold-stake.vkey
  7. cold-stake.skey
  8. cold-kes.vkey
  9. cold-vrf.vkey
(everything in the "cold-keys" folder).
  • Back up the encrypted versions on various places.
⚠️ NEVER LET THE UNENCRYPTED VERSIONS OF THE KEYS LEAVE YOUR AIR-GAPPED COMPUTER.
🔒 On Air-gapped Computer
Generate Operational Certificate
  • Replace kesPeriod with the value taken earlier.
cardano-cli node issue-op-cert \
    --kes-verification-key-file $HOME/cardano/cold-keys/cold-kes.vkey \
    --cold-signing-key-file $HOME/cardano/cold-keys/cold-pool.skey \
    --operational-certificate-issue-counter $HOME/cardano/cold-keys/cold-op-cert-issue.counter \
    --kes-period kesPeriod \
    --out-file $HOME/cardano/pool-operation.cert
🔒 On Air-gapped Computer
  • Create stake address based on cold-stake.vkey
cardano-cli stake-address build \
    --stake-verification-key-file $HOME/cardano/cold-keys/cold-stake.vkey \
    --out-file $HOME/cardano/stake.addr \
    --mainnet
This stake.addr is used to receive rewards of your pool. It cannot receive fund directly.
🔒 On Air-gapped Computer
  • Create payment address based on cold-payment.vkey, which stakes to cold-stake.vkey
cardano-cli address build \
    --payment-verification-key-file $HOME/cardano/cold-keys/cold-payment.vkey \
    --stake-verification-key-file $HOME/cardano/cold-keys/cold-stake.vkey \
    --out-file $HOME/cardano/payment-with-stake.addr \
    --mainnet
🔒 On Air-gapped Computer
  • Create stake certificate using cold-stake.vkey
cardano-cli stake-address registration-certificate \
    --stake-verification-key-file $HOME/cardano/cold-keys/cold-stake.vkey \
    --out-file $HOME/cardano/stake.cert
🔒 On Air-gapped Computer
  • Copy
  1. pool-operation.cert
  2. stake.addr
  3. payment-with-stake.addr
  4. stake.cert
  5. vrf.skey
  6. kes.skey
from cardano folder to your 💻 Local computer via a USB stick.
  • Back up all the keys on various places.
💻 On local computer
  • Upload pool-operation.cert to your 🔷 Block-producing Node
scp -r -P SSH_port -i /path/to/id_rsa /path/to/pool-operation.cert username@ip_of_block_producing_node:~/cardano
💡 Tip: Drag and drop a file into terminal to get its path.
Example:
scp -r -P 1234 -i /Users/Charles/RSA/id_rsa /Users/Charles/Uploads/pool-operation.cert charles@12.34.56.78:~/cardano
  • Save the customized command to a text file for later use.
💡 Tip: If you put other files in the same "Uploads" folder, just change pool-operation.cert to other files' names to upload them.
  • Upload
  1. stake.addr
  2. payment-with-stake.addr
  3. stake.cert
  4. kes.skey
  5. vrf.skey
to your 🔷 Block-producing Node
🔷 On Block-producing Node
  • Check if files has been uploaded
ls $NODE_HOME
Output should contains
  1. pool-operation.cert
  2. payment-with-stake.addr
  3. stake.addr
  4. stake.cert
  5. kes.skey
  6. vrf.skey
Update Startup Scripts
🔷 On Block-producing Node
  • Add kes.skey , vrf.skey , and pool-operation.cert to startCardanoNode.sh
cat > $NODE_HOME/startCardanoNode.sh << EOF
#!/bin/bash
$HOME/.local/bin/cardano-node +RTS -N -RTS run \
--topology ${NODE_TOPOLOGY} \
--database-path ${NODE_DB_PATH} \
--socket-path ${CARDANO_NODE_SOCKET_PATH} \
--host-addr ${NODE_IP} \
--port ${POOL_RELAY_PORT} \
--config ${NODE_CONFIG} \
--shelley-kes-key ${NODE_HOME}/kes.skey \
--shelley-vrf-key ${NODE_HOME}/vrf.skey \
--shelley-operational-certificate ${NODE_HOME}/pool-operation.cert
EOF
Adding these keys tells cardano-node that this is a block-producing node.
🔷 On Block-producing Node
  • Restart node
sudo systemctl restart cardano-node
or just type restartnode
🔷 On Block-producing Node
  • Run simpleLiveView to make sure the node is running
cd $NODE_HOME/simpleLiveView
./liveview.sh
or just type liveview
It may take a while for cardano-node to start syncing.
  • When the node starts syncing (Slot number increasing), press Ctrl-C to exit SimpleLiveView
Get Protocol Parameters
🔷 On Block-producing Node
  • Get protocol parameters into a file for later reference
cardano-cli query protocol-parameters \
    --mainnet \
    --out-file $NODE_HOME/params.json
Fund Your payment-with-stake.addr
You will need at least 505 ADA for pool deposit and transaction fees.
⚠️ SAFETY ALERT ⚠️
⚠️ You may want to send 1 ADA first to check if everything's ok before sending a large amount of ADA.
⚠️ You are dealing with REAL money. Read carefully and do things slowly.
🔷 On Block-producing Node
  • Get the address to transfer fund
echo $(cat $NODE_HOME/payment-with-stake.addr)
Address begins with "addr", for example:
addr1q8skk3gwmm0cunv...
  • Use your Daedalus/Yoroi wallet to transfer fund to your address.
🔷 On Block-producing Node
  • Check payment-with-stake.addr balance
cardano-cli query utxo \
    --address $(cat $NODE_HOME/payment-with-stake.addr) \
    --mainnet
⚠️ Please wait for your transaction to be included in a block to see your fund.
Output may look like this:
TxHash                  TxIx        Amount
------------------------------------------------
0b66...                 0           10000000 lovelace
The balance is displayed in lovelace (1 ADA = 1,000,000 lovelace). In this example, it is 10 ADA.
🔷 On Block-producing Node
  • Open .bashrc
sudo nano $HOME/.bashrc
Or just type editbash
  • Add this line to the end of file
alias mybalance="cardano-cli query utxo --address $(cat $NODE_HOME/payment-with-stake.addr) --mainnet"
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Source .bashrc for changes to take effect
source $HOME/.bashrc
Or just type sourcebash
💡 Now every time you want to check your balance, just type mybalance
Understanding UTxO and How Transactions Work
Before continuing, it's important for you to understand the UTxO model as it is the basis of how transactions work in the Cardano blockchain.

To build a transaction with cardano-cli, you'll need these basic options:

1. --tx-in
A list of all UTxO inputs. Each consists of UTxO ID and Index.
By running cardano-cli query utxo of an address, you will get a list of all UTxOs associated with that address, for example:
TxHash                       TxIx        Amount
----------------------------------------------------------
0b66..                       0           10000000 lovelace
0d99..                       1           100000000 lovelace
  • TxHash is transaction hash, which is also the UTxO ID
  • TxIx is transaction index
The UTxO list above will translate into these options. The # sign separates TxHash and TxIx.
--tx-in 0b66..#0 \
--tx-in 0b99..#1
2. --tx-out
A list of all transaction outputs, each consists of an address and the amount of fund for that address.
3. --invalid-hereafter
A slot in the future, after which, if the transaction has yet to be included in a block, it will be discarded.
4. --fee
The transaction fee.
Register Stake Address
🔷 On Block-producing Node
The commands below will query the UTxOs of your payment-with-stake.addr, process the output and turn it into a series of variables that will be used in transaction.
  • Copy all the command lines below and paste into terminal at the same time.
mv $NODE_HOME/tx.draft $NODE_HOME/tx-old.draft 2>/dev/null ;\
mv $NODE_HOME/tx.raw $NODE_HOME/tx-old.raw 2>/dev/null ;\
mv $NODE_HOME/tx.signed $NODE_HOME/tx-old.signed 2>/dev/null ;\
cd $NODE_HOME ;\
\
cardano-cli query utxo \
    --address $(cat $NODE_HOME/payment-with-stake.addr) \
    --mainnet > fullUtxo.out ;\
tail -n +3 fullUtxo.out | sort -k3 -nr > balance.out ;\
tx_in="" ;\
lovelace_total_balance=0 ;\
while read -r utxo; do
    in_addr=$(awk '{ print $1 }' <<< "${utxo}")
    idx=$(awk '{ print $2 }' <<< "${utxo}")
    utxo_balance=$(awk '{ print $3 }' <<< "${utxo}")
    lovelace_total_balance=$((${lovelace_total_balance}+${utxo_balance}))
    tx_in="${tx_in} --tx-in ${in_addr}#${idx}"
done < balance.out ;\
tx_in_count=$(cat balance.out | wc -l) ;\
currentSlot=$(cardano-cli query tip --mainnet | jq -r '.slot') ;\
invalidHereafter=$((${currentSlot} + 10000)) ;\
\
echo ;\
echo ✅ VERIFY THE INFORMATION BELOW: ;\
echo ● UTxOs List: ; \
cat balance.out ; \
echo ● Total Lovelace balance: ${lovelace_total_balance} ;\
echo ● Number of UTxOs: ${tx_in_count} ;\
echo ● Transaction Input: ${tx_in} ;\
echo ● Current Slot: $currentSlot ;\
echo ● Transaction Invalid Hereafter: $invalidHereafter ;\
\
rm fullUtxo.out ;\
rm balance.out
🔷 On Block-producing Node
  • Find the most current stakeAddressDeposit value
stakeAddressDeposit=$(cat $NODE_HOME/params.json | jq -r '.stakeAddressDeposit') ;\
echo ;\
echo ● stakeAddressDeposit: $stakeAddressDeposit
🔷 On Block-producing Node
  • Create transaction draft
cardano-cli transaction build-raw \
    ${tx_in} \
    --tx-out $(cat $NODE_HOME/payment-with-stake.addr)+0 \
    --invalid-hereafter ${invalidHereafter} \
    --fee 0 \
    --out-file $NODE_HOME/tx.draft \
    --certificate $NODE_HOME/stake.cert
🔷 On Block-producing Node
  • Calculate minimum transaction fee
fee=$(cardano-cli transaction calculate-min-fee \
    --tx-body-file $NODE_HOME/tx.draft \
    --tx-in-count ${tx_in_count} \
    --tx-out-count 1 \
    --mainnet \
    --witness-count 2 \
    --byron-witness-count 0 \
    --protocol-params-file $NODE_HOME/params.json | awk '{ print $1 }');\
echo ;\
echo ● fee: $fee
🔷 On Block-producing Node
  • Calculate change (current balance - deposit - fee)
tx_out_change=$((${lovelace_total_balance}-${stakeAddressDeposit}-${fee})) ;\
echo ;\
echo ● Change Output: ${tx_out_change}
🔷 On Block-producing Node
  • Build transaction with all available information
cardano-cli transaction build-raw \
    ${tx_in} \
    --tx-out $(cat $NODE_HOME/payment-with-stake.addr)+${tx_out_change} \
    --invalid-hereafter ${invalidHereafter} \
    --fee ${fee} \
    --certificate-file $NODE_HOME/stake.cert \
    --out-file $NODE_HOME/tx.raw
💻 On local computer
  • Download tx.raw from 🔷 Block-producing node
scp -r -P SSH_port -i /path/to/id_rsa username@ip_of_block_producing_node:~/cardano/tx.raw /path/to/Downloads/folder
💡 Tip: Drag and drop a file into terminal to get its path.
Example:
scp -r -P 1234 -i /Users/Charles/RSA/id_rsa charles@12.34.56.78:~/cardano/tx.raw /Users/Charles/Downloads
  • Save the customized command in a text file for later use.
💡 Most of the times you just change the file name to download other files.
💻 On local computer
  • Copy tx.raw to cardano folder on your 🔒 Air-gapped computer via a USB stick.
🔒 On Air-gapped Computer
  • Sign the transaction with cold keys
mv $HOME/cardano/tx.signed $HOME/cardano/tx-old.signed ;\
cardano-cli transaction sign \
    --tx-body-file $HOME/cardano/tx.raw \
    --signing-key-file $HOME/cardano/cold-keys/cold-payment.skey \
    --signing-key-file $HOME/cardano/cold-keys/cold-stake.skey \
    --mainnet \
    --out-file $HOME/cardano/tx.signed
  • Copy tx.signed to your 💻 Local computer
💻 On local computer
  • Upload tx.signed to cardano folder on 🔷 Block-producing node
🔷 On Block-producing Node
  • Check if tx.signed is uploaded
ls $NODE_HOME
Output should contain tx.signed
🔷 On Block-producing Node
  • Submit the signed transaction to the blockchain
cardano-cli transaction submit \
    --tx-file $NODE_HOME/tx.signed \
    --mainnet
🔷 On Block-producing Node
  • Check if transaction is confirmed by blockchain.
cardano-cli query utxo --address $(cat $NODE_HOME/payment-with-stake.addr) --mainnet ;\
echo ● Expected total balance: ${tx_out_change}
The balance of payment-with-stake.addr should equal to Expected total balance. If not, wait a few minute for the transaction to be included in a block then recheck.
Wait for your transaction to be confirmed by the blockchain to continue.
Prepare to Create Stake Pool Registration Certificate
In order to run the stake-pool registration-certificate command, you'll need to specify the following options:
--pool-pledge
--pool-cost
--pool-margin
--metadata-url
--metadata-hash
--pool-relay
We will go through each one.
Determine --pool-pledge
Pledge is your promise to delegate to your own pool.
  • Due to the a0 parameter, the higher your pledge, the higher the reward your pool (and your delegators) can get.
  • The balance of payment-with-stake.addr must be higher than the pledge amount. If it is lower, the promised is not fulfilled, your pool will not get any reward.
  • --pool-pledge is specified in lovelace (1 Ada = 1,000,000 lovelace).
Determine --pool-cost
Cost (or "fee") is the amount that you will take from your pool's reward after each epoch. This is meant to cover your operational costs.
  • It must be equal or higher than the minimum cost. To stay competitive, most pools just stick with the minimum value.
  • --pool-cost is specified in lovelace.
🔷 On Block-producing Node
  • Get minimum pool cost
minPoolCost=$(cat $NODE_HOME/params.json | jq -r .minPoolCost) ;\
echo ;\
echo --pool-cost ${minPoolCost}
Determine --pool-margin
After taking pool-cost from the reward, you will take another amount at a fixed rate, called pool-margin. This is meant to be your revenue for running the stake pool.
  • --pool-margin is specified by a number from 0 to 1, not from 0 to 100.
For example, --pool-margin 0.01 means you're taking 1%.
Prepare Metadata
A pool needs the following metadata:
Name
The full name of your pool.
Description
Max length 255 characters. Make it persuasive.
Ticker
3-5 characters, will be displayed prominently in wallets as the main way to distinguish stake pools.
Technically, a ticker is not unique, many pools can have the same ticker. However, off-chain systems called SMASH Metadata Management are used to manage tickers, probably on a "first come - first served" basis. So make sure your ticker has not already been used by searching for it in Daedalus and Yoroi wallets, and on websites like adapools.org or pooltool.io. More on that here.
Homepage
Optional, the website of your stake pool.
Extended metadata
Extra information about your pool like logo, social channels...
Optional but recommended. You can just create it and decide to use it or not later.
Create poolExtendedMetadata.json
💻 On local computer
  • Make neccessary change. You don't need to provide all information right now, can add more later.
  • Upload it to your website and get its URL. Or you can upload it to github instead.
How to Upload poolExtendedMetadata.json to Github
💻 On local computer
  • Create a new repository
  • Give it a short name and click "Create repository"
  • Click "creating a new file"
  • Set the name as poolExtendedMetadata.json
  • Copy and paste your poolExtendedMetadata.json content.
  • Click "Commit new file"
  • Click on "poolExtendedMetadata.json"
  • Click "Raw" button
  • Verify the json content and copy URL from address bar.
The URL looks like: https://raw.githubusercontent.com/...
The URL looks like: https://git.io/abcde
Create poolMetadata.json
🔷 On Block-producing Node
  • Fill in the correct information to create poolMetadata.json
cat > $NODE_HOME/poolMetadata.json << EOF
{
"name": "Cool Pool name",
"description": "Here's why you should stake with me",
"ticker": "3-5 CHARACTERS",
"homepage": "https://www.examplepooldomain.com Just leave blank if don't use",
"extended": "https://link/to/poolExtendedMetadata.json"
}
EOF
cat $NODE_HOME/poolMetadata.json
🔷 On Block-producing Node
  • Get the hash of poolMetadata.json
cardano-cli stake-pool metadata-hash \
  --pool-metadata-file $NODE_HOME/poolMetadata.json > $NODE_HOME/poolMetadataHash.txt ;\
echo ;\
echo ● Hash is: $(cat $NODE_HOME/poolMetadataHash.txt)
📒 The commands above will validate the correctness of your JSON file before hashing it.
📒 This hash will be registered on the blockchain. That means if you want to change metadata, you'll have to register your pool again.
Upload poolMetadata.json
💻 On local computer
  • Download poolMetadata.json to your 💻 Local computer.
  • Upload poolMetadata.json to your website and get its URL. The URL must be HTTPS and no longer than 64 characters.

Alternatively, you can upload it to GitHub

Do that just like you uploaded poolExtendedMetadata.json to github earlier.
⚠️ Because GitHub may modify the file content to optimize it, which in turn may change the hash of the file, you need to download it from GitHub to get the hash again.
🔷 On Block-producing Node
⚠️ Only do this if you choose to upload poolMetadata.json to GitHub
  • Download poolMetadata.json from GitHub
wget -O $NODE_HOME/poolMetadata.json https://git.io/abcde
  • Get the hash again
cardano-cli stake-pool metadata-hash \
  --pool-metadata-file $NODE_HOME/poolMetadata.json > $NODE_HOME/poolMetadataHash.txt ;\
echo ;\
echo ● Hash is: $(cat $NODE_HOME/poolMetadataHash.txt)
Four Ways to Register Your Relay Addresses
📒 Method 1: IP addresses
--pool-relay-ipv4 Relay_1_IPv4 \
--pool-relay-port 6000 \
--pool-relay-ipv4 Relay_2_IPv4 \
--pool-relay-port 6000 \
Just list out all the IPs of your relays.
PROS:
  • The simplest way
  • Relay addresses are separated from each other and can be optimized individually by the network. For example, if you have one relay in the US and another in Europe, other pools can choose a relay that is the nearest to them to connect.
  • One relay's downtime doesn't affect other relays' reputation.
CONS:
  • If you change relay's IP or add/remove relays:
    • You will have to register your stake pool again.
    • Other pools will have to update to your new relay's IP.
📒 Method 2: One subdomain for each relay
--single-host-pool-relay relay1.examplepooldomain.com\
--pool-relay-port 6000 \
--single-host-pool-relay relay2.examplepooldomain.com\
--pool-relay-port 6000 \
If you have a domain name for your stake pool, go to your domain's DNS setting and add an "A" record. The name of the record is your subdomain. The IPv4 of the record is your relay's static IPv4 address. Repeat for each relay.
PROS:
  • Relay addresses are separated from each other and can be optimized individually by the network. For example, if you have one relay in the US and another in Europe, other pools can choose a relay that is the nearest to them to connect.
  • One relay's downtime doesn't affect other relays' reputation.
  • When your relay's IP is changed, you'll just need to update the DNS record.
CONS:
  • If you add/remove relays, you will have to register your stake pool again.
📒 Method 3: One subdomain for all relays
--single-host-pool-relay relays.examplepooldomain.com \
--pool-relay-port 6000 \
This time, you create only one subdomain for all relays.
Depending on your domain registrar, you may be able to create a single "A" record and point to multiple IPs:
Or you may need to create multiple "A" records with the same name but each points to a different IP:
This is called "Round Robin". For example, if you point "relays" to IP1 and IP2, the first connection to relays.examplepooldomain.com will be pointed to IP1, the second will be pointed to IP2, the third will be pointed to IP1 again, and so on.
PROS:
  • When you change your relay's IP or add/remove relays, you'll just need to update your "A" record without having to register your stake pool again.
CONS:
  • IPs of all relays are group under one domain, making it impossible for the network to optimize the relays individually.
  • One relay's downtime does affect other relays' reputation.
Please note that despite having multiple relays, you still use the option --single-host-pool-relay because according to the --help command, the option --multi-host-pool-relay is used for SRV DNS.
📒 Method 4: SRV DNS record
--multi-host-pool-relay relays.examplepooldomain.com\
--pool-relay-port 6000 \
Basically it's the same as method 3 but with support for weights and priorities among multiple IPs (and other things). More on that.
If you use method 2, 3 or 4, remember to set the "Time to Live" (TTL) of your DNS records not too long so your changes will take effect soon enough. (1 hour is OK).
Create Pool Registration Certificate
💻 On local computer
  • Download poolMetadataHash.txt from 🔷 Block-producing node.
  • Copy poolMetadataHash.txt to folder "cardano" on 🔒 Air-gapped computer.
🔒 On Air-gapped Computer
  • Specify the right values for the first six options
cardano-cli stake-pool registration-certificate \
  --pool-pledge YOUR_PLEDGE_IN_LOVELACE \
  --pool-cost 340000000 \
  --pool-margin 0.01 \
  --single-host-pool-relay relays.examplepooldomain.com \
  --pool-relay-port 6000 \
  --metadata-url https://link/to/poolMetadata.json \
  \
  \
  --metadata-hash $(cat $HOME/cardano/poolMetadataHash.txt) \
  --cold-verification-key-file $HOME/cardano/cold-keys/cold-pool.vkey \
  --vrf-verification-
  key-file $HOME/cardano/cold-keys/cold-vrf.vkey \
  --pool-reward-account-verification-key-file $HOME/cardano/cold-keys/cold-stake.vkey \
  --pool-owner-stake-verification-key-file $HOME/cardano/cold-keys/cold-stake.vkey \
  --mainnet \
  --out-file $HOME/cardano/pool-registration.cert
cat $HOME/cardano/pool-registration.cert
pool-registration.cert should look like this
type: CertificateShelley
description: Stake Pool Registration Certificate
cborHex:
885e22d5d63...
Generate Delegation Certificate to Honor Your Pledge
If you want to connect all the dots, here's the explanation:
  • payment-with-stake.addr stakes to cold-stake.vkey
  • Just above you declared that cold-stake.vkey belongs to the owner of cold-pool.vkey (with the option --pool-owner-stake-verification-key-file)
  • Now you're going to delegate cold-stake.vkey to cold-pool.vkey by creating a delegation certificate.
  • All of that means you are delegating to your own pool, thus honoring your pledge.
🔒 On Air-gapped Computer
  • Create delegation certificate
cardano-cli stake-address delegation-certificate \
--stake-verification-key-file $HOME/cardano/cold-keys/cold-stake.vkey \
--cold-verification-key-file $HOME/cardano/cold-keys/cold-pool.vkey \
--out-file $HOME/cardano/delegation.cert
  • Copy delegation.cert and pool-registration.cert to 💻 Local computer and upload them to 🔷 Block-producing node.
🔷 On Block-producing Node
  • Get information for transaction. Copy all the command lines below and paste into terminal at the same time.
mv $NODE_HOME/tx.draft $NODE_HOME/tx-old.draft 2>/dev/null ;\
mv $NODE_HOME/tx.raw $NODE_HOME/tx-old.raw 2>/dev/null ;\
mv $NODE_HOME/tx.signed $NODE_HOME/tx-old.signed 2>/dev/null ;\
cd $NODE_HOME ;\
\
cardano-cli query utxo \
    --address $(cat $NODE_HOME/payment-with-stake.addr) \
    --mainnet > fullUtxo.out ;\
tail -n +3 fullUtxo.out | sort -k3 -nr > balance.out ;\
tx_in="" ;\
lovelace_total_balance=0 ;\
while read -r utxo; do
    in_addr=$(awk '{ print $1 }' <<< "${utxo}")
    idx=$(awk '{ print $2 }' <<< "${utxo}")
    utxo_balance=$(awk '{ print $3 }' <<< "${utxo}")
    lovelace_total_balance=$((${lovelace_total_balance}+${utxo_balance}))
    tx_in="${tx_in} --tx-in ${in_addr}#${idx}"
done < balance.out ;\
tx_in_count=$(cat balance.out | wc -l) ;\
currentSlot=$(cardano-cli query tip --mainnet | jq -r '.slot') ;\
invalidHereafter=$((${currentSlot} + 10000)) ;\
\
echo ;\
echo ✅ VERIFY THE INFORMATION BELOW: ;\
echo ● UTxOs List: ; \
cat balance.out ; \
echo ● Total Lovelace balance: ${lovelace_total_balance} ;\
echo ● Number of UTxOs: ${tx_in_count} ;\
echo ● Transaction Input: ${tx_in} ;\
echo ● Current Slot: $currentSlot ;\
echo ● Transaction Invalid Hereafter: $invalidHereafter ;\
\
rm fullUtxo.out ;\
rm balance.out
🔷 On Block-producing Node
  • Get current pool deposit amount
stakePoolDeposit=$(cat $NODE_HOME/params.json | jq -r '.stakePoolDeposit') ;\
tx_out_before_fee="$(cat $NODE_HOME/payment-with-stake.addr)+$(($lovelace_total_balance - $stakePoolDeposit))" ;\
echo ;\
echo ✅ VERIFY THE INFORMATION BELOW: ;\
echo ● Stake Pool Deposit: $stakePoolDeposit ;\
echo ● Transaction Output BEFORE Fee: ${tx_out_before_fee}
⚠️ Make sure your payment-with-stake.addr balance is greater than Stake Pool Deposit plus 5 Ada for fees.
🔷 On Block-producing Node
  • Build transaction draft
cardano-cli transaction build-raw \
  ${tx_in} \
  --tx-out ${tx_out_before_fee}  \
  --invalid-hereafter ${invalidHereafter} \
  --fee 0 \
  --certificate-file $NODE_HOME/pool-registration.cert \
  --certificate-file $NODE_HOME/delegation.cert \
  --out-file $NODE_HOME/tx.draft
🔷 On Block-producing Node
  • Calculate fee
fee=$(cardano-cli transaction calculate-min-fee \
  --tx-body-file $NODE_HOME/tx.draft \
  --tx-in-count ${tx_in_count} \
  --tx-out-count 1 \
  --witness-count 3 \
  --byron-witness-count 0 \
  --mainnet \
  --protocol-params-file $NODE_HOME/params.json | awk '{ print $1 }') ;\
tx_out_change=$(($lovelace_total_balance - $stakePoolDeposit - $fee)) ;\
tx_out_with_fee="$(cat $NODE_HOME/payment-with-stake.addr)+${tx_out_change}" ;\
echo ;\
echo ✅ VERIFY THE INFORMATION BELOW: ;\
echo ● fee: $fee ;\
echo ● Transaction Output Change: ${tx_out_change} ;\
echo ● Transaction Output WITH Fee: ${tx_out_with_fee}
🔷 On Block-producing Node
  • Build raw transaction
cardano-cli transaction build-raw \
  ${tx_in} \
  --tx-out ${tx_out_with_fee}  \
  --invalid-hereafter ${invalidHereafter} \
  --fee ${fee} \
  --certificate-file $NODE_HOME/pool-registration.cert \
  --certificate-file $NODE_HOME/delegation.cert \
  --out-file $NODE_HOME/tx.raw
  • Download tx.raw to 💻 Local computer and copy to 🔒 Air-gapped computer via a USB stick.
🔒 On Air-gapped Computer
  • Sign the transaction
mv $HOME/cardano/tx.signed $HOME/cardano/tx-old.signed ;\
cardano-cli transaction sign \
  --tx-body-file $HOME/cardano/tx.raw \
  --signing-key-file $HOME/cardano/cold-keys/cold-payment.skey \
  --signing-key-file $HOME/cardano/cold-keys/cold-pool.skey \
  --signing-key-file $HOME/cardano/cold-keys/cold-stake.skey \
  --mainnet \
  --out-file $HOME/cardano/tx.signed
🔒 On Air-gapped Computer
  • Get your stake pool ID
cardano-cli stake-pool id --cold-verification-key-file $HOME/cardano/cold-keys/cold-pool.vkey --output-format "hex" > $HOME/cardano/pool-id.txt
cat $HOME/cardano/pool-id.txt
  • Copy tx.signed & pool-id.txt to 💻 Local computer and upload to 🔷 Block-producing node.
🔷 On Block-producing Node
  • Submit transaction
cardano-cli transaction submit \
--tx-file $NODE_HOME/tx.signed \
--mainnet
🔷 On Block-producing Node
  • Check if transaction is confirmed by blockchain.
cardano-cli query utxo --address $(cat $NODE_HOME/payment-with-stake.addr) --mainnet ;\
echo ● Expected total balance: ${tx_out_change}
The balance of payment-with-stake.addr should equal to Expected total balance. If not, wait a few minute for the transaction to be included in a block then recheck.
Wait for your transaction to be confirmed by blockchain before continuing.
Now you can verify if the stake pool is included in the blockchain by searching for its ID or ticker on websites like adapools.org or pooltool.io
It may take some time for your pool to appear on those sites. In the meantime, let's continue.
Install Topology Updater
Before P2P network is launched, this is the best way to connect to other pools' relays.
❤️ Big thanks to Andrew Westberg (BCSH, Jor Manager) for creating this awesome tool.
✳️ On Relay Node
  • Create topologyUpdater script
cat > $NODE_HOME/topologyUpdater.sh << EOF
#!/bin/bash
# shellcheck disable=SC2086,SC2034
USERNAME=$(whoami)
CNODE_PORT=$POOL_RELAY_PORT
echo \${CNODE_PORT}
CNODE_HOSTNAME="CHANGE ME"
CNODE_BIN="\${HOME}/.local/bin"
CNODE_HOME=$NODE_HOME
CNODE_LOG_DIR="\${CNODE_HOME}/logs"
GENESIS_JSON="\${CNODE_HOME}/mainnet-shelley-genesis.json"
NETWORKID=\$(jq -r .networkId \$GENESIS_JSON)
CNODE_VALENCY=1  
NWMAGIC=\$(jq -r .networkMagic < \$GENESIS_JSON)
[[ "\${NETWORKID}" = "Mainnet" ]] && HASH_IDENTIFIER="--mainnet" || HASH_IDENTIFIER="--testnet-magic \${NWMAGIC}"
[[ "\${NWMAGIC}" = "1097911063" ]] && NETWORK_IDENTIFIER="--mainnet" || NETWORK_IDENTIFIER="--testnet-magic \${NWMAGIC}"
export PATH="\${CNODE_BIN}:\${PATH}"
export CARDANO_NODE_SOCKET_PATH="\${CNODE_HOME}/db/socket"
blockNo=\$($HOME/.local/bin/cardano-cli query tip \${NETWORK_IDENTIFIER} | jq -r .block )
if [ "\${CNODE_HOSTNAME}" != "CHANGE ME" ]; then
  T_HOSTNAME="&hostname=\${CNODE_HOSTNAME}"
else
  T_HOSTNAME=''
fi
if [ ! -d \${CNODE_LOG_DIR} ]; then
  mkdir -p \${CNODE_LOG_DIR};
fi
curl -s "https://api.clio.one/htopology/v1/?port=\${CNODE_PORT}&blockNo=\${blockNo}&valency=\${CNODE_VALENCY}&magic=\${NWMAGIC}\${T_HOSTNAME}" | tee -a \$CNODE_LOG_DIR/topologyUpdater_lastresult.json
EOF
✳️ On Relay Node
  • Give execution permission and run the script
chmod +x $NODE_HOME/topologyUpdater.sh
$NODE_HOME/./topologyUpdater.sh
Output should look like this
{ "resultcode": "201", "datetime":"...", "clientIp": "123.45.67.89", "iptype": 4, "msg": "nice to meet you" }
✳️ On Relay Node
  • Edit crontab
crontab -e
If you're asked to "Select an editor", just hit Enter/Return.
  • Look for the time and change 54 to your current minute (0-59). Then add this line to the end of file.
54 * * * * ${NODE_HOME}/topologyUpdater.sh
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
You've just told crontab to run topologyUpdater.sh exactly every hour.
  • Wait at least 4 hours for your relay to be registered on TopologyUpdater before continuing.
✳️ On Relay Node
  • Update mainnet-topology.json to include relays from other pools
BLOCK_PRODUCING_NODE_IP=$(cat $NODE_HOME/bp-node-ip.txt)
curl -s -o $NODE_HOME/mainnet-topology.json "https://api.clio.one/htopology/v1/fetch/?max=20&customPeers=${BLOCK_PRODUCING_NODE_IP}:${POOL_RELAY_PORT}:1|relays-new.cardano-mainnet.iohk.io:3001:2"
✳️ On Relay Node
  • Verify that Block-producing node's IPv4 and IOHK's relay are included in mainnet-topology.json, probably the first two entries.
cat $NODE_HOME/mainnet-topology.json
Or just type topo
✳️ On Relay Node
  • Restart relay node for the new mainnet-topology.json to take effect
sudo systemctl restart cardano-node
Install Grafana & Prometheus
You are about to install 3 tools
  • prometheus-node-exporter: Installed on all nodes to export hardware- and kernel-related metrics.
  • prometheus: Installed on one of your relay node as a monitoring server. It collects data from prometheus-node-exporter and cardano-node on all nodes.
  • grafana: Installed on the same relay node that has prometheus, providing visualization for the data.
📒 Only ONE relay node is needed to run the monitoring server.
We'll call it 📊✳️ Monitoring relay node.
📊✳️ On Monitoring Relay Node
  • Install prometheus and prometheus-node-exporter
sudo apt-get install -y prometheus prometheus-node-exporter
🔷 On Block-producing Node and other ✳️ Relay Nodes
  • Install prometheus-node-exporter
sudo apt-get install -y prometheus-node-exporter
📊✳️ On Monitoring Relay Node
  • Install grafana
wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install -y grafana
📊✳️ On Monitoring Relay Node
  • Make grafana, prometheus and prometheus-node-exporter auto start on server boot
sudo systemctl enable grafana-server
sudo systemctl enable prometheus
sudo systemctl enable prometheus-node-exporter
🔷 On Block-producing Node and other ✳️ Relay Nodes
  • Make prometheus-node-exporter auto start on server boot
sudo systemctl enable prometheus-node-exporter
📊✳️ On Monitoring Relay Node
  • Setup prometheus.yml
cat > prometheus.yml << EOF
global:
  scrape_interval: 10s
  external_labels:
    monitor: 'codelab-monitor'
scrape_configs:
  # Scrape data from cardano-node
  - job_name: 'cardano-node'
    static_configs:
      - targets: ['localhost:12798']
      - targets: ['$(cat $NODE_HOME/bp-node-ip.txt):12798']
      # Add more relay nodes here if needed
  # Scrape data from prometheus-node-exporter
  - job_name: 'node-exporter'
    static_configs:
      - targets: ['localhost:9100']
      - targets: ['$(cat $NODE_HOME/bp-node-ip.txt):9100']
      # Add more relay nodes here if needed
EOF
sudo mv prometheus.yml /etc/prometheus/prometheus.yml
📊✳️ On Monitoring Relay Node
  • Change Grafana's default port 3000
sudo nano /etc/grafana/grafana.ini
  • Look for the line ;http_port = 3000 (near the top).
  • Remove the ; character to uncomment it.
  • Change 3000 to a random 4-digit number only you know.
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
📊✳️ On Monitoring Relay Node
  • Restart all services for changes to take effect
sudo systemctl restart grafana-server
sudo systemctl restart prometheus
sudo systemctl restart prometheus-node-exporter
📊✳️ On Monitoring Relay Node
  • Check if the services are running
sudo systemctl status grafana-server prometheus prometheus-node-exporter
Output should look like this
🟢 grafana-server.service - Grafana instance
     Loaded: loaded (/usr/lib/systemd/system/grafana-server.service; enabled; vendor pres>
     Active: active (running)
...
🟢 prometheus.service - Monitoring system and time series database
     Loaded: loaded (/lib/systemd/system/prometheus.service; enabled; vendor preset: enab>
     Active: active (running)
...
🟢 prometheus-node-exporter.service - Prometheus exporter for machine metrics
     Loaded: loaded (/lib/systemd/system/prometheus-node-exporter.service; enabled; vendo>
     Active: active (running)
  • Press ctrl-C to exit
📊✳️ On Monitoring Relay Node
  • Open Grafana port so you can access it from local computer
  • Replace grafana_port with the Grafana port you've just set earlier.
sudo ufw allow grafana_port/tcp &&\
sudo ufw status numbered
Output should look like this
Status: active
     To                         Action      From
     --                         ------      ----
[ 1] SSH_port/tcp               ALLOW IN    Anywhere
[ 2] POOL_RELAY_PORT/tcp        ALLOW IN    Anywhere
[ 3] grafana_port/tcp           ALLOW IN    Anywhere
🔷 On Block-producing Node and other ✳️ Relay Nodes
Open port 12798 for cardano-node and 9100 for prometheus-node-exporter so Prometheus on monitoring relay node can scrape metric data from them.
  • Replace 1.2.3.4 with 📊✳️ Monitoring relay node's IPv4 so only it can access those ports.
MONITORING_RELAY_NODE_IP=1.2.3.4
sudo ufw allow proto tcp from $MONITORING_RELAY_NODE_IP to any port 12798 &&\
sudo ufw allow proto tcp from $MONITORING_RELAY_NODE_IP to any port 9100 &&\
sudo ufw status numbered
Output should look like this
Status: active
     To                   Action      From
     --                   ------      ----
[ 1] POOL_RELAY_PORT/tcp  ALLOW IN    RELAY_NODE_IP
[ 2] SSH_port/tcp         ALLOW IN    Anywhere
[ 3] 12798/tcp            ALLOW IN    MONITORING_RELAY_NODE_IP
[ 4] 9100/tcp             ALLOW IN    MONITORING_RELAY_NODE_IP
🔷 On Block-producing Node and other ✳️ Relay Nodes
  • Change Prometheus address so Monitoring relay node can scrape data from cardano-node.
sed -i $NODE_HOME/mainnet-config.json -e "s/127.0.0.1/0.0.0.0/g"  
🔷✳️ On All Nodes
  • Restart cardano-node for changes to take effect
sudo systemctl restart cardano-node
Setup Grafana
💻 On local computer
  • Open web browser and enter this address: Monitoring_relay_node_ipv4:grafana_port
Example: 123.45.67.89:3000
  • Login with username: admin, password: admin
  • Change password
  • Click "Add your first data source"
  • Select Prometheus
  • Rename it to "prometheus" (lowercase)
  • Set URL to "http://localhost:9090"
  • Click "Save & Test"
  • On the left column, hover on Dashboard menu, then click "Manage".
  • Click "Import"
  • Paste the json content you've just copied into "Import via panel json".
  • Click "Load"
After a while, you'll begin to see data coming in.
Setup Telegram to Receive Important Notifications
  • Install Telegram app on your phone and create an account if you haven't.
  • In the "Chats" tab, pull down and search "BotFather", choose the bot with the blue check mark.
  • Tap "/start", then tap "/newbot"
  • Choose a name for your bot, for example "Server Noti Bot"
  • Choose a unique username for your bot, ending with "bot"
  • Tap on the "t.me/username_bot" that appear.
  • Tap "/start" in the new chat screen, this will create the chat ID.
  • Go back to the BotFather chat screen, copy the HTTP API token.
  • Replace YOUR_API_TOKEN in the link below with the token you've just copied and open it in browser:
https://api.telegram.org/botYOUR_API_TOKEN/getUpdates
  • Find the "chat":{"id":123456789... part and get the chat ID.
  • Save your API token and chat ID, you'll need them later.
Setup Grafana to Send Alerts to Telegram
📊✳️ On Monitoring Relay Node
  • Install grafana-image-renderer so Grafana can attach graph images with telegram alerts.
sudo grafana-cli plugins install grafana-image-renderer
Output:
✔ Installed grafana-image-renderer successfully
  • Restart grafana server
sudo systemctl restart grafana-server
💻 On local computer
  • Go to your Grafana Dashboard
  • Hover on the bell icon on the left column and click "Notification Channels"
  • Click "Add channel"
  • Enter name and choose type as "Telegram"
  • Enter API Token and Chat ID saved earlier.
  • Click on "Notification Settings", check the "Default" box.
  • Click "Test", you'll receive a test message on Telegram
  • Click "Save".
  • On your dashboard, click on "TIME SYNC OFFSET" and choose "Edit"
  • Click on the "Alert" tab and set values as in the image below
  • Click "Save"
  • Repeat for any metric you want to receive alerts.
📒 At the time of writing, Grafana only supports alerts for "Graph" type.
📒 Grafana alerts run even when you don't open Grafana in your browser.
Install OSSEC
OSSEC is a powerful, indispensable tool for a secure server. It provides Host-based Intrusion Detection System (HIDS), log monitoring and more.
Here you will setup OSSEC to send important notifications to Telegram.
Do this 🔷✳️ On All Nodes
  • Install dependencies
sudo apt install build-essential make zlib1g-dev libpcre2-dev libevent-dev libssl-dev -y
  • Download OSSEC
cd $HOME/src
git clone https://github.com/ossec/ossec-hids.git
cd ossec-hids
git checkout 3.6.0
  • Install OSSEC
sudo ./install.sh
Output:
(en/br/cn/de/el/es/fr/hu/it/jp/nl/pl/ru/sr/tr) [en]:
  • Press Enter/Return to use English
Output:
You are about to start the installation process of the OSSEC HIDS.
You must have a C compiler pre-installed in your system.
...
  -- Press ENTER to continue or Ctrl-C to abort. --
  • Press Enter/Return to continue
Output:
1- What kind of installation do you want (server, agent, local, hybrid or help)?
  • Type "local" then Enter/Return
Output:
2- Setting up the installation environment.
- Choose where to install the OSSEC HIDS [/var/ossec]:
  • Press Enter/Return to use default location.
Output:
3- Configuring the OSSEC HIDS.
  3.1- Do you want e-mail notification? (y/n) [y]:
  • Type "n" then Enter/Return to disable email notifications. We will use Telegram notification instead.
Output:
3.2- Do you want to run the integrity check daemon? (y/n) [y]:
  • Press Enter/Return to enable integrity check daemon
Output:
3.3- Do you want to run the rootkit detection engine? (y/n) [y]:
  • Press Enter/Return to enable rootkit detection engine'
Output:
3.4- Active response allows you to execute a specific
       command based on the events received. For example,
       you can block an IP address or disable access for
       a specific user.  
       More information at:
       http://www.ossec.net/en/manual.html#active-response
   - Do you want to enable active response? (y/n) [y]:
  • Press Enter/Return to enable active response
Output:
   - By default, we can enable the host-deny and the
     firewall-drop responses. The first one will add
     a host to the /etc/hosts.deny and the second one
     will block the host on iptables (if linux) or on
     ipfilter (if Solaris, FreeBSD or NetBSD).
   - They can be used to stop SSHD brute force scans,
     portscans and some other forms of attacks. You can
     also add them to block on snort events, for example.
   - Do you want to enable the firewall-drop response? (y/n) [y]:
  • Press Enter/Return to enable firewall-drop response
Output:
   - Do you want to add more IPs to the white list? (y/n)? [n]:
  • If your local computer has a static IP, maybe you'll want to add it the the whitelist. If not, just press Enter/Return.
Output:
3.6- Setting the configuration to analyze the following logs:
    -- /var/log/auth.log
    -- /var/log/syslog
    -- /var/log/dpkg.log
- If you want to monitor any other file, just change
   the ossec.conf and add a new localfile entry.
   Any questions about the configuration can be answered
   by visiting us online at http://www.ossec.net .
   --- Press ENTER to continue ---
  • Press Enter/Return. The installation will begin.
Output:
- System is Debian (Ubuntu or derivative).
- Init script modified to start OSSEC HIDS during boot.
- Configuration finished properly.
- To start OSSEC HIDS:
      /var/ossec/bin/ossec-control start
- To stop OSSEC HIDS:
      /var/ossec/bin/ossec-control stop
- The configuration can be viewed or modified at /var/ossec/etc/ossec.conf
    Thanks for using the OSSEC HIDS.
    If you have any question, suggestion or if you find any bug,
    contact us at https://github.com/ossec/ossec-hids or using
    our public maillist at
    https://groups.google.com/forum/#!forum/ossec-list
    More information can be found at http://www.ossec.net
    ---  Press ENTER to finish (maybe more information below). ---
  • Press Enter/Return.
  • Create a systemd service to auto start OSSEC
cat > $HOME/ossec.service <<EOF
[Unit]
Description=OSSEC service
[Service]
Type=forking
ExecStart=/var/ossec/bin/ossec-control start
ExecStop=/var/ossec/bin/ossec-control stop
[Install]
WantedBy=multi-user.target
EOF
sudo mv $HOME/ossec.service /etc/systemd/system
sudo chmod 644 /etc/systemd/system/ossec.service
sudo systemctl daemon-reload
sudo systemctl enable ossec
  • Create a script to send Active Response notifications to your telegram bot
sudo nano /var/ossec/active-response/bin/ossec-telegram.sh
  • Paste this to nano editor
  • Replace telegram_bot_token and telegram_bot_chat_id with the values you created earlier.
#!/bin/sh
# Author: Yevgeniy Goncharov aka xck, http://sys-adm.in
# Send alert to Telegram fromm OSSEC
# Sys env / paths / etc
# -------------------------------\
PATH=$PATH:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
# Telegram settings
TOKEN="telegram_bot_token"
CHAT_ID="telegram_bot_chat_id"
ACTION=$1
USER=$2
IP=$3
ALERTID=$4
RULEID=$5
LOCAL=`dirname $0`;
cd $LOCAL
cd ../
PWD=`pwd`
# Logging the call
echo "`date` $0 $1 $2 $3 $4 $5 $6 $7 $8" >> ${PWD}/../logs/active-responses.log
# Getting alert time
ALERTTIME=`echo "$ALERTID" | cut -d  "." -f 1`
# Getting end of alert
ALERTLAST=`echo "$ALERTID" | cut -d  "." -f 2`
# Getting full alert
ALERT=`grep -A 5 "$ALERTTIME" ${PWD}/../logs/alerts/alerts.log | grep -v ".$ALERTLAST: " -A 5`
curl -s \
-X POST \
https://api.telegram.org/bot$TOKEN/sendMessage \
-d text="$ALERT" \
-d chat_id=$CHAT_ID
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Give execution permission
sudo chmod +x /var/ossec/active-response/bin/ossec-telegram.sh
  • Edit OSSEC Configuration
sudo nano /var/ossec/etc/ossec.conf
  • Add this before the <!-- Active Response Config --> line
  <command>
    <name>send-telegram</name>
    <executable>ossec-telegram.sh</executable>
    <expect></expect>
  </command>
  <active-response>
    <command>send-telegram</command>
    <location>local</location>
    <level>4</level>
  </active-response>
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
Some rules need to be disabled to avoid being spammed:
  • Rule 1003 (syslog message too large) caused by cardano-node.
  • Rule 531 (partition usage 100%) caused by loop device.
  • Edit local rules
sudo nano /var/ossec/rules/local_rules.xml
  • Add this before the line </group> <!-- SYSLOG,LOCAL -->
<rule id="400001" level="0">
    <if_sid>1003</if_sid>
    <description>ignore this message</description>
</rule>
<rule id="400002" level="0">
    <if_sid>531</if_sid>
    <match>/dev/loop</match>
    <description>ignore this message</description>
</rule>
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
  • Start OSSEC
sudo systemctl start ossec
  • Test if telegram notifications is working
crontab -e
Don't make any change. After a short while, you will receive a message in Telegram:
"Rule: 2834 (level 5) ➔ 'Crontab opened for editing.'"
  • Press Ctrl-X to exit.
Get Alerts When Server is Down
A "Check" is already created for you.
Each "Check" is used for ONE server.
  • Click on "My First Check" and rename it to one of your server's name.
  • Click on "1 day 1 hour", set period to 10 minutes and grace to 5 minutes.
  • Click Save.
  • Click on "Integration" tab
  • Add all the notification channels you want and follow the instructions for each channel.
  • Go back to the "Check" tab and copy the Ping URL
  • Edit crontab
crontab -e
  • Add this line to the end, replacing ping_url with the url you've just copied.
*/10 * * * * curl -fsS --retry 5 -o /dev/null ping_url
This cronjob will ping the url every 10 minutes.
  • Press Ctrl-X to exit.
  • Press Y then Enter/Return to confirm saving.
Output:
crontab: installing new crontab
After a while, the "Last Ping" column will show "x minutes ago".
  • Create a new "Check" and repeat for each server.
Pool Setup Checklist

✅ Checkpoint 1 🔴 Critical

  • In Grafana, the number of transactions on all nodes must always be increasing.
Please note that topologyUpdater takes 4 hours to register your relays, then it takes some more time for other pools to include you in their mainnet-topology.json. After that, your Transactions number will start increasing.

✅ Checkpoint 2 🔴 Critical

🔷 On Block-producing Node
  • Check for files with the word cold in $NODE_HOME and $HOME folder
ls $NODE_HOME | grep 'cold'
ls $HOME | grep 'cold'
  • Make sure there is NO file with the word "cold", especially these files:
  1. cold-payment.skey
  2. cold-payment.vkey
  3. cold-stake.skey
  4. cold-stake.vkey
⛔️ Exposing any of these keys to the internet can cause lost of fund.
⚠️ If one of those keys ever exposed to the internet, it is of your best interest to just start over and generate new keys.

✅ Checkpoint 3 🔴 Critical

✳️ On Relay Node
  • Check for files with extensions .vkey, .skey, or .cert in $NODE_HOME and $HOME folder
ls $NODE_HOME | grep '.vkey\|.skey\|.cert'
ls $HOME | grep '.vkey\|.skey\|.cert'
  • Make sure there is no file with extensions .vkey, .skey, or .cert in it.

✅ Checkpoint 4 🔴 Critical

🔷 On Block-producing Node
  • Simple Live View must show that you're running in block-producing mode.
+---------------------+----------------+
|  RUNNING IN BLOCK PRODUCER MODE! :)  |
+---------------------+----------------+

✅ Checkpoint 5

  • Go to https://pool.vet and paste your pool ID or ticker to scan for common problems. If you've just registered your pool, please wait a few hours for pool.vet to update their database.

✅ Checkpoint 6

  • Make sure your pool's ticker appears in Daedalus and Yoroi wallets. This is important to get delegators. It may take a few days for your pool to appear in those wallets.
Important Metrics to Watch in Grafana
1. KES RENEWAL days left.
You'll need to renew KES keys before the expiry period or your forged blocks will be invalid.
2. TRANSACTIONS must always be increasing.
If it's flat, check your IN peers and topologyUpdater.
3. MISSED SLOTS should show "No Data" or at least not increasing.
Missed slots are slots that your block-producing node cannot check if it is a leader or not. If you are sheduled to lead those slots, you will lose your block.
There are many reasons for missing slots. Compare the missed slots' time against other metrics' timeline to check for correlations.
🎉🥳 Congratulations
You've just finish making a secure stake pool. I wish you great success. Let's say hi on Telegram at @staypool, I'd love to hear from you.
❤️ Donate to my address: addr1qyxd2rpa3fwxpj739zcddwac5knke4s2casn0czfr32v5zf6g5qmpfv40fal70xtuqc4wx4h708h26l78eajgmhuvntqhkhr4z