Running a Nomad Node
Difficulty: Medium (linux server management, cli usage)
Setup Time: ~1 hr
Technical Requirements
- Dedicated server or virtual machine supporting Intel SGX2, for example:
- OVH Cloud Rise 1 or Rise 2 ($70-80/mo)
- Azure Cloud DC2s-v3 Instances ($140/mo)
- Linux Kernel >=5.11
- Docker (with compose)
- Intel Aesmd
SGX Setup (Ubuntu 24.04 LTS)
- Ensure SGX is enabled in BIOS
Note: On OVH, make sure to enable Remote KVM or IPMI when provisioning the machine to be able to access the BIOS
- SSH into your server
ssh ubuntu@my.server.ipNote: It is highly recommended to do some basic hardening on the instance.
- Create a new user and delete the ubuntu user
- Setup
~/.ssh/authorized_keysand disable password login for ssh- Routinely update the system packages
- Setup Intel's package signing key and install
aesmd
curl -fsSL https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key | sudo apt-key add -
sudo add-apt-repository "deb https://download.01.org/intel-sgx/sgx_repo/ubuntu noble main"
sudo apt-get update
sudo apt-get install -y sgx-aesm-service libsgx-dcap-default-qpl
sudo ln -s /usr/lib/x86_64-linux-gnu/libdcap_quoteprov.so.1 /usr/lib/x86_64-linux-gnu/libdcap_quoteprov.so- Configure the quote provider to use Intel PCS for attestations
sudo cp /etc/sgx_default_qcnl.conf /etc/sgx_default_qcnl.conf.bkp
sudo sed -s -i 's/localhost:8081/api.trustedservices.intel.com/' /etc/sgx_default_qcnl.conf- Install the Rust programming language
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup toolchain default stable- Install sgxs tools from fortanix
cargo install sgxs-tools- Add the current user to the sgx group
sudo groupadd sgx_prv
sudo groupadd sgx
sudo usermod -aG sgx_prv $USER
sudo usermod -aG sgx $USERLogout and re-login to ensure the group is fully added to your user.
- Verify sgx installation
From the sgxs-tools crate we installed, we can verify if the machine has been provisioned correctly by running:
sgx-detectYou should see an output similar to the one below:
Detecting SGX, this may take a minute...
✔ SGX instruction set
✔ CPU support
✔ CPU configuration
✔ Enclave attributes
✔ Enclave Page Cache
SGX features
✔ SGX2 ✔ EXINFO ✔ ENCLV ✔ OVERSUB ✔ KSS
Total EPC size: 378.0MiB
✔ Flexible launch control
✔ CPU support
? CPU configuration
✔ Able to launch production mode enclave
✔ SGX system software
✔ SGX kernel device (/dev/sgx_enclave)
✔ libsgx_enclave_common
✔ AESM service
✔ Able to launch enclaves
✔ Debug mode
✔ Production mode
✔ Production mode (Intel whitelisted)
You're all set to start running SGX programs!Docker compose
Create a directory for your node with the following structure:
nomad/
├─ data/
│ └─ config.toml
└─ docker-compose.ymlFor example:
# create both directories
mkdir -p nomad/data
# create the configuration files
touch nomad/docker-compose.yml
touch nomad/data/config.tomlExample docker-compose.yml
The following docker compose file runs the nomad node with the required sgx devices connected, configuration and state mounted, node auto-restart, and a healthcheck script:
services:
nomad:
container_name: nomad
hostname: nomad
image: "..." # Unreleased for now
command: ["run", "-v"]
devices:
# Allow access to sgx devices
- /dev/sgx_enclave:/dev/sgx_enclave
- /dev/sgx_provision:/dev/sgx_provision
volumes:
# Mount aesmd dir for socket access
- /var/run/aesmd:/var/run/aesmd
# Mount quote provider configuration
- ./sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf
# Mount configuration and state directory
- ./data:/root/.config/nomad
environment:
# Enable sgx logs
- SGX_DEBUG=1
# Enable rust backtraces
- RUST_BACKTRACE=0
ports:
# Forward networking ports from configuration
- "8000:8000"
- "9000:9000"
healthcheck:
# Healthcheck endpoint integration
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
restart: unless-stoppedTelemetry
Telemetry can be exported by adding standard OTEL_EXPORTER environment variables to the compose file:
For example, using Signoz Cloud:
services:
nomad:
...
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.us.signoz.cloud
- OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key=<key>You may also create a .env file next to the docker-compose.yml containing the variables, and expose it to the node:
services:
nomad:
...
env: .envExample config.toml
Next to your docker compose file, the data/ directory is exposed to the container for configuration and enclave state.
Create a configuration for the node in data/config.toml:
[enclave]
# Mainnet ethereum bootstrap node
nodes = ["15.235.50.174:8000"]
# Enclave binary configuration, do not touch
enclave_path = "nomad-enclave-0.1.0.sgxs"
signature_path = "nomad-enclave-0.1.0.sig"
signal_log_path = "signal.log"
[eth]
# Minimum amount of eth each account should have when swapping tokens
min_eth = 0.0002
# Attested eth rpc url
geth_rpc = "..."
# Attested buildernet rpc url
builder_rpc = "..."
# Mirage platform compliance signers
whitelist_signers = ["9070a3c16a130e1db8f27414f3c6081c99798c7c856670ae49ac2f10b3faa4a4"]
[eth.uniswap]
# Enable swapping configured tokens for ETH to maintain EOA transaction funds
enabled = false
# Number of active eoa accounts to manage funds inside
num_accounts = 2
# Mainnet ethereum uniswap router - do not touch unless deploying on an alternate network
router = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
# Minimum amount of eth to swap for at a time
# If the node needs to swap more than this, a multiple is used to get to at least min_eth
target_eth_amount = 0.005
max_slippage_percent = 5
swap_deadline = "20m"
check_interval = "5m"
[eth.token.USDT]
# Mainnet USDT token address
address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
# Minimum total balance to reserve when swapping (in human-readable units)
min_balance = 5000.0
# Enable swapping from this token
swap = true
[p2p]
# List of peer multiaddrs to bootstrap from
bootstrap = ["/ip4/15.235.50.174/tcp/9000"]
# Interval for searching for more network peers
bootstrap_interval = "5m"
# Port to listen on for the node's libp2p tcp server.
# Must be the same as the port forwarded in the docker container.
tcp = 9000
[api]
# Port to listen on for the node's rest api server.
# Must be the same as the port forwared in the docker container.
port = 8000
[otlp]
# Opentelemetry sources, exporter configured using standard opentelemetry
# environment variables
logs = false
traces = false
metrics = falseFunding the node
Node runners can onboard new clean funds by sending them to the address printed by the deposit subcommand:
docker compose run nomad depositThe node will move the funds into encumbered accounts and uses them to execute user signals. Direct CEX transfers are best to ensure the smoothest onboarding process.
Starting and updating the node
Once the configuration has been created and funds have been deposited, the node can be started and managed using normal docker compose commands:
docker compose up -d --pull alwaysIt is required to re-run this command with --pull always periodically, to ensure the node software is up-to-date. Failure to do so may lead to missing out on signals and rewards.
Stopping and withdrawing from the node
At any point the node operator can request funds to be withdrawn from the node's accounts. Simply halt the node process, and run one of the the following commands:
# Stop the node first
docker compose down
# For all funds
docker compose run nomad withdraw --all <RECIPIENT>
# For a specific amount of funds.
# Amount MUST include token decimals, for example 5000 USDC with 6 decimals:
docker compose run nomad withdraw --contract <TOKEN_ADDRESS> --amount 5000000000 <RECIPIENT>The node will begin the withdraw process inside the enclave, sending the platform any outstanding fees, producing Mirage signals, and broadcasting them to other nodes for final transfers.
