ZYCORD docs
English
ZycordDocsRunning a node

Running a node

The operator's guide to zycordd. The protocol it speaks is the wire specification; this page is about running it.

The one-line version#

zycordd --devnet --dir ./devnet

That is a full node: it validates every block from genesis, serves peers, and answers a read-only RPC on 127.0.0.1:9420. To join the network that is actually running, see Public testnet — and note that it needs the -randomx binary.

What the node does not have#

There is no key material in the node process, ever. It accepts no seed, no passphrase and no key file. Signing happens in zcd, and the node's only write endpoint — /submit — grants no authority: a submitted certificate is validated exactly as one arriving from a stranger would be.

There is also no privileged endpoint, because there is nothing to privilege. No key can pause, upgrade, censor or mint, so there is no call to expose.

If you put the RPC behind a proxy, rate-limit at the proxy

The limiter keys on the transport peer and never on X-Forwarded-For: a limiter that trusts a header the client sets is a limiter the client turns off. Behind a proxy every request therefore arrives with the proxy's address, so the per-client limit stops being per-client and becomes one shared bucket of 600 a minute for everybody at once — and a single caller can empty it for all of them. That is arguably worse than no limit at all, and nothing in the response says so: callers get a plain 429 whether they caused it or someone else did.

The read-only surface#

Anything that watches the chain from outside — an explorer, a monitor, an indexer — reads it here. The node hands over bytes and never interpretation.

EndpointAnswers
/status, /headTip identity, height, state root
/block?height=N or ?id=0x…One block as JSON, with canonical, orphaned and confirmations
/block?…&format=sszThe same block as canonical SSZ bytes, application/octet-stream
/paramsThe active parameter set and its consensus root
/cell, /balanceTip state
/feesBase fees, and the elastic ceilings in force
/mempoolPool counters; ?limit=N adds up to 1000 pending ids, lowest first
/network, /metricsConnection shape and counters
/submitThe only write, and it grants nothing
The ceilings are not parameters

Whitepaper §8.1 makes the block's byte, gas and certificate ceilings functions of T, the sequential target, and T is consensus state the epoch controller moves. /params therefore carries only the genesis values — block_byte_limit_genesis and friends — which are where T starts and the floor it can decay back to, never the limit in force. Those numbers stay real forever, so reading one for "the block size limit" is wrong silently, and by up to the distance between the 2.5 MB genesis value and the 8 MB capacity wall. /fees serves the live T and the four ceilings derived from it, so the derivation can be checked rather than trusted.

Why the bytes matter#

blake3("zcd/block/v1" ‖ header_bytes) is the block id, so an observer that re-derives ids from what it was served either agrees with the network or finds out immediately. It is also how per-certificate outcomes are obtained: whether a certificate was applied, skipped and billed, or dropped is computed inside the fold and never persisted, because one row per certificate forever, in a store that lives in memory, is not something a node can be asked to carry. An observer that wants the outcomes folds the block itself.

What is not here, and will not be#

  • No arbitrary state iteration. No address list, no prefix scan, no rich list.
  • No historical state. /cell and /balance read the tip; "balance at height H" implies retained history the node does not keep.
  • No admin, reindex or debug call, because there is nothing to privilege.

Aggregation belongs to whatever is watching, computed once at ingest into its own database.

Status codes#

Reads answer GET and HEAD and refuse every other verb with 405. A well-formed question with a negative answer — a height the chain has not reached, an unknown id, the body of a block that lost a reorg — is 404; only a malformed request is 400. A record the node wrote and can no longer read back is 500, not 400: the fault is the node's disk, and telling a caller its request was malformed invites it to stop retrying a request that was always well-formed. A poller should never have to read prose to tell absence from error, or its own bug from the node's.

Reorgs#

Height lookup answers for the canonical chain only. A block that loses a reorg keeps its header and loses its body, so its id still resolves, comes back marked orphaned, and still carries the parent link back to the fork point — which makes id lookup the only reorg-safe path.

Reachability, and why it matters more than it looks#

Zycord does not do NAT traversal. A node with --listen on a reachable port is somewhere others can bootstrap from, and the share of such nodes is the falsifiable condition the whole no-traversal decision rests on.

# periphery: nothing to configure
zycordd --testnet --dir ./testnet

# core: bind a port, and make sure it is actually reachable
zycordd --testnet --dir ./testnet \
  --listen 0.0.0.0:9421 --advertise <public-address>:9421
Advertise an address that answers, or none at all

--advertise falls back to --listen, so --listen 0.0.0.0:9421 with no --advertise publishes 0.0.0.0:9421, which nobody can dial. A wrong address propagates through peer exchange and costs every node that tries it a dial. If you cannot forward a port, leave --listen off and be periphery.

Checking whether it worked#

curl -s localhost:9420/network
{"enabled":true,"peers":8,"listening":true,"inbound":5,"outbound":3,"reachable":true}

listening: true with inbound: 0 after the node has been up for a few minutes means the port is not actually reachable: the process is bound and waiting, and nothing is arriving. That looks healthy in every other view, which is why this endpoint exists.

Bootstrap addresses from a file#

zycordd --testnet --dir ./testnet --peers-file peers.txt

One address per line; # comments and blank lines are ignored, and the list is merged with the network's built-in seeds. --peers takes the same thing as a comma-separated argument, and --no-seeds drops the built-ins while keeping both.

The wallet interface, over an ssh tunnel#

A node holds no key and never will. The wallet is a separate process, and on a server it is zcd ui:

zcd ui --key wallet.json --no-open

That prints a URL and serves it on 127.0.0.1:9430. It binds loopback and refuses anything else, with no flag to override. The process behind that listener holds an unlocked private key and authenticates with a bearer token in a URL, which is adequate for a socket only the local machine can open and is not adequate for anything else. Reaching it from elsewhere is ssh's job, and ssh is better at it than this would be:

# on your own machine
ssh -L 9430:127.0.0.1:9430 <host>

Then open the URL the server printed. The token rides in the URL fragment, so it never reaches the server, a log, or a Referer header — treat the URL as the secret it is. It is new on every run, and Ctrl-C wipes the key and stops serving.

The local end of the forward does not have to be port 9430: the interface checks the hostname in the Host header and deliberately not the port. The hostname is what matters — it is what blocks DNS rebinding, which is the real attack against a server on loopback, since any page in your browser can make requests to 127.0.0.1 and the one thing it cannot forge is the name it was navigated to.

Do not put a reverse proxy in front of zcd ui

The advice above about the node's RPC is about a surface that holds no key and grants no authority. This one holds a key. There is no version of exposing it that is a good idea, and the tunnel costs one flag.

Two other shapes worth knowing:

  • zcd ui --locked starts without asking for a passphrase in the terminal; the browser asks instead. Useful when the terminal is shared or logged.
  • zcd ui --lock-after 5m shortens the idle lock, which wipes the key in place — the seed is overwritten, not merely dereferenced.

Peer identity and anonymity#

The node generates a fresh Ed25519 peer key on every start and never writes it to disk. It is derived from nothing — not from your wallet, not from a seed, not from anything else on the machine. Restarting rotates it.

If you are running a node in a way that matters to you, the thing that deanonymises you is not the key. It is the address. A bootstrap node you run on infrastructure traceable to you is a deanonymisation vector that no amount of key hygiene fixes.

Data directory#

data/
  chain/       blocks, state, and the write-ahead log
  peers.json   the persisted peer store

The peer store is persisted on purpose: a node that starts from a blank slate after every restart hands an attacker a fresh chance to fill it. Deleting it is safe but throws that away.

The node survives being killed at any moment — that is what the write-ahead log is for, and it is tested by killing nodes at random under network chaos. It does not survive its data directory being edited underneath it: on start it recomputes the state root and refuses to run if the stored one disagrees.

Choosing a network#

FlagNetworkChain idEngine
(none)mainnet1randomx-v1
--testnetpublic testnet2randomx-v1
--devnetlocal devnet1337development engine

The parameters are embedded in the binary, not read from a path. Every participant of a public network has to carry the same bytes or they are not on one network, and a file passed by --params is a file that drifts. A different network is a different network: the two disconnect at the handshake, and a data directory holding the wrong chain is refused at startup rather than silently mixed.

Recovering a damaged data directory#

zycordd carries a repair path for a data directory that a node refuses to start against. Stop the node first — zycordd repair --dir ./data takes the directory lock and refuses while a node holds it — then ask what happened with --dry-run before changing anything. Only one damage class is repairable in place; for the others the remedy is to resync, and the dry run is what tells you which one you have.