Run a node
nulld is the NULL full node. It downloads and verifies the chain, relays transactions, serves a local control socket and, when asked, JSON-RPC over HTTP and Prometheus metrics. This guide takes a node from download to a long-running mainnet service.
nulld has no configuration file and no init step. Everything is a flag on
nulld run, and the first run creates the data directory. The full flag list
is in the command line reference.
Install a release
Releases are published at github.com/NullNetProtocol/null-chain/releases. Each release has one archive per platform and a SHA256SUMS file:
| Platform | Archive |
|---|---|
| Linux x86-64 (glibc 2.28 or later) | null-0.1.8-x86_64-unknown-linux-gnu.tar.gz |
| macOS Apple silicon | null-0.1.8-aarch64-apple-darwin.tar.gz |
| macOS Intel | null-0.1.8-x86_64-apple-darwin.tar.gz |
| Windows x86-64 | null-0.1.8-x86_64-pc-windows-msvc.zip |
Every archive holds three binaries: nulld (the node and command line wallet), null-wallet-rpc (the wallet daemon) and null-desktop (the desktop wallet).
Download the archive and SHA256SUMS, check the hash, then install the binaries:
Linux
VERSION=0.1.8
BASE=https://github.com/NullNetProtocol/null-chain/releases/download/v$VERSION
curl -LO $BASE/null-$VERSION-x86_64-unknown-linux-gnu.tar.gz
curl -LO $BASE/SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
tar -xzf null-$VERSION-x86_64-unknown-linux-gnu.tar.gz
sudo install -m 0755 null-$VERSION-x86_64-unknown-linux-gnu/nulld \
null-$VERSION-x86_64-unknown-linux-gnu/null-wallet-rpc /usr/local/bin/
nulld --version
On macOS use shasum -a 256 --check --ignore-missing SHA256SUMS. On Windows, compare the output of certutil -hashfile null-0.1.8-x86_64-pc-windows-msvc.zip SHA256 with the line in SHA256SUMS.
Binaries are not code-signed. The checksum confirms the download matches the release; for stronger assurance, build from source and compare.
Build from source
You need a Rust toolchain, version 1.85 or later.
git clone https://github.com/NullNetProtocol/null-chain.git
cd null-chain
git checkout v0.1.8
cargo build --release --locked -p null-node
./target/release/nulld --version
This builds nulld and null-wallet-rpc into target/release/. Add -p null-desktop to build the desktop wallet too; on Linux it needs the X11 or Wayland development libraries and an OpenGL-capable driver.
Start a mainnet node
nulld run
With no flags the node joins mainnet, dials the seed nodes seed1 to seed5.nullnet.sh:19000, stores the chain in the default data directory and opens the control socket on 127.0.0.1:18444. The first lines of output say what it serves:
nulld up on the main network
peers outbound only
control 127.0.0.1:18444
json-rpc disabled
metrics disabled
token /home/you/.local/share/null/main/chain/rpc.token
Stop it with Ctrl-C or SIGTERM; it shuts down cleanly. On Windows, closing the console, logging off and shutting down also stop it cleanly.
Use --datadir to keep the chain somewhere else. Only one process can open a chain database, so stop the desktop wallet before running nulld on its default chain.
Data directory
The node's data directory holds:
| File | Contents |
|---|---|
chain.redb | the chain database |
rpc.token | the token for the control socket and JSON-RPC, owner-readable only |
peers.dat | the address book of peers the node verified, kept across restarts |
--in-memory keeps the chain in memory instead, for tests and throwaway nodes. Such a node prints its generated token at startup.
Logs
Each line has a time and a level, colored on a terminal (set NO_COLOR or pipe the output to turn colors off). --log-level debug|info|warn|error sets the threshold. --log-file <path> also appends every line to a file, dated in UTC and uncolored, owner-only on Unix. nulld does not rotate this file; use logrotate with copytruncate for a long-running node.
Accept inbound peers
A node connects out only until you give it --listen. To serve peers, listen on the public interface and open TCP port 19000:
nulld run --listen 0.0.0.0:19000
The node announces its public interface address and port to peers automatically. Behind NAT, forward the port and also pass the public endpoint:
nulld run --listen 0.0.0.0:19000 --advertise 203.0.113.10:19000
Other nodes check the endpoint with a full handshake before sharing it, and address exchange repeats every ten minutes, so a reachable listener is discovered without being added to any seed list. Use --connect to dial specific peers and --seed to add bootstrap peers, both repeatable.
Control socket and token
The control socket is a line protocol on 127.0.0.1:18444. A session must start with auth <token>; three refused requests close the connection. The node generates a random token at startup and writes it to rpc.token in the data directory, or uses the one given with --rpc-token or NULL_RPC_TOKEN.
| Command | Reply |
|---|---|
auth <token> | ok or err unauthorized |
status | ok height=<h> hash=<hex> peers=<n> mempool=<n> mined=<n> in_chain=<n> syncing=<bool> |
block <height> | the block as hex, or err not found |
compact <height> | the compact block as hex, or err not found |
hash <height> | the block hash, or err not found |
coinbase <height> | the coinbase transaction as hex, or err not found |
submit <hex tx> | ok <txid> or err <reason> |
txstatus <txid> | ok unknown, ok pooled or ok confirmed <height> <index> |
spent <nullifier> | ok true or ok false |
The wallet commands of nulld (balance, send, consolidate) and null-wallet-rpc talk to the node through this socket. For a quick check:
nulld status --rpc-token-file ~/.local/share/null/main/chain/rpc.token
Keep the control socket on localhost. The token stops other local users, not the network.
Enable JSON-RPC
Exchanges, pools and explorers integrate against JSON-RPC 2.0 over HTTP. Turn it on with --rpc-http. It uses the same token as the control socket, sent as a bearer token:
nulld run --rpc-http 127.0.0.1:18446
cURL
curl -s -H "Authorization: Bearer $(cat ~/.local/share/null/main/chain/rpc.token)" \
-d '{"jsonrpc":"2.0","method":"getblockchaininfo","id":1}' \
http://127.0.0.1:18446/
Bind it to 127.0.0.1 or a private interface. The endpoint is plain HTTP and the token is the only protection; if another host must reach it, put it behind a TLS proxy or an SSH tunnel, never directly on a public address. To keep the token stable across restarts, pass your own with NULL_RPC_TOKEN instead of reading rpc.token each time. The methods are listed in the JSON-RPC reference.
--blocknotify '<command>' runs a shell command on every new main-chain tip, with %s replaced by the block hash. Pools use it instead of polling; see the pool guide.
Check sync
Three views of the same state:
nulld statusprints the control socket's status line.syncing=falseand aheightmatching the explorer mean the node is caught up.getblockchaininfoover JSON-RPC returnsheight,best_block_hash,syncingand the network's upgrade schedule.--metrics 127.0.0.1:9100serves Prometheus metrics atGET /metrics.
| Metric | Meaning |
|---|---|
null_height | height of the chain tip |
null_peers{direction} | connected peers past the handshake, inbound and outbound |
null_mempool_transactions | transactions in the mempool |
null_blocks_mined{state} | blocks this node mined since it started (found), and how many are still in the main chain (in_chain) |
null_pending_compact_blocks | compact blocks waiting for transactions |
null_uptime_seconds | seconds since the node started |
null_mining_ready | 1 when the node hands out mining work: caught up and, on mainnet, with at least one peer |
The metrics endpoint has no authentication. Bind it to localhost or a monitoring network. Mining itself is covered in Mining.
Run under systemd
Create a system user and install the unit below as /etc/systemd/system/nulld.service. It keeps the chain in /var/lib/null, accepts peers on port 19000 and keeps the control socket and JSON-RPC on localhost.
nulld.service
[Unit]
Description=NULL full node
Wants=network-online.target
After=network-online.target
[Service]
User=null
Group=null
ExecStart=/usr/local/bin/nulld run \
--datadir /var/lib/null \
--listen 0.0.0.0:19000 \
--rpc 127.0.0.1:18444 \
--rpc-http 127.0.0.1:18446
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=60
StateDirectory=null
StateDirectoryMode=0700
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/null
[Install]
WantedBy=multi-user.target
sudo useradd --system --home-dir /var/lib/null --shell /usr/sbin/nologin null
sudo systemctl daemon-reload
sudo systemctl enable --now nulld
journalctl -u nulld -f
sudo -u null nulld status --rpc-token-file /var/lib/null/rpc.token
Output goes to the journal, so no --log-file is needed. Remove --listen for an outbound-only node, and --rpc-http if nothing needs JSON-RPC.
Run in Docker
The repository has a Dockerfile that builds nulld and runs it as an unprivileged null user with nulld as the entrypoint:
git clone https://github.com/NullNetProtocol/null-chain.git
cd null-chain && git checkout v0.1.8
docker build -t nulld:0.1.8 .
docker run -d --name nulld --restart unless-stopped \
-v nulld-data:/home/null -p 19000:19000 \
nulld:0.1.8 run --listen 0.0.0.0:19000
The volume holds the null user's home, so the chain lands in the default data directory, /home/null/.local/share/null/main/chain, and survives container upgrades.
The control socket binds 127.0.0.1 inside the container, so use docker exec to reach it:
docker exec nulld nulld status \
--rpc-token-file /home/null/.local/share/null/main/chain/rpc.token
Publish JSON-RPC only to the host's loopback: add --rpc-http 0.0.0.0:18446 to the command and -p 127.0.0.1:18446:18446 to docker run.
Tor and I2P
Tor
To send every outbound connection through Tor, run Tor with a SOCKS port and start the node with --proxy:
nulld run --proxy 127.0.0.1:9050 --listen 127.0.0.1:19000
.onion names then work in --connect and --seed. To accept peers as an onion service, add this to torrc:
torrc
SocksPort 127.0.0.1:9050
HiddenServiceDir /var/lib/tor/nulld
HiddenServicePort 19000 127.0.0.1:19000
Share the hostname in /var/lib/tor/nulld/hostname with peers as <hostname>:19000. With a proxy the node does not announce IP addresses, --advertise is rejected, and onion addresses are shared by explicit configuration only.
I2P
Run an I2P router with the SAM bridge enabled and start the node with --i2p:
nulld run --i2p 127.0.0.1:7656 --connect <destination>.i2p:19000
The bare flag --i2p uses the default bridge address 127.0.0.1:7656. The node opens a transient session and reaches .i2p peers passed to --connect or --seed; other peers use the proxy, if any, or a direct connection. Inbound I2P service is not supported yet.
Upgrade
- Read the release notes. A release that schedules a network upgrade says so and gives the activation height.
- Stop the node (
sudo systemctl stop nulld). - Install the new binaries as in Install a release.
- Start the node and watch the log.
The chain database records the consensus rules it was built under. If a release changes them, the node refuses to open the old database and names the directory to delete:
the chain in /var/lib/null was built under different consensus rules, for example by an older version; delete that directory and restart to resync
Delete that chain directory and start again; the node resyncs from peers. Wallet files are not affected, because wallets rescan from the node. There is no in-place rewind.