Skip to content

Instantly share code, notes, and snippets.

@austintgriffith
Last active August 21, 2026 15:32
Show Gist options
  • Select an option

  • Save austintgriffith/0154ad39e2e562b4af1e9370e0edb286 to your computer and use it in GitHub Desktop.

Select an option

Save austintgriffith/0154ad39e2e562b4af1e9370e0edb286 to your computer and use it in GitHub Desktop.
Claude Code skill: set up & operate an Interfold ciphernode (FOLD staking) — incl. the gotchas the docs don't cover
name interfold-ciphernode
description Set up, register, and operate an Interfold ciphernode (FOLD staking). Use when the user wants to run a ciphernode, stake FOLD, bond/buy tickets, debug an interfold node, or help someone else set one up. Contains hard-won gotchas the official docs lack.

Interfold Ciphernode Setup & Operation

Official docs: https://docs.theinterfold.com/ciphernode-operators (running, registration, tickets) Dashboard (bond/register/tickets from a browser wallet): https://dashboard.theinterfold.com/#operator

Requirements

  • Linux box (8+ cores, 16G+ RAM), Ethereum RPC with both WebSocket and HTTP. The best RPC is your own local full node — the easy path is the BuidlGuidl client (https://client.buidlguidl.com — one command, runs reth + lighthouse). A remote RPC works too if the user prefers; that's the human's call. (Note the split-port gotcha in #2 below applies to local reth.)
  • UDP 9091 reachable from the internet (port forward through every NAT layer)
  • Operator hot wallet (generate fresh ON the box) + small gas ETH
  • Bond owner wallet (cold/Safe): 32,000 FOLD + 1,000 sUSDS per ticket + gas
  • Mainnet tokens (verify on-chain, don't trust UI copy): FOLD bond token 0xe172e9b6cfbeeb5593bdce3f077356fdb33af904; ticket token underlying is sUSDS 0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD; fee token is USDS 0xdC035D45d973E3EC169d2276DDab16f1e407384F. Ignore the dashboard's "testnet deployment" faucet banner on mainnet — real FOLD is what the bonding registry pulls.

Install (CLI path, recommended)

curl -fsSL https://raw.githubusercontent.com/theinterfold/interfold/main/install | bash
interfoldup install   # installs to ~/.local/bin/interfold

Gotchas the docs don't tell you (v0.9.0)

  1. interfold ciphernode setup and password delete demand a real TTY even with -p/-k flags ("IO error: not a terminal"). Headless: skip the wizard — write ~/.config/interfold/interfold.config.yaml by hand (docs have the full YAML with contract addresses), then interfold password set -p ... and interfold wallet set --private-key ... work fine non-interactively.
  2. One rpc_url, two protocols: interfold opens a WebSocket to rpc_url AND makes plain HTTP POSTs to the same URL. Nodes that split WS/HTTP across ports (reth: 8546 ws-only, 8545 http-only) 403 one of them → node dies with HTTP error 403 with empty body. Fix: tiny localhost mux that routes Upgrade requests to the ws port and POSTs to the http port; point rpc_url at the mux. (Node.js ~40 lines: http server + upgrade handler piping to net.connect.)
  3. Startup timeout crash loop: every start re-scans ALL chain history from the earliest deploy_block (no checkpoint, ~500+ chunks, 30-45 min) and the default startup_timeout_secs: 1800 kills it at minute 30 → infinite restart loop that looks alive (peers connect early!). Set startup_timeout_secs: 7200 under node:. Healthy startup ends with "Sync finished." + "LAUNCHING CIPHERNODE". Check systemctl show <unit> -p NRestarts — a climbing count means the loop.
  4. interfold nodes up --detach silently starts nothing with a single node: config. Use interfold start (foreground) under systemd/nohup instead.
  5. CLI vs running node: on-chain commands (ciphernode set-bond-owner, etc.) need exclusive DB access — stop the service, run the command, restart.
  6. autopassword: true generates its own keyfile at ~/.config/interfold/.interfold/config/_default/key — THAT file is the real secret; back it up (not whatever password you thought you set).
  7. interfold config check may say "mainnet: not in the published manifest, skipping" — manifest lags; docs' addresses are authoritative. (Fixed in ≥0.11.0: manifest now covers mainnet and reports "up to date".)

Updating to a new version

Releases ship fast (https://github.com/theinterfold/interfold/releases) — check often; version-skewed nodes can't peer (see below). Official path is interfoldup update, but the real flow has landmines; use a script like update-interfold.sh (in this gist) that does: version check → back up old binary → update → interfold config check → restart.

  • interfoldup can't overwrite a running binary ("Text file busy"). rm the binary first — the running process keeps its inode — then update, then restart.
  • No sudo needed with a Restart=always systemd unit: swap the binary, then pkill -x interfold; systemd relaunches on the new binary.
  • Every restart = full rescan (~40 min outage) — plan the window (gotcha 3).
  • Run interfold node validate (new in ≥0.10) BEFORE upgrading — it's the official pre-upgrade health check. It needs the DB lock: with a crash-looping/running unit, park the binary (mv it aside so systemd's restart can't exec) to free the lock.
  • 0.9.0 → 0.11.0 migration landmine: pre-0.10 event logs contain legacy events written to the wrong aggregate store (tagged chain 11155111 = Sepolia). 0.11.0 quarantines them, then dies with EventStore replay sequence gap → crash loop (node validate shows FAIL on aggregate 0). Fix (safe if data/ has no jobs/ key shares yet, i.e. the node has never served an E3): tar-backup the data dir, mv data/_default data/_default.pre-0.11, restart → node rebuilds from full rescan.
  • The wallet key lives in the data DB, not the config dir. After wiping data the node dies with No private key found in repository — re-run interfold wallet set --private-key ... from your key backup (needs exclusive DB access: park the binary again while you run it).
  • Startup buffer crash — the #1 post-update landmine (0.11.0+). During the ~40 min rescan the node buffers all live p2p gossip (fail-closed: it exits rather than drop a message). Default max_buffered_net_events: 1024 dies in ~1 min on current mainnet — gossip runs ~20 events/sec with ~20 peers (~1 heartbeat/peer/sec). Symptom: network startup buffer limit exceeded ... events=N/N → exit 1 → systemd crash loop where every attempt restarts the scan from chunk 1 and dies at the same wall-clock point. Size it as (observed events/sec) × (full-sync seconds) × 2; we run:
    node:
      max_buffered_net_events: 131072
    Memory is a non-issue (events ~220 B; the separate 256 MB byte cap is the backstop). The Event broadcast channel backpressure: 1024/1000 WARN during sync is a different internal queue — noisy, not fatal.
  • v0.11.0 isolated p2p networks per deployment: mismatched peers are dropped with "Rejected an incompatible Interfold peer". From an OLD node this looks like one peer connect/disconnect-flapping several times per second — that's your cue to upgrade, not a network problem.

Registration flow

  1. Fund operator wallet with gas ETH.
  2. From the node box: interfold ciphernode set-bond-owner --owner 0xCOLD_WALLET (sent BY the operator key — the dashboard cannot do this step).
  3. From the bond owner wallet on the dashboard: bond 32,000 FOLD → register → buy ticket(s) (1,000 sUSDS each; sUSDS keeps earning Sky yield while staked).
  4. Verify: interfold ciphernode status → Registered: true, Active: true.

Verification probes (prove reachability end-to-end)

QUIC version-negotiation probe — any live QUIC endpoint must reply (~31 bytes); run it from an EXTERNAL host against the public IP:9091/udp:

import socket, os
pkt = bytearray(1200); pkt[0] = 0xc0; pkt[1:5] = bytes.fromhex("fafafafa")
pkt[5] = 8; pkt[6:14] = os.urandom(8); pkt[14] = 8; pkt[15:23] = os.urandom(8)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(6)
s.sendto(bytes(pkt), ("PUBLIC_IP", 9091))
print(s.recvfrom(2048))  # reply = reachable; timeout = a hop is broken

Validate the probe against a known-good QUIC server (any Google IP :443) first. If the Interfold bootstrap itself seems down, probe _dnsaddr.bootstrap.interfold.network's resolved IP:port the same way from multiple networks before blaming local config.

Operations

  • systemd unit: ExecStart=<home>/.local/bin/interfold start -v, Restart=always, TimeoutStopSec=300, User=. Watch: journalctl -u interfold -f -o cat.
  • Backups (slashing risk if lost): ~/.config/interfold/ (config + keyfile) and ~/.config/interfold/.interfold/data/ (db + jobs/ encrypted key shares).
  • Don't publish the libp2p Peer ID alongside your identity — peers can map it to the node's IP for targeted DoS during committee rounds.
  • Every restart = full re-scan (~40 min outage). Restart rarely; batch config changes.

Economics (per Interfold dev guidance)

One node + stacked tickets beats many nodes: the 32k FOLD bond is per-node dead weight, sortition weight comes from tickets, and one node serves multiple committees concurrently (CPU-bound at high ticket counts — that's when a second node makes sense).

#!/usr/bin/env bash
# update-interfold.sh — update the interfold ciphernode binary and restart the node.
#
# Usage: ./update-interfold.sh # update binary only, tell you how to restart
# ./update-interfold.sh -y # update AND restart the node (~40 min rescan!)
#
# No sudo needed: interfoldup swaps ~/.local/bin/interfold in place, then we
# pkill the running node and systemd (Restart=always) relaunches on the new binary.
set -euo pipefail
BIN="$HOME/.local/bin/interfold"
UP="$HOME/.local/bin/interfoldup"
CUR=$("$BIN" --version | awk '{print $2}')
JSON=$(curl -fsSL https://api.github.com/repos/theinterfold/interfold/releases/latest)
LATEST=$(printf '%s' "$JSON" | grep -o '"tag_name": *"[^"]*"' | head -1 | sed -E 's/.*"v?([^"]+)"$/\1/')
echo "installed: $CUR latest: $LATEST"
if [ -z "$LATEST" ]; then
echo "ERROR: could not determine latest version from GitHub API" >&2
exit 1
fi
if [ "$CUR" = "$LATEST" ]; then
echo "Already up to date."
exit 0
fi
cp "$BIN" "$BIN.$CUR.bak"
echo "Backed up old binary -> $BIN.$CUR.bak"
# Unlink first: interfoldup can't overwrite a running binary (ETXTBSY).
# The running node keeps its inode; the new file lands at the same path.
rm -f "$BIN"
"$UP" update
NEW=$("$BIN" --version | awk '{print $2}')
echo "Binary updated: $CUR -> $NEW"
echo "Running config check against new binary..."
"$BIN" config check || echo "WARN: config check reported issues — review before restarting (rollback: cp $BIN.$CUR.bak $BIN)"
if [ "${1:-}" = "-y" ]; then
echo "Restarting node (pkill; systemd relaunches on new binary)..."
pkill -x interfold || true
echo "Restarted. NOTE: every restart = full chain rescan (~40 min outage)."
echo 'Watch: journalctl -u interfold -f -o cat (healthy end: "Sync finished." + "LAUNCHING CIPHERNODE")'
else
echo
echo "Binary swapped but the RUNNING node is still $CUR."
echo "Restart when ready (~40 min rescan): pkill -x interfold"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment