Set up a Cardano staking pool

From Organic Design wiki
Cone.png This article or section is a stub. Stubs are articles that have not yet received substantial attention from the authors. They are short or insufficient pieces of information and require additions to further increase the article's usefulness. The project values stubs as useful first steps toward complete articles.


The official documentation for setting up a node and configuring it as a stake pool on the mainnet is mostly very clear and complete. I was able to get my node compiled, installed and running pretty easily. But configuring the node to run as a stake pool is more complicated and the documentation seems to have some missing bits, so I'm documenting here just the specific parts that I had trouble with.

Prerequisites

You will need to have two servers (but three service is ideal so you can run two relays so one can be restarted to update topology without interrupting the service) and one offline machine, all running the node, so the first part of the procedure for compiling, installing and running is done on all three of the machines.

Procedure

First ensure that cardano-node is installed on the pool server, the relay server and a local machine (didn't have much luck with docker images).

Install cardano-addresses on the local machine.

Create a file called generate-keys.sh on the local machine with these contents (Note, the first line generates a phrase - remove it if you want to use an existing one.):

# Remove this line if you want to use an existing phrase stored in phrase.prv
cardano-address recovery-phrase generate > phrase.prv

# generate root keys
cat phrase.prv | cardano-address key from-recovery-phrase Shelley > rootkey.prv

cat rootkey.prv | cardano-address key public --with-chain-code > rootkey.pub
cat rootkey.prv | cardano-address key child 1852H/1815H/0H/0/0 > addr.prv
cat addr.prv | cardano-address key public --with-chain-code | cardano-address address payment --network-tag mainnet > payment.addr
cat rootkey.prv | cardano-address key child 1852H/1815H/0H/2/0 > stake.prv
cat stake.prv | cardano-address key public --with-chain-code | cardano-address address stake --network-tag mainnet > stake.addr


# Create payment and stake vkey/skey files

cardano-cli key convert-cardano-address-key --signing-key-file stake.prv --shelley-stake-key --out-file ShelleyStake.skey
cardano-cli key convert-cardano-address-key --signing-key-file addr.prv --shelley-payment-key --out-file ShelleyPayment.skey

cardano-cli key verification-key --signing-key-file ShelleyStake.skey --verification-key-file Ext_ShelleyStake.vkey
cardano-cli key verification-key --signing-key-file ShelleyPayment.skey --verification-key-file Ext_ShelleyPayment.vkey

cardano-cli key non-extended-key --extended-verification-key-file Ext_ShelleyStake.vkey --verification-key-file ShelleyStake.vkey
cardano-cli key non-extended-key --extended-verification-key-file Ext_ShelleyPayment.vkey --verification-key-file ShelleyPayment.vkey

cardano-cli stake-address build --stake-verification-key-file ShelleyStake.vkey --out-file ShelleyStake.addr --mainnet
cardano-cli address build --payment-verification-key-file ShelleyPayment.vkey  --stake-verification-key-file ShelleyStake.vkey --out-file ShelleyPayment.addr --mainnet

# Nicer formatting
mkdir out

cp ShelleyPayment.addr out/payment.addr
cp ShelleyPayment.skey out/payment.skey
cp ShelleyPayment.vkey out/payment.vkey

cp ShelleyStake.addr out/stake.addr
cp ShelleyStake.skey out/stake.skey
cp ShelleyStake.vkey out/stake.vkey

Once you have that script ready disconnect from the internet and insert a fresh usb create a directory on it called wallet-keys and go into that directory in the terminal and run the script (script will work in your current working directory).
The script will have generated the keys you want int the out/ directory, copy the two .addr files onto your machine since these are not secret, and remove the drive.

On the server run:

cardano-cli query protocol-parameters \
  --mainnet \
  --out-file protocol.json


Send the keyDeposit amount (2 Ada at time of writing) as defined in the protocol.json and 1.5 Ada (1 for minimum wallet amount and 0.5 for transaction fees) to the payment address contained in payment.addr that you copied of the usb.

You should then continue as per the documentation page 4 but on step 5 generate the pool keys offline on the usb. Note: you will need to run any queries on the server (which has the node running) and formulate the transactions locally - disconnecting from the net, generating certs and signing transactions - then copy the transaction to the server to get fee size and actually send the transaction.

Skip step 6. Key Evolving Signature and KES period as it is redundant because it is just reiterating over the commands done in step 5.

Note on step 8 you can add the pledge balance to the address at a later time but your node will not produce blocks until the pledge is met. Note: for some reason the pre-calculated fee is different than what it should be - you just have to fail and use the fee that it says you should have used. If you want to use DNS use --single-host-pool-relay instead of --pool-relay-ipv4.

Once you hit step 9. Start your nodes copy kes.skey and vrf.skey from the offline usb to the computer and upload them to the server when you reconnect.

I recommend to write some startup scripts (for the block producing server, the relay and Prometheus) that log to a file like this:

#!/bin/sh
/root/.local/bin/cardano-node run \
--topology /root/config/mainnet-topology.json \
--database-path /root/data \
--socket-path /root/data/node.socket \
--host-addr <IP> \
--port <PORT> \
--config /root/config/mainnet-config.json \
--shelley-kes-key /root/keys/kes.skey \
--shelley-vrf-key /root/keys/vrf.skey \
>> /root/debug.log

And a corresponding systemd service:

# /etc/systemd/system/cardano.service
########################################
[Unit]
Description=Starts the cardano node.
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
ExecStart=/root/start.sh
Restart=on-failure
RestartSec=5
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

Start with service cardano start
Start on boot with systemctl enable cardano
I would recommend changing minSeverity in mainnet-config.json to Warning for efficiency and log readability.

Testnet Procedure

Everything went smoothly until I got to Register stake address on the blockchain. The main issue is that you need to have some balance in your payment.addr address, and the previous step of creating a sample transaction is not optional, things in that step such as protocol.json and the output hash are referred to in this step.

The Key Evolving Signature and KES period section is redundant because it is done already in the Generate your stake pool keys section, but it is explained better in the later one.

The Configure topology files for block-producing and relay nodes section requires that we have a relay node IP address.

Relay Quickstart

Copy the binaries to the relay in /root/.local/bin/.

cd ~

# packages
apt-get update
apt-get install -y jq curl libtool git automake build-essential bc man-db -y

# Libsodium
mkdir src
cd src
git clone https://github.com/input-output-hk/libsodium
cd libsodium
git checkout 66f017f1
./autogen.sh
./configure
make
make install
cd ~

# setup paths
echo "export PATH=\"/root/.local/bin:/root/.local/bin/cardano-node:/root/.local/bin/cardano-cli:$PATH\"" >> .bashrc
echo "export CARDANO_NODE_SOCKET_PATH=/root/data/node.socket" >> .bashrc
echo "export LD_LIBRARY_PATH=\"/usr/local/lib:$LD_LIBRARY_PATH\"" >> .bashrc
echo "export PKG_CONFIG_PATH=\"/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH\"" >> .bashrc
source .bashrc

# get config
mkdir config
cd config
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-topology.json
cd ~

# topology updater
mkdir scripts
cd scripts
wget https://raw.githubusercontent.com/cardano-community/guild-operators/alpha/scripts/cnode-helper-scripts/env
wget https://raw.githubusercontent.com/cardano-community/guild-operators/alpha/scripts/cnode-helper-scripts/topologyUpdater.sh
wget https://raw.githubusercontent.com/cardano-community/guild-operators/master/scripts/cnode-helper-scripts/gLiveView.sh

chmod +x topologyUpdater.sh
chmod +x gLiveView.sh
cd ~

# startup script
echo "#!/bin/sh

export PATH=\"/root/.local/bin:/root/.local/bin/cardano-node:/root/.local/bin/cardano-cli:$PATH\"
export CARDANO_NODE_SOCKET_PATH=/root/data/node.socket
export LD_LIBRARY_PATH=\"/usr/local/lib:$LD_LIBRARY_PATH\"
export PKG_CONFIG_PATH=\"/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH\"

cardano-node run \
--topology /root/config/mainnet-topology.json \
--database-path /root/data \
--socket-path /root/data/node.socket \
--host-addr 0.0.0.0 \
--port 3002 \
--config /root/config/mainnet-config.json \
>> /root/debug.log" > /root/start.sh

chmod +x start.sh

# Restart script
echo "#!/bin/sh

/bin/systemctl restart cardano.service" > /root/reboot-relay.sh

chmod +x /root/reboot-relay.sh

# systemd script
echo "
# /etc/systemd/system/cardano.service
########################################
[Unit]
Description=Starts the cardano node.
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
ExecStart=/root/start.sh
Restart=on-failure
RestartSec=5
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/cardano.service

# Set to start on boot.
systemctl enable cardano

Remember to edit the Prometheus IP in /root/config/mainnet-config.json to 0.0.0.0
Add relays to /root/config/mainet-topology.json
Edit your static IP address in /root/start.sh

Edit the /root/scripts/env file to have the following configuration:

CCLI="/root/.local/bin/cardano-cli"
CNODE_PORT=3002
CONFIG="/root/config/mainnet-config.json"
TOPOLOGY="/root/config/mainnet-topology.json"

Edit the /root/scripts/topologyUpdater.sh file to include your block producing node's ip in CUSTOM_PEERS (Don't change the CNODE_HOSTNAME variable to an ip address.) Also set BATCH_AUTO_UPDATE=Y
Test the topology updater script runs by running it: /root/scripts/topologyUpdater.sh
Add it as a cron job crontab -e

0 * * * * /root/scripts/topologyUpdater.sh >> /root/debug-topology-updater.log
30 0 * * * /root/reboot-relay.sh >> /root/debug.log

Note change the second 0 on the reboot-relay script to what hour of the day you want it to run - best space out the relay restarts as far as possible from the other relays, we run it on the half hour so that topology updater does not try to query it when it is restarting.
You will need wait until the second time it can execute the topology updater (1 hour after the first time) for it to update the providers.


Run /root/scripts/gLiveView.sh to get information on how the relay is running.


Once you are ready run service cardano start to start the relay.

Stopping

The Cardano node should always be stoped with the SIGINT signal (e.g. kill -SIGINT <PROCESS>) otherwise it might not shutdown cleanly, it is designed to recover after a messy shutdown but next startup might be 30-40 minutes instead of the usual 5 minutes.

Updating Pool Registration

Pool registrations can be updated by simply sending a new pool registration certificate - no need to de-register! Just follow the same process but don't worry about the pool deposit or adding a delegation certificate.

Delegation (Offline)

If you want to delegate to a pool without being required to be online first ensure that you have access to your payment/stake keys (see script under procedure).

Generate your stake address registration certificate and your delegation certificate:

cardano-cli stake-address registration-certificate \
	--stake-verification-key-file stake.vkey \
	--out-file stake.cert

# Delegate to PUDIM! (or use the pool id of the pool you wish to delegate to)
cardano-cli stake-address delegation-certificate \
	--stake-verification-key-file stake.vkey \
	--stake-pool-id pool1ujcu3myfg9wwvdyh2ks653954lamtufy3lefjs73jrr327q53j4 \
	--out-file delegation.cert

Then generate a raw transaction to calculate fees:

cardano-cli transaction build-raw \
	--tx-in <TxHash>#<TxIx> \
	--tx-out <PAYMENT ADDRESS>+0 \
	--invalid-hereafter 0 \
	--fee 0 \
	--out-file tx.raw \
	--certificate-file stake.cert \
	--certificate-file delegation.cert

Upload tx.raw to an online machine that has cardano-node running to calculate fees.
Once you know how much fees will be calculate your change using BALANCE - KEYDEPOSIT - FEES (see procedure if you don't know what pool deposit is - probably 2 Ada).
The build the transaction for real - note the order of certificate is important!

cardano-cli transaction build-raw \
	--tx-in <TxHash>#<TxIx> \
	--tx-out <PAYMENT ADDRESS>+<CHANGE> \
	--invalid-hereafter <SLOT NO> \
	--fee <FEE> \
	--out-file tx.raw \
	--certificate-file stake.cert \
	--certificate-file delegation.cert

Sign it:

cardano-cli transaction sign \
	--tx-body-file tx.raw \
	--signing-key-file payment.skey \
	--signing-key-file stake.skey \
	--mainnet \
	--out-file tx.signed

Upload the signed transaction tx.signed to your online machine and send it!

cardano-cli transaction submit \
	--tx-file tx.signed \
	--mainnet

De-registering Certificates

To register the stake key you create a registration certificate which requires a key deposit, to get that deposit back follow the same steps as registering a certificate but use deregistration-certificate instead of registration-certificate and add the key deposit to the change address.

Prometheus

The documentation has pretty good information on how to get this running but I had to change the ip (under Prometheus) in mainnet-config.json to 0.0.0.0.
You may want to setup Prometheus on two different servers for redundancy so if something goes wrong with one you can still get information from the other.

You need to set the data directory otherwise it defaults to ~/data what cardano-node is using --storage.tsdb.path /root/prometheus/data

My prometheus.yml config uses this config:

scrape_configs:
  - job_name: 'cardano' # To scrape data from the block producing node
    scrape_interval: 5s
    static_configs:
      - targets: ['<Block Producing Node IP>:12798']

  - job_name: 'relay' # To scrape data from the relay node
    scrape_interval: 5s
    static_configs:
      - targets: ['<Relay Node IP>:12798']

  - job_name: 'node' # To scrape data from a node exporter to monitor your linux host metrics.
    scrape_interval: 5s
    static_configs:
      - targets: ['<Block Producing Node IP>:9100']

  - job_name: 'prometheus monitor (external)' # To watch relay monitor for problems
    scrape_interval: 5s
    static_configs:
      - targets: ['<External Monitor IP>:9090']

  - job_name: 'prometheus monitor (self)' # To watch self for completeness
    scrape_interval: 5s
    static_configs:
      - targets: ['<Block Producing Node IP>:9090']

Updating

Check you library versions still match the recommended ones on the page.


You can update Cabal without reinstall like so:

cabal install Cabal cabal-install

Update KES

Currently KES needs to be updated every 90 days, gLiveView will tell you how long is left. You can update the KES from a cold machine with Daedalus synced, you need to generate kes.vkey, kes.skey and node.cert to update. Of these 3, only the kes.skey and node.cert need to be uploaded to the server. You need access to cold.skey and cold.counter.


This script will output the needed files in the current working directory, after running save kes.vkey, kes.skey and node.cert to a secure location then copy only kes.skey and node.cert to the block producing node and restart.

#!/bin/bash

export CARDANO_NODE_SOCKET_PATH="$(readlink -f ~)/.local/share/Daedalus/mainnet/cardano-node.socket";
CCLI="$(readlink -f ~)/.local/bin/cardano-cli";
GENESIS_SHELLY="$(readlink -f ~)/config/mainnet-shelley-genesis.json";
COLD_SKEY="$(readlink -f ~)/keys/cold.skey";
COLD_COUNTER="$(readlink -f ~)/keys/cold.counter";

# DO NOT MODIFY BELOW THIS LINE

CURRNET_SLOT=$("$CCLI" query tip --mainnet | grep "slot" | grep -Po "[0-9]+");

SLOTS_PER_KES_PERIOD=$(cat "$GENESIS_SHELLY" |
	grep "slotsPerKESPeriod" |
	grep -Po "[0-9]+");

CURRENT_PERIOD=$(expr $CURRNET_SLOT / $SLOTS_PER_KES_PERIOD | bc);

"$CCLI" node key-gen-KES \
	--verification-key-file kes.vkey \
	--signing-key-file kes.skey;

"$CCLI" node issue-op-cert \
	--kes-verification-key-file "./kes.vkey" \
	--cold-signing-key-file "$COLD_SKEY" \
	--operational-certificate-issue-counter "$COLD_COUNTER" \
	--kes-period $CURRENT_PERIOD \
	--out-file node.cert;

PoolTool

Sending Tip

If you have setup the node without all the extra tools sending your tip is easiest done through the shell/systemd script found here. You can send this info from your relays.


You need to ensure minSeverity logging is set to Notice!


You may also need to modify the cadano-node and cardano-cli if the script is running from systemd:

# CARDANO BINARIES
CNODE=$(command -v cardano-node)
CCLI=$(command -v cardano-cli)

Sending Slots

The easiest way to send slots to pooltool is to use the cncli sendSlots command, but that does not allow you to use a proxy - the below script is how you can send it manually while allowing specification of proxy. You will need run this with the cncli-fivedays.sh script as shown in the installation guide.


Manual sending of slots involve sending your slot count with the hash (Using blake2 NOT sha256!) of the compressed array of slot numbers (e.g. "[123, 456, 789]") and the array of the last epoch's slot numbers.


Ensure that the OUTPUT_DIR exits and you install blake2!

#!/bin/bash

POOL_ID="xxxxxx";
API_KEY="xxxx-xx-xx-xx-xxxx";
BLAKE=/root/.cargo/bin/blake2;
CNCLI=/usr/local/bin/cncli;
CNCLI_DB=/root/scripts/cncli.db;
BYRON_GENESIS=/root/config/mainnet-byron-genesis.json;
SHELLEY_GENESIS=/root/config/mainnet-shelley-genesis.json;
VRF_SKEY=/root/keys/vrf.skey;
OUTPUT_DIR=/root/leaderlogs/; # requires the '/' at the end
DESTINATION_URL="https://api.pooltool.io/v0/sendslots"; # Change to your proxy
RETRY_DELAY=5m;
RETRY_COUNT=5;

# END OF VARIABLES

get_leader_log () {
	echo $($CNCLI leaderlog \
		--byron-genesis $BYRON_GENESIS \
		--pool-id $POOL_ID \
		--pool-vrf-skey $VRF_SKEY \
		--shelley-genesis $SHELLEY_GENESIS \
		--db $CNCLI_DB);
}

LEADER_LOG=$(get_leader_log);

# Sometimes leaderlog fails - retry if this happens
RETRY_COUNTER=0;

while [ $(echo "$LEADER_LOG" | jq -c '.status') == "\"error\"" ]; do
	if [ $RETRY_COUNTER -ge $RETRY_COUNT ]; then
		echo "Failed to get leader logs after $RETRY_COUNT attempt(s)";
		exit 1;
	fi;

	echo "Failed to get leader logs, retrying in $RETRY_DELAY";
	sleep $RETRY_DELAY;
	LEADER_LOG=$(get_leader_log);

	RETRY_COUNTER=$((RETRY_COUNTER+1));
done;

# Get the other data.
CURRENT_SLOTS=$(echo "$LEADER_LOG" | jq -c '.assignedSlots' | jq -c '[ .[].slot ]');

CURRENT_EPOCH=$(echo "$LEADER_LOG" | jq -c '.epoch' | bc);
PREVIOUS_EPOCH=$(expr $CURRENT_EPOCH - 1 | bc);
CURRENT_EPOCH_HASH=$(echo -n "$CURRENT_SLOTS" | $BLAKE --length 32);
ASSIGNED_SLOTS=$(echo "$LEADER_LOG" | jq -c '.epochSlots' | bc);

echo "$CURRENT_SLOTS" > /root/leaderlogs/"$CURRENT_EPOCH"_logs.json;
echo "$CURRENT_EPOCH_HASH" > /root/leaderlogs/"$CURRENT_EPOCH"_logs.hash;
echo "$LEADER_LOG" > /root/leaderlogs/"$CURRENT_EPOCH"_logs_full.json;

if [ -f "${OUTPUT_DIR}${PREVIOUS_EPOCH}_logs.json" ]; then
	LAST_EPOCH_SLOTS=$(cat "${OUTPUT_DIR}${PREVIOUS_EPOCH}_logs.json");
else
	LAST_EPOCH_SLOTS="";
fi

JSON="$(jq -n --compact-output \
	--arg CURRENTEPOCH "$CURRENT_EPOCH" \
	--arg POOLID "$POOL_ID" \
	--arg APIKEY "$API_KEY" \
	--arg ASSIGNED "$ASSIGNED_SLOTS" \
	--arg HASH "$CURRENT_EPOCH_HASH" \
	--arg SLOTS $LAST_EPOCH_SLOTS \
	'{epoch: $CURRENTEPOCH, poolId: $POOLID, apiKey: $APIKEY, slotQty: $ASSIGNED, hash: $HASH, prevSlots: $SLOTS}')"

echo "Packet Sent: $JSON";
echo "Response Received: $(curl -s -H "Accept: application/json" -H "Content-Type:application/json" -X POST --data "$JSON" "$DESTINATION_URL")"

Proxy

If you wish to send stats from your block producing node I recommend writing a script on a relay or other server that listens for the request and makes an identical one to pooltool so that the identity of your block producing node is not compromised. An example of such a script can be found here.

CNCLI

The prebuilt CNCLI release does not work with Debian 10 so you need to build from source.


If cncli is missing after build:

cp /root/.cargo/bin/cncli /usr/local/bin/cncli

Debugging

Missing bin-path script (cardano-node build location)

I found the second time I build cardano-node the script that outputs the build was missing, so if you need to find it manually run:

find ./dist-newstyle -type f -name cardano-node
find ./dist-newstyle -type f -name cardano-cli

The locations when I ran it was:

./dist-newstyle/build/x86_64-linux/ghc-8.10.2/cardano-node-1.26.2/x/cardano-node/build/cardano-node/cardano-node
./dist-newstyle/build/x86_64-linux/ghc-8.10.2/cardano-cli-1.26.2/x/cardano-cli/build/cardano-cli/cardano-cli


Era Mismatch

If you get errors running some commands saying there is an era mismatch like the following:

Shelley command failed: query utxo  Error: A query from a certain era was applied to a ledger from a different era: EraMismatch {ledgerEraName = "Allegra", otherEraName = "Shelley"}

This is because the ledger is on the "Allegra" soft fork while the cli is running "Shelley", you need to either update the cli and/or add a parameter to the command specifying the era like so:

cardano-cli query utxo \
	--address $(cat payment.addr) \
	--mainnet \
	--allegra-era

Cardano Node Socket

If you have issues with cli commands referencing the cardano node socket, first ensure that the node is running (it can take a minute to generate the node.socket file in the database, then ensure the environment variable has been set.

export CARDANO_NODE_SOCKET_PATH=path/to/db/node.socket

Transactions

If your transaction fails due to a input-output mismatch, you need to check that the inputs (--tx-in) values sum to the output values (--tx-out) plus the fee's (--fee).

If your transaction fails due to output too small, you need to check that all the outputs (including change addresses) have the minimum value as set in the "minUTxOValue" parameter in the genesis block config (1000000 Lovelace or 1 Ada at the time of writing).

Floating IP/DNS

Floating IP's for some reason will not work with cardano-node so if you want the flexibility of floating IP's use DNS instead (with --single-host-pool-relay).

Guild Live View

I had an issue where the gLiveView script suddenly failed to connect to a running instance with the following error message:

COULD NOT CONNECT TO A RUNNING INSTANCE, 3 FAILED ATTEMPTS IN A ROW!

This problem appears to be with how the monitor records uptime, this should be fixed when cardano-node version 1.26 is released. A temporary fix is to prevent it from throwing the error, to do this change line 468 from:

if [[ ${uptimens} -le 0 ]]; then

To:

if false; then

Staking from a paper wallet

todo... I'll document this properly after I've done it myself

The idea is to first temporarily install a Daedalus on an offline live booted Linux and then restore a paper wallet into it with the 24 word backup phrase. Then we should be able to interact with the running wallet's associated node from the CLI to create and sign a delegation transaction. That transaction can then be broadcast to the network using any online Cardano node. This Shelley exercise explains the process of CLI delegation.

First you'll need to find the port that your Daedalus is running on:

ps x | grep cardano-node

Then you'll need to create the key files:

cardano-cli shelley address key-gen \
--verification-key-file payment.vkey \
--signing-key-file payment.skey \
--host-addr 127.0.0.1 \
--port PORT

Todo: We need to use a registered stake address that we have the keys for.

Create a delegation certificate, here the pool is the characters after the 5820 in the cborHex of the target pool's cold.vkey file.

cardano-cli shelley stake-address delegation-certificate \
--stake-verification-key-file stake.vkey \
--cold-verification-key-hash POOL \
--out-file delegation.cert \
--host-addr 127.0.0.1 \
--port PORT


Draft the transaction:

cardano-cli shelley transaction build-raw \
--tx-in <UTXO>#<TxIx> \
--tx-out $(cat payment.addr)+0 \
--ttl 0 \
--fee 0 \
--out-file tx.draft \
--certificate-file delegation.cert


Calculate the fees:

cardano-cli shelley transaction calculate-min-fee \
--tx-body-file tx.draft \
--tx-in-count 1 \
--tx-out-count 1 \
--mainnet \
--witness-count 1 \
--byron-witness-count 0 \
--protocol-params-file protocol.json


Build the transaction:

cardano-cli shelley transaction build-raw \
--tx-in <UTXO>#<TxIx> \
--tx-out $(cat payment.addr)+<CHANGE IN LOVELACE> \
--ttl <TTL> \
--fee <FEE> \
--out-file tx.raw \
--certificate-file pool-registration.cert \
--certificate-file delegation.cert


Sign the transaction:

cardano-cli shelley transaction sign \
--tx-body-file tx.raw \
--signing-key-file payment.skey \
--signing-key-file stake.skey \
--signing-key-file cold.skey \
--mainnet \
--out-file tx.signed


Submit the transaction:

cardano-cli shelley transaction submit --tx-file tx.signed --mainnet

Resources

See also