Run a Peer
A peer is the base unit of the Liqua network: a booted genesis instance that finds other peers, opens an encrypted channel to them, and gossips transactions and blocks. Everything else — mining, validation — rides on top of this layer. This page gets one running and shows you how to drive it.
The networking node lives in liqua/node/ and has its own dependencies. All commands on this page
are run from inside that folder unless noted. It's ours, not geth — same discv4 design, but Liqua-native:
lnode:// URIs, JSON payloads, and a node's identity is its genesis instance
(R6 · instance = auth). We build on audited crypto (@noble secp256k1 / keccak / ChaCha20) — we don't roll our own.
What a peer is
A node's identity is derived from a seed — the key of a (public, private) genesis instance:
a node = a booted genesis instance
identity priv = key of the (public, private) instance (--seed)
node-id compressed secp256k1 pubkey → lnode://<id>@host:port
dht-key keccak256(uncompressed pubkey) → Kademlia distance metric
Peers are addressed by an lnode://<node-id>@host:port URI — the Liqua analog of an enode. The
node-id is the public key; the sender of every packet is recovered from its signature, so no
pubkey ever travels on the wire.
Install
cd liqua/node
npm install
Three dependencies, all audited: @noble/curves (secp256k1), @noble/hashes (keccak256),
@noble/ciphers (ChaCha20-Poly1305). Node ≥18.
Boot a node · the GO start
start.mjs brings the whole stack up in one command — BOOT IS GENESIS (R7):
GENESIS → IDENTITY → DISCOVERY → TRANSPORT → READY. The first node is the genesis seed;
everyone else bootstraps from it.
# ① be the genesis seed node (others bootstrap from you)
node start.mjs --seed genesis --port 30303
# ② join from another terminal / machine, pointing at the seed
node start.mjs --seed me --port 30304 --bootnode lnode://<id>@127.0.0.1:30303
One --port serves both UDP discovery and TCP transport. The seed prints its own
lnode:// URI at boot — copy that into the joiner's --bootnode. There's also an
npm run go shortcut for the seed node.
If you only want the peer-finding daemon (no transport / gossip), run liqd.mjs instead — the
geth-like discv4 bootnode. Useful as a standalone, always-on rendezvous point:
node liqd.mjs --seed liqua-genesis-boot --port 30303
node liqd.mjs --seed alice --port 30304 --bootnode lnode://<id>@127.0.0.1:30303
Live dashboard
For a browser UI over a running peer — identity, chain head, connected peers, recent blocks, a live event log, and a PRODUCE BLOCK button — boot the dashboard server. It runs a full node (discovery + transport + gossip + chain + control) and serves the UI, because a browser can't reach the raw-TCP control plane directly:
node server.mjs --port 30303 --http 8040 --seed genesis
# dashboard → http://localhost:8040 · node udp/tcp :30303 · liqctl :31303
Open http://localhost:8040. State streams over SSE; the
liqctl socket stays open alongside it. The
Server Hub launches this as node · peer dashboard, and every route is in
the API reference.
Boot one dashboard node and hit PRODUCE BLOCK a few times — height climbs and blocks stream in,
no second machine needed. Then boot a second node pointed at the first (--bootnode) to watch discovery
and sync go live.
Command-line flags
| Flag | Meaning |
|---|---|
--port <n> | UDP+TCP port (default 30303) |
--host <ip> | bind host (default 127.0.0.1) |
--seed <s> | genesis instance seed (hex32 or phrase) → this node's identity. Omit for a random identity. |
--bootnode <uri> | lnode://… to bootstrap from. Repeatable. |
--genesis <file> | a genesis.json carrying a bootnodes[] list |
The --seed derives the private key that is your node. Anyone with the seed can impersonate
the node on the network. Use a real, secret seed for anything beyond localhost; the phrases above
(genesis, alice) are for devnet demos only.
How peers find & trust each other
Four phases, in order. The first three are discovery; the fourth is the genesis gate that keeps your mesh on the same chain.
Bootstrap
PINGthe bootnodes from--bootnode/genesis.json.Bond — both directions
PING ⇄ PONGin both directions before a peer is usable. This bidirectional endpoint-proof is anti-amplification and was the fix for the early "peers find the bootnode but not each other" bug.Learn the graph
FINDNODE(target) → NEIGHBORS(k closest), then bond the new peers. Random-target refresh lookups keep the Kademlia routing table live.Genesis gate
Over the encrypted channel, the first message is STATUS. A peer is accepted only if its
chainIdand genesis (the SOMA-256 root) match yours. Peers on a different genesis are rejected at the gate — the mesh agrees on genesis before block 1.
The wire packet is MAGIC('LIQD') · sig(64) · recid(1) · payloadJSON. Stale packets (20 s TTL)
and bad signatures are dropped. Once bonded and gated, the transport is an authenticated
encrypted TCP channel — secp256k1 ECDH → keccak key → ChaCha20-Poly1305 frames —
over which tx and block messages gossip (deduped by hash, relayed onward).
Drive a running node · liqctl
A full node exposes an admin control plane on port + 1000 (line-JSON over TCP).
liqctl is the operator's handle on it:
node liqctl.mjs --port 30303 status # name · height · head · peers · miner
node liqctl.mjs --port 30303 head # the current canonical head block
node liqctl.mjs --port 30303 chain # chain summary
node liqctl.mjs --port 30303 peers # discovered / connected / evm peers
node liqctl.mjs --port 30303 produce '{"n":1}' # produce N blocks (operator-driven)
node liqctl.mjs --port 30303 stop # graceful shutdown
Pass --port as the node's UDP/TCP port — liqctl adds 1000 to reach the control socket
(you can also target it directly with --control 31303). A status reads like:
liqctl · status → control:31303
name node-30303 lnode://02af…@127.0.0.1:30303
height #6 head 8f2c1a09e4b7…
stateRoot 0x37d309f2091ec80a…
peers 2 evm / 3 tcp / 4 discovered
miner idle
Block sync is by re-execution: a node validates a gossiped block by deterministically
re-running produceBlock and checking the SOMA root matches. A late-joining peer catches up with
getblocks / blocks range requests, then converges on the same head and state root as everyone else.
Prove it · the mesh demos
Two self-contained gates that spin up multiple peers in one process and exit 0 only on success:
npm run demo # bootnode + 3 peers · exits 0 when the full discovery mesh forms
npm run evmdemo # bootnode + 2 peers form an ENCRYPTED genesis mesh, gossip a tx,
# and REJECT an intruder booted on the wrong genesis
Run npm run evmdemo first — a clean exit 0 proves discovery, the encrypted transport,
the genesis gate, and tx gossip all work on your machine before you wire up real peers.
Files in node/
| File | Role |
|---|---|
identity.mjs | node key / id / dht-key · lnode:// · sign & recover |
kademlia.mjs | XOR-distance k-bucket routing table |
wire.mjs | signed packet encode/decode (PING / PONG / FINDNODE / NEIGHBORS) |
discover.mjs | the UDP daemon — bonding, lookup, refresh |
genesis-id.mjs | the chain's genesis id (SOMA root) peers must agree on |
transport.mjs | encrypted TCP channel — ECDH + ChaCha20-Poly1305 |
protocol.mjs | EVM sub-protocol — STATUS genesis-gate + tx / block gossip |
node.mjs · full.mjs | LiquaNode / FullNode = discovery + transport + protocol + chain |
control.mjs · liqctl.mjs | admin control plane (port + 1000) + its CLI |
liqd.mjs · start.mjs | discovery-only CLI · full-node GO boot |
demo.mjs · evmdemo.mjs · sync-demo.mjs | the mesh / EVM / late-join sync test gates |
Honest limits
- No NAT traversal yet — works on LAN, public IPs, and localhost. ENR-style signed records and discv5 topic discovery are the next step.
- JSON-over-UDP is simple, not byte-optimal; RLP framing is a later optimization.
- Bonding resists amplification to spoofed sources; a full eclipse-attack hardening pass (bucket eviction policy, IP limits) is roadmap.
- The L2 reward-claim path (§5) is not wired — see the roadmap.
Next: with peers talking, you can produce blocks or stake and validate. Every endpoint touched here is catalogued in the API reference.