Skip to content

Repository files navigation

Consensus Node

Verifiable Bun worker node runtime for the Consensus Protocol. Nodes register with the orchestrator and serve proxied requests for the network.

Part of Consensus: consensus (orchestrator/server) · consensus-client (CLI + SDK) · consensus-node (this repo).

This runtime is built from scratch and is separate from the instance/ reference implementation that lives in the consensus monorepo.

Architecture direction (in progress): the data path is moving so clients connect directly to nodes instead of the orchestrator relaying everything over the control tunnel. The orchestrator stays the control plane (auth, x402 payment, node selection, signed tickets); each node will host its own reachable data endpoint, enforce SSRF locally (the consensus SSRF guard is being ported into this runtime), and keep a per-node cache. Until that lands, nodes serve work over the encrypted control tunnel as described below.

Development

bun install
bun run start

Default local API:

http://localhost:9090

State defaults to:

~/.consensus/node

Override with:

CONSENSUS_STATE_DIR=/path/to/state bun run start

Join Flow

Recommended interactive setup:

bun install
bun run setup

The setup wizard explains the network commitment, installs the server-approved runtime release, detects IPv4/optional IPv6, classifies region through the Consensus server, runs encrypted eval, verifies contact email, and registers the node. Email verification happens after eval so failed nodes do not spend an email API call.

Node evaluation checks route integrity, system capacity, SHA-256 CPU throughput, ChaCha20-Poly1305 throughput, and memory pressure before registration continues. Registration consumes the encrypted eval authorization; it does not require port forwarding or a public benchmark endpoint.

First run encrypted eval against the public server tunnel. A passing eval writes join-auth.json into the node state directory.

CONSENSUS_STATE_DIR=~/.consensus/node \
CONSENSUS_SERVER_URL=https://consensus.canister.software \
bun run eval

Then register the node with the join authorization:

CONSENSUS_SERVER_URL=https://consensus.canister.software \
CONSENSUS_NODE_IPV6=... \
CONSENSUS_NODE_IPV4=... \
CONSENSUS_NODE_PORT=9090 \
CONSENSUS_NODE_CONTACT=ops@example.com \
CONSENSUS_EMAIL_VERIFICATION_TOKEN=... \
CONSENSUS_EVM_ADDRESS=0x... \
CONSENSUS_SOLANA_ADDRESS=... \
CONSENSUS_ICP_ADDRESS=... \
bun run register

The interactive setup flow can collect these public payout addresses from MetaMask, Phantom, and Plug with a local browser page. To test that page without running the full setup flow:

bun run wallet:addresses

After registration, keep the outbound control tunnel open:

CONSENSUS_SERVER_URL=https://consensus.canister.software \
bun run control

Verify the registered node code and manifest:

CONSENSUS_SERVER_URL=https://consensus.canister.software \
bun run verify

The verification payload signs the node release manifest with the node Ed25519 identity. The server checks that signature against the registered node key and, when an admin-required manifest is set, compares version, platform, commit, routes hash, and tarball SHA.

Check whether this node matches the required server release:

CONSENSUS_SERVER_URL=https://consensus.canister.software \
bun run update

If the server requires a newer release and the manifest includes a verified artifact URL, download it into the node state directory:

CONSENSUS_SERVER_URL=https://consensus.canister.software \
bun run update -- --download

Release Build

Version bumps are explicit commits. Normal commits do not publish releases.

bun run version:bump -- patch
git add package.json
git commit -m "Bump node version"

Build a node artifact and matching server admin manifest payload:

bun run release -- \
  --version 0.1.0-alpha.0 \
  --commit "$(git rev-parse HEAD)" \
  --platform darwin-arm64 \
  --download-url https://consensus.canister.software/releases/consensus-node-0.1.0-alpha.0-darwin-arm64.tgz

The command writes the tarball, manifest, and /admin/manifest payload into dist/. Upload the tarball to the download_url, then post the admin payload to the Pi server with x-admin-key.

GitHub releases are manual. Run the Release workflow from GitHub Actions when you want to publish a real node artifact. If no version is provided in the workflow form, it uses the committed package.json version. The workflow does not run on ordinary commits.

Server-Directed Updates

The Consensus server decides when a node should update. It sends update_prepare over the encrypted control tunnel; the node downloads and verifies the release artifact, then replies update_ready. The server drains the node from routing and sends update_apply only when the router sees the node as idle.

On apply, the node acknowledges, runs the installer, closes its control tunnel with WebSocket code 1012, and exits cleanly with code 0. In production, run the node through PM2 with ecosystem.config.cjs; PM2 supervises scripts/run-control.sh and restarts from the new current symlink. The wrapper still handles legacy exit code 75 for older releases during migration. If CONSENSUS_NODE_UPDATE_COMMAND is set, the node runs it before exiting with:

CONSENSUS_NODE_UPDATE_ID
CONSENSUS_NODE_ARTIFACT_PATH
CONSENSUS_NODE_TARGET_VERSION

That command should install the verified artifact. If the command is not set, the runtime falls back to scripts/install-release.sh from the current release.

The default installer contract is:

export CONSENSUS_NODE_INSTALL_DIR="$HOME/.consensus/node-runtime"
export CONSENSUS_NODE_UPDATE_COMMAND="$CONSENSUS_NODE_INSTALL_DIR/current/scripts/install-release.sh"

scripts/install-release.sh unpacks the verified tarball into $CONSENSUS_NODE_INSTALL_DIR/releases/, installs production dependencies with the lockfile, and atomically moves the current symlink. scripts/run-control.sh always starts bun run control from that current release.

Recommended PM2 start:

CONSENSUS_NODE_INSTALL_DIR="$HOME/.consensus/node-runtime" \
CONSENSUS_STATE_DIR="$HOME/.consensus/node" \
CONSENSUS_SERVER_URL=https://consensus.canister.software \
"$HOME/.consensus/node-runtime/current/scripts/start-pm2.sh"

For reboot persistence, run pm2 startup, follow the command it prints, then run pm2 save.

The installer prunes old release directories after each successful install. It keeps the newest 3 releases by default; set CONSENSUS_NODE_RELEASE_RETENTION=1 to keep only the active release, except for the release currently finishing an update process.

For the first install, download a release tarball and run:

CONSENSUS_NODE_ARTIFACT_PATH=/path/to/consensus-node.tgz \
CONSENSUS_NODE_TARGET_VERSION=0.1.0-alpha.0 \
scripts/install-release.sh

scripts/ensure-pm2.sh installs missing macOS dependencies in order: Homebrew, Node.js/npm, then PM2. It also persists Homebrew shell setup in ~/.zprofile when Homebrew is installed or discovered outside PATH. The older A systemd/ template is still available if you do not want PM2.

Running headless (starts at boot, no login)

sudo scripts/install-launchd.sh

macOS only; on Linux install systemd/consensus-node.service instead. This installs a LaunchDaemon, not a LaunchAgent — agents load only once a user logs in, which is why pm2 startup is not used here. Before enabling the unit it runs bun run secrets:check as the account the daemon will run as, and refuses to install if that account cannot read the encryption data key without a login.

Two prerequisites for a truly headless node:

  • FileVault must be off. It halts the boot at a pre-boot unlock prompt, so nothing runs until someone types the password. For planned reboots on a FileVault machine, sudo fdesetup authrestart boots once unattended, but power loss still needs a human.
  • Automatic restart after a power cut, so the machine comes back at all: sudo pmset -a autorestart 1 (and sudo pmset -a sleep 0 to stop it sleeping).

Operating and proving it

sudo scripts/node-service.sh restart   # relaunch, then wait for the node to serve
sudo scripts/node-service.sh status
scripts/node-service.sh ping           # is the orchestrator seeing this node?
scripts/node-service.sh logs

restart relaunches the unit and then waits for the orchestrator to report this node active again. That distinction matters: a live pid only says the unit relaunched, while the orchestrator confirms the node reconnected and is serving. ping asks the same question on its own.

There is deliberately no command that claims to prove the node started before a login. Whether a given start was pre- or post-login is not something the machine can report reliably after the fact, so the check that earns its keep is operational — restart it and confirm it comes back and serves.

Existing nodes that predate the boot unit do not need the wizard: the supervisor logs headless-boot-NOT-configured at startup and /health reports headless_boot, so an operator can see which nodes would not survive an unattended reboot.

About

Template repository for joining the Consensus network

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages