Step-by-Step Guide to Setting Up Stellar Validator Node
Tags: stellar, devops, blockchain, tutorial
Published: 2024-06-12 08:17:34

## Note:

- The tutorial will guide you to set up three validation nodes.
- The tutorial is brief and should help you set up a node within 15 minutes (excluding time for synchronization).
- I will not explain in detail the reasons for each step here, please refer to [the detailed documentation](https://developers.stellar.org/network/core-node) provided by SDF for more information.
- You need to have a basic understanding of Stellar.
- You need to have a basic understanding of Linux.
- Some community members requested a simpler setup guide, so I wrote this tutorial hoping it helps. If you have any questions, feel free to ask in [the Stellar Dev Discord](https://discord.com/invite/stellardev).

## Prerequisites:

- Three servers with Ubuntu 22.04 installed. You can find the hardware requirements [here](https://developers.stellar.org/network/core-node/admin-guide/prerequisites).
- A domain name; we assume your domain is `example.com`.
- Three Stellar accounts for the nodes. We assume the account for node A is `GA`, for node B is `GB`, and for node C is `GC`. Their corresponding private keys are `SA`, `SB`, and `SC`. You need to activate these three accounts on the network and [set their home domain](https://stellar-sdk.readthedocs.io/en/stable/api.html#stellar_sdk.transaction_builder.TransactionBuilder.append_set_options_op) to `example.com`.

## Let's get started

### Setup domain

- Point `core-live-a.example.com`, `core-live-b.example.com`, and `core-live-c.example.com` to the IP addresses of nodes A, B, and C, respectively.
- Point `history.core-live-a.example.com`, `history.core-live-b.example.com`, and `history.core-live-c.example.com` to the IP addresses of nodes A, B, and C, respectively.
- Point `example.com` to any server; it can be the IP address of node A, B, or C, or it could be another server. Here, we assume it points to the IP address of node A.

### Setup Stellar Core

#### Add the Stellar repository

```bash
sudo curl -fsSL https://apt.stellar.org/SDF.asc -o /etc/apt/keyrings/SDF.asc
sudo chmod a+r /etc/apt/keyrings/SDF.asc
echo "deb [signed-by=/etc/apt/keyrings/SDF.asc] https://apt.stellar.org $(lsb_release -cs) stable" | sudo tee -a /etc/apt/sources.list.d/SDF.list
sudo apt update
```

#### Install PosttgreSQL and Stellar Core

```bash
sudo apt install postgresql postgresql-contrib stellar-core
```

#### Setup PostgreSQL

Let's create a new database user and database.

```bash
sudo -u postgres psql -c "CREATE ROLE stellar WITH LOGIN;"
sudo -u postgres psql -c "CREATE DATABASE stellar OWNER stellar;"
```

#### Add a custom cp command

This is to allow `Nginx` to have permission to read these files.

```bash
sudo nano /usr/bin/stellarcp
# Add the following content
# Save the file
```

```bash
#!/bin/bash

# Check the number of arguments
if [ "$#" -ne 2 ]; then
    echo "Usage: stellarcp <source_file> <destination_file>"
    exit 1
fi

# Copy the file
cp "$1" "$2"

# modify the permissions of the destination file
chmod 0644 "$2"
```

```bash
sudo chmod +x /usr/bin/stellarcp
```

#### Configure Stellar Core

```bash
sudo -u stellar nano /etc/stellar/stellar-core.cfg
# Add the following configuration
# save the file
```

You need to talk care of the `EDIT ME` in the configuration file. In addition, you can use https://stellarbeat.io to search for existing nodes on the network in order to select suitable validator nodes to add to your own configuration file.

In the example configuration file, the three nodes of `lightsail.network` are set by me. You can keep or remove them according to your preference.

{% details ⚙️ click to display stellar-core.cfg %}

```toml
# complete example config: https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg

# Path to the file you want stellar-core to write its log to.
# You can set to "" for no log file.
LOG_FILE_PATH="/var/log/stellar/stellar-core.log"

# BUCKET_DIR_PATH (string) default "buckets"
# Specifies the directory where stellar-core should store the bucket list.
# This will get written to a lot and will grow as the size of the ledger grows.
BUCKET_DIR_PATH="/var/lib/stellar/buckets"

# Sets the DB connection string for SOCI.
# DATABASE="postgresql://user=stellar password=passw&rd host=127.0.0.1 port=5432 dbname=stellar"
# Local Peer connection string
DATABASE="postgresql://dbname=stellar user=stellar host=/var/run/postgresql/"

# This example also adds a common name to NODE_NAMES list named `self` with the
# public key associated to this seed
# EDIT ME: replace the NODE_SEED with your own seed
# EDIT ME: for the node A, use `SA`, for node B, use `SB`, and so on.
NODE_SEED="SA self"

# HOME_DOMAIN for this validator
# Required when NODE_IS_VALIDATOR=true
# When set, this validator will be grouped with other validators with the
# same HOME_DOMAIN (as defined in VALIDATORS/HOME_DOMAINS)
# EDIT ME: replace `example.com` with your domain
NODE_HOME_DOMAIN="example.com"

# Only nodes that want to participate in SCP should set NODE_IS_VALIDATOR=true.
# Most instances should operate in observer mode with NODE_IS_VALIDATOR=false.
# See QUORUM_SET below.
NODE_IS_VALIDATOR=true

# CATCHUP_COMPLETE (true or false) defaults to false
# if true will catchup to the network "completely" (replaying all history)
# if false will look for CATCHUP_RECENT for catchup settings
# If you set it to false, then you should be able to complete the synchronization in a short time.
CATCHUP_COMPLETE=false

# DEPRECATED_SQL_LEDGER_STATE (bool) default false
# When set to true, SQL is used to store all ledger state instead of
# BucketListDB. This is not recommended and may cause performance degregradation.
# This is deprecated and will be removed in the future. Note that offers table
# is still maintained in SQL when this is set to false, but all other ledger
# state tables are dropped.
DEPRECATED_SQL_LEDGER_STATE=false

# HTTP_PORT (integer) default 11626
# What port stellar-core listens for commands on.
# If set to 0, disable HTTP interface entirely
HTTP_PORT=11626

# PUBLIC_HTTP_PORT (true or false) default false
# If false you only accept stellar commands from localhost.
# Do not set to true and expose the port to the open internet. This will allow
#  random people to run stellar commands on your server. (such as `stop`)
PUBLIC_HTTP_PORT=false

# WORKER_THREADS (integer) default 11
# Number of threads available for doing long durations jobs, like bucket
# merging and vertification.
WORKER_THREADS=11

# MAX_CONCURRENT_SUBPROCESSES (integer) default 16
# History catchup can potentially spawn a bunch of sub-processes.
# This limits the number that will be active at a time.
MAX_CONCURRENT_SUBPROCESSES=16

# Configure which network this instance should talk to
NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"

# COMMANDS  (list of strings) default is empty
# List of commands to run on startup.
# Right now only setting log levels really makes sense.
COMMANDS=["ll?level=info"]
############################
# list of HOME_DOMAINS
############################
[[HOME_DOMAINS]]
HOME_DOMAIN = "www.stellar.org"
QUALITY = "HIGH"

[[HOME_DOMAINS]]
HOME_DOMAIN = "stellar.blockdaemon.com"
QUALITY = "HIGH"

[[HOME_DOMAINS]]
HOME_DOMAIN = "publicnode.org"
QUALITY = "HIGH"

[[HOME_DOMAINS]]
HOME_DOMAIN = "satoshipay.io"
QUALITY = "HIGH"

[[HOME_DOMAINS]]
HOME_DOMAIN = "lobstr.co"
QUALITY = "HIGH"

[[HOME_DOMAINS]]
HOME_DOMAIN = "lightsail.network"
QUALITY = "HIGH"

# EDIT ME: edit the following to match your domain
[[HOME_DOMAINS]]
HOME_DOMAIN = "example.com"
QUALITY = "HIGH"

# EDIT ME: I suggest you choose a variety of validators
# EDIT ME: to increase the decentralization of the network.
# See https://developers.stellar.org/network/core-node/admin-guide/configuring#choosing-your-quorum-set
############################
# List of Validators
############################
[[VALIDATORS]]
NAME = "SDF 1"
PUBLIC_KEY = "GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH"
ADDRESS = "core-live-a.stellar.org:11625"
HISTORY = "curl -sf http://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}"
HOME_DOMAIN = "www.stellar.org"

[[VALIDATORS]]
NAME = "SDF 2"
PUBLIC_KEY = "GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK"
ADDRESS = "core-live-b.stellar.org:11625"
HISTORY = "curl -sf http://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}"
HOME_DOMAIN = "www.stellar.org"

[[VALIDATORS]]
NAME = "SDF 3"
PUBLIC_KEY = "GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ"
ADDRESS = "core-live-c.stellar.org:11625"
HISTORY = "curl -sf http://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}"
HOME_DOMAIN = "www.stellar.org"

[[VALIDATORS]]
NAME = "Blockdaemon Validator 1"
PUBLIC_KEY = "GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU"
ADDRESS = "stellar-full-validator1.bdnodes.net:11625"
HISTORY = "curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}"
HOME_DOMAIN = "stellar.blockdaemon.com"

[[VALIDATORS]]
NAME = "Blockdaemon Validator 2"
PUBLIC_KEY = "GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C"
ADDRESS = "stellar-full-validator2.bdnodes.net:11625"
HISTORY = "curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}"
HOME_DOMAIN = "stellar.blockdaemon.com"

[[VALIDATORS]]
NAME = "Blockdaemon Validator 3"
PUBLIC_KEY = "GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4"
ADDRESS = "stellar-full-validator3.bdnodes.net:11625"
HISTORY = "curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}"
HOME_DOMAIN = "stellar.blockdaemon.com"

[[VALIDATORS]]
NAME = "Hercules by OG Technologies"
PUBLIC_KEY = "GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT"
ADDRESS = "hercules.publicnode.org:11625"
HISTORY = "curl -sf https://hercules-history.publicnode.org/{0} -o {1}"
HOME_DOMAIN = "publicnode.org"

[[VALIDATORS]]
NAME = "Lyra by BP Ventures"
PUBLIC_KEY = "GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z"
ADDRESS = "lyra.publicnode.org:11625"
HISTORY = "curl -sf https://lyra-history.publicnode.org/{0} -o {1}"
HOME_DOMAIN = "publicnode.org"

[[VALIDATORS]]
NAME = "Boötes"
PUBLIC_KEY = "GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2"
ADDRESS = "bootes.publicnode.org:11625"
HISTORY = "curl -sf https://bootes-history.publicnode.org/{0} -o {1}"
HOME_DOMAIN = "publicnode.org"

[[VALIDATORS]]
NAME = "SatoshiPay Iowa"
PUBLIC_KEY = "GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY"
ADDRESS = "stellar-us-iowa.satoshipay.io:11625"
HISTORY = "curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}"
HOME_DOMAIN = "satoshipay.io"

[[VALIDATORS]]
NAME = "SatoshiPay Singapore"
PUBLIC_KEY = "GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT"
ADDRESS = "stellar-sg-sin.satoshipay.io:11625"
HISTORY = "curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}"
HOME_DOMAIN = "satoshipay.io"

[[VALIDATORS]]
NAME = "SatoshiPay Frankfurt"
PUBLIC_KEY = "GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE"
ADDRESS = "stellar-de-fra.satoshipay.io:11625"
HISTORY = "curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}"
HOME_DOMAIN = "satoshipay.io"

[[VALIDATORS]]
NAME = "LOBSTR 1 (Europe)"
PUBLIC_KEY = "GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7"
ADDRESS = "v1.stellar.lobstr.co:11625"
HISTORY = "curl -sf https://archive.v1.stellar.lobstr.co/{0} -o {1}"
HOME_DOMAIN = "lobstr.co"

[[VALIDATORS]]
NAME = "LOBSTR 2 (Europe)"
PUBLIC_KEY = "GCB2VSADESRV2DDTIVTFLBDI562K6KE3KMKILBHUHUWFXCUBHGQDI7VL"
ADDRESS = "v2.stellar.lobstr.co:11625"
HISTORY = "curl -sf https://archive.v2.stellar.lobstr.co/{0} -o {1}"
HOME_DOMAIN = "lobstr.co"

[[VALIDATORS]]
NAME = "LOBSTR 3 (North America)"
PUBLIC_KEY = "GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63"
ADDRESS = "v3.stellar.lobstr.co:11625"
HISTORY = "curl -sf https://archive.v3.stellar.lobstr.co/{0} -o {1}"
HOME_DOMAIN = "lobstr.co"

[[VALIDATORS]]
NAME = "LOBSTR 4 (Asia)"
PUBLIC_KEY = "GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J"
ADDRESS = "v4.stellar.lobstr.co:11625"
HISTORY = "curl -sf https://archive.v4.stellar.lobstr.co/{0} -o {1}"
HOME_DOMAIN = "lobstr.co"

[[VALIDATORS]]
NAME = "LOBSTR 5 (India)"
PUBLIC_KEY = "GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7"
ADDRESS = "v5.stellar.lobstr.co:11625"
HISTORY = "curl -sf https://archive.v5.stellar.lobstr.co/{0} -o {1}"
HOME_DOMAIN = "lobstr.co"

[[VALIDATORS]]
NAME = "Lightsail Network 1"
PUBLIC_KEY = "GCAT2DUDAW7FSNEX4O2TKH3X2UN6RM6LJGBS4FVV6UN62ROBPWYDMYY5"
ADDRESS = "core-live-a.lightsail.network:11625"
HISTORY = "curl -sf https://core-live-a-history.lightsail.network/{0} -o {1}"
HOME_DOMAIN = "lightsail.network"

[[VALIDATORS]]
NAME = "Lightsail Network 2"
PUBLIC_KEY = "GBZSLUW7NHPXCJN7SIDUGDH754VFYDPXZII6S74EGV6I5Y34BHE7E2PJ"
ADDRESS = "core-live-b.lightsail.network:11625"
HISTORY = "curl -sf https://core-live-b-history.lightsail.network/{0} -o {1}"
HOME_DOMAIN = "lightsail.network"

[[VALIDATORS]]
NAME = "Lightsail Network 3"
PUBLIC_KEY = "GA3FLRTZLNMBXCQ2GG4W2CO2WXWGDDROCD3KVD5QYMYB5NXBUYMO2QXT"
ADDRESS = "core-live-c.lightsail.network:11625"
HISTORY = "curl -sf https://core-live-c-history.lightsail.network/{0} -o {1}"
HOME_DOMAIN = "lightsail.network"

# EDIT ME: replace the following with your own validators, this is for the node A.
# EDIT ME: For node A, you need to set the information of nodes B and C below.
# EDIT ME: For node B, you need to set the information of nodes A and C below.
# EDIT ME: For node C, you need to set the information of nodes A and B below.
[[VALIDATORS]]
NAME = "Example Node B"
PUBLIC_KEY = "GB"
ADDRESS = "core-live-b.example.com:11625"
HISTORY = "curl -sf https://history.core-live-b.example.com/{0} -o {1}"
HOME_DOMAIN = "example.com"

[[VALIDATORS]]
NAME = "Example Node C"
PUBLIC_KEY = "GC"
ADDRESS = "core-live-c.example.com:11625"
HISTORY = "curl -sf https://history.core-live-c.example.com/{0} -o {1}"
HOME_DOMAIN = "example.com"

# HISTORY
# Used to specify where to fetch and store the history archives.
# Fetching and storing history is kept as general as possible.
# Any place you can save and load static files from should be usable by the
#  stellar-core history system.   s3, the file system, http, etc
# stellar-core will call any external process you specify and will pass it the
#  name of the file to save or load.
# Simply use template parameters `{0}` and `{1}` in place of the files being transmitted or retrieved.
# You can specify multiple places to store and fetch from. stellar-core will
# use multiple fetching locations as backup in case there is a failure fetching from one.
#
# Note: any archive you *put* to you must run `$ stellar-core new-hist <historyarchive>`
#       once before you start.
#       for example this config you would run: $ stellar-core new-hist local

# this creates a `local` archive on the local drive
# NB: this is an example, in general you should probably not do this as
#   archives grow indefinitely
[HISTORY.local]
get="cp /var/lib/stellar/history/{0} {1}"
put="stellarcp {0} /var/lib/stellar/history/{1}"
mkdir="mkdir -p /var/lib/stellar/history/{0}"
```

{% enddetails %}

#### Initialize the database

```bash
sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg new-db
```

If the configuration is correct, you will not see any error message.

#### Initialize the history archive

```bash
sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg new-hist local
```

If you see `cp: target '/var/lib/stellar/history/.well-known/stellar-history.json' is not a directory`, don't worry, it's not your fault. You can ignore this message.

#### Start Stellar Core

```bash
sudo systemctl restart stellar-core
```

Check the status of Stellar Core:

```bash
sudo systemctl status stellar-core
```

You should see `Active: active (running)`. If you see other messages, please check the log file `/var/log/stellar/stellar-core.log`.

#### Check the sync status

```bash
stellar-core --conf /etc/stellar/stellar-core.cfg http-command 'info'
```

You should see the `state` is `Catching up`, now, you need to wait until the `state` is `Synced!`. Because we have set `CATCHUP_COMPLETE` to `false`, the validator will not synchronize the complete historical records. You should be able to complete the synchronization in a few minutes or hours.

### Publishing History Archives

Next, let's publish the history archives. We'll use Nginx to publish the locally stored history archives. You can use other methods to publish the history archives, such as publicly exposing the AWS S3 bucket.

The following example publishes history archives on node A. For nodes B and C, you need to repeat this process, adjusting the configuration file `EDIT ME` sections and replacing the domain information appropriately.

#### Install Nginx and Certbot

```bash
sudo apt install nginx certbot python3-certbot-nginx
```

#### Setup Nginx

```bash
# Create a new site configuration file, replace `history.core-live-a.example.com` with your domain
sudo nano /etc/nginx/sites-available/history.core-live-a.example.com
# Add the following configuration
# Save the file
```

```text
server {
  listen 80;
  root /var/lib/stellar/history/;

  # EDIT ME: replace `history.core-live-a.example.com` with your domain
  server_name history.core-live-a.example.com;

  # do not cache 404 errors
  error_page 404 /404.html;
  location = /404.html {
    add_header Cache-Control "no-cache" always;
  }

  # do not cache history state file
  location ~ ^/.well-known/stellar-history.json$ {
    add_header Cache-Control "no-cache" always;
    try_files $uri =404;
  }

  # cache entire history archive for 1 day
  location / {
     add_header Cache-Control "max-age=86400";
     try_files $uri =404;
  }
}
```

#### Enable the site

```bash
sudo ln -s /etc/nginx/sites-available/history.core-live-a.example.com /etc/nginx/sites-enabled/
# Test the configuration
sudo nginx -t
# Reload Nginx
sudo nginx -s reload
```

#### Setup SSL Certificate

```bash
sudo certbot --nginx -d history.core-live-a.example.com
```

### Setup Stellar TOML

Here we will set up a `.well-known/stellar.toml` file to publish node information. Earlier, we pointed `example.com` to the IP address of node A, so we will set up this file on node A.

```bash
sudo mkdir -p /var/www/example.com/.well-known/
sudo nano /var/www/example.com/.well-known/stellar.toml
# Add the following content
# Save the file
```

```toml
# See https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md for more information.
VERSION = "2.0.0"
NETWORK_PASSPHRASE = "Public Global Stellar Network ; September 2015"

[DOCUMENTATION]
ORG_NAME = "Example"
ORG_URL = "https://example.com"

[[VALIDATORS]]
ALIAS="ena"
DISPLAY_NAME="Example Node A"
HOST="core-live-a.example.com:11625"
PUBLIC_KEY="GA"
HISTORY="https://history.core-live-a.example.com/"

[[VALIDATORS]]
ALIAS="enb"
DISPLAY_NAME="Example Node B"
HOST="core-live-b.example.com:11625"
PUBLIC_KEY="GA"
HISTORY="https://history.core-live-b.example.com/"

[[VALIDATORS]]
ALIAS="enc"
DISPLAY_NAME="Example Node C"
HOST="core-live-c.example.com:11625"
PUBLIC_KEY="GC"
HISTORY="https://history.core-live-c.example.com/"
```

```bash
sudo chown -R www-data:www-data /var/www/example.com/
sudo chmod -R 755 /var/www/example.com/
```

```bash
sudo nano /etc/nginx/sites-available/example.com
# Add the following configuration
# Save the file
```

```text
server {
    listen 80;
    # EDIT ME: replace `example.com` with your domain
    server_name example.com;

    root /var/www/example.com/;

    location / {
        try_files $uri $uri/ =404;
    }

    location /.well-known/stellar.toml {
        add_header Content-Type text/plain;
        add_header Access-Control-Allow-Origin *;
    }
}
```

```bash
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
# Test the configuration
sudo nginx -t
# Reload Nginx
sudo nginx -s reload
```

```bash
sudo certbot --nginx -d example.com
```