aere-node/run-node.sh
Aere Network 184ec23cd4 Publish the two bootnodes; run-node.sh works with zero arguments now
Founder approved publishing the network entry points. Both are public read hosts
outside the validator set, so their downtime cannot cost consensus, and their
addresses were already public through the services they serve. One runs Besu and
one runs Nethermind, so a new node's first two peers are two independent
implementations. run-node.sh uses both by default; --bootnode still overrides.
The mail path stays as fallback for the day both are down.

This closes the last gap between "one command plus an email" and one command.
2026-08-25 01:11:25 +03:00

290 lines
15 KiB
Bash

#!/usr/bin/env bash
#
# run-node.sh - one command to build the Aere Network (chain 2800) fork and follow the chain
# from block 0: verify -> patch -> build -> sync -> validate the post-quantum anchor.
#
# This script is deterministic and idempotent. It checks its preconditions loudly, verifies the
# genesis and the registries by sha256 and refuses to run on a mismatch, fetches upstream Besu at
# the exact named commit, applies the Aere patches in order, builds the node, and starts it syncing
# chain 2800 from genesis with the registry and anchor configuration a following node needs.
#
# It holds no key and signs nothing. It configures a node that VALIDATES the chain, including the
# post-quantum certificate anchor, but does not PRODUCE seals; producing seals needs a validator
# Falcon key, which is not in this repository and is not required to follow the chain.
#
# Re-running it does not rebuild what is already built or re-apply patches already applied.
#
# Usage:
# ./run-node.sh [options] (the two published bootnodes are used unless --bootnode is given)
#
# Options:
# --bootnode <enode> Override the published bootnodes with your own peer for discovery.
# --data-path <dir> Node data directory. Default: ./aere-data next to this script.
# --build-dir <dir> Where upstream Besu is cloned and built. Default: ./.besu-build next to this script.
# --p2p-port <n> P2P (TCP+UDP) port. Default: 30303.
# --rpc-port <n> JSON-RPC HTTP port. Default: 8545.
# --sync-min-peers <n> Minimum peers before full sync starts. Default: Besu's default (5). Lower it
# (e.g. 1) on a young network where few public peers are reachable.
# --rebuild Force a rebuild even if a built node is already present.
# --no-start Do everything up to and including the build, then stop. Does not start the node.
# -h | --help Print this help and exit.
#
# Bootnode: chain 2800 does not publish a public bootnode in this repository yet (publishing a node's
# p2p address is an infrastructure decision, and validator addresses are deliberately not printed).
# Until a dedicated public bootnode exists, request a current enode by mail (office@aere.network,
# subject "bootnode") and pass it with --bootnode. Everything except peer discovery works without one.
#
set -euo pipefail
# --- constants ---------------------------------------------------------------------------------
BESU_UPSTREAM_URL="https://github.com/hyperledger/besu.git"
BASE_COMMIT="d2032017bb3b8cb215a97303980a1e4a643f7180" # Besu develop line after 26.4.0; see anchor/BASE.txt
GENESIS_SHA256="361709dccec4e9fc85be5aec33b30b58a55dc53424087d84c1ad11bd1d944c24"
NETWORK_ID=2800
# Patches applied in this order. 0002 (testnet precompiles) is deliberately NOT applied on mainnet.
PATCHES=(
"0001-aere-pqc-precompiles-mainnet.patch"
"0003-aere-pq-anchor.patch"
"0004-aere-basefee-floor.patch"
"0005-aere-eip2935-futureeips.patch"
)
# --- locate self -------------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- defaults ----------------------------------------------------------------------------------
BOOTNODE=""
DATA_PATH="$SCRIPT_DIR/aere-data"
BUILD_DIR="$SCRIPT_DIR/.besu-build"
P2P_PORT=30303
RPC_PORT=8545
REBUILD=0
NO_START=0
SYNC_MIN_PEERS="" # empty = Besu default (5); lower it on a young network with few reachable peers
# --- helpers -----------------------------------------------------------------------------------
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
ok() { printf ' \033[1;32mOK\033[0m %s\n' "$*"; }
die() { printf '\n\033[1;31mFATAL: %s\033[0m\n' "$*" >&2; exit 1; }
note() { printf ' %s\n' "$*"; }
usage() { awk 'NR>=2 && /^#/{sub(/^# ?/,"");print;next} NR>=2{exit}' "${BASH_SOURCE[0]}"; exit 0; }
sha256_of() { # portable: prefer sha256sum, fall back to shasum
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1
else die "no sha256sum or shasum on PATH; cannot verify file integrity"; fi
}
# --- parse args --------------------------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--bootnode) BOOTNODE="${2:-}"; shift 2 ;;
--data-path) DATA_PATH="${2:-}"; shift 2 ;;
--build-dir) BUILD_DIR="${2:-}"; shift 2 ;;
--p2p-port) P2P_PORT="${2:-}"; shift 2 ;;
--rpc-port) RPC_PORT="${2:-}"; shift 2 ;;
--sync-min-peers) SYNC_MIN_PEERS="${2:-}"; shift 2 ;;
--rebuild) REBUILD=1; shift ;;
--no-start) NO_START=1; shift ;;
-h|--help) usage ;;
*) die "unknown argument: $1 (try --help)" ;;
esac
done
say "Aere Network chain 2800 node setup"
note "bundle: $SCRIPT_DIR"
note "build dir: $BUILD_DIR"
note "data path: $DATA_PATH"
# --- 1. preconditions --------------------------------------------------------------------------
say "1/6 Checking preconditions"
command -v git >/dev/null 2>&1 || die "git not found on PATH"
ok "git: $(git --version | awk '{print $3}')"
command -v java >/dev/null 2>&1 || die "java not found on PATH; JDK 21 is required"
JAVA_VER_LINE="$(java -version 2>&1 | head -1)"
JAVA_MAJOR="$(printf '%s' "$JAVA_VER_LINE" | sed -E 's/.*version "([0-9]+).*/\1/')"
[ "$JAVA_MAJOR" = "21" ] || die "JDK 21 is required, found: $JAVA_VER_LINE"
ok "java: $JAVA_VER_LINE"
# Disk: a full sync from genesis to the live tip stores tens of GB (headers are ~95% of it),
# plus a few GB for the build. Warn (do not hard-fail) below ~40 GB free on the build filesystem.
FREE_KB="$(df -Pk "$SCRIPT_DIR" | awk 'NR==2{print $4}')"
FREE_GB=$(( FREE_KB / 1024 / 1024 ))
if [ "$FREE_GB" -lt 40 ]; then
note "WARNING: only ${FREE_GB} GB free here. A full from-genesis sync needs tens of GB; the run may fill the disk."
else
ok "disk: ${FREE_GB} GB free"
fi
# --- 2. verify genesis -------------------------------------------------------------------------
say "2/6 Verifying genesis.json"
[ -f "$SCRIPT_DIR/genesis.json" ] || die "genesis.json missing from bundle"
GOT="$(sha256_of "$SCRIPT_DIR/genesis.json")"
[ "$GOT" = "$GENESIS_SHA256" ] || die "genesis.json sha256 mismatch
expected $GENESIS_SHA256
got $GOT
This is not the chain-2800 genesis (a CRLF checkout will also fail here). Refusing to run."
ok "genesis.json sha256 $GOT"
# --- 3. verify registries ----------------------------------------------------------------------
say "3/6 Verifying Falcon validator registries"
REG_DIR="$SCRIPT_DIR/registries"
[ -d "$REG_DIR" ] || die "registries/ directory missing. A node cannot validate the anchor past block 13,014,000 without it."
for f in manifest-13014000.json manifest-13600000.json registru-2800-v2-13600000.properties; do
[ -f "$REG_DIR/$f" ] || die "registries/$f missing"
done
if [ -f "$REG_DIR/SHA256SUMS" ]; then
( cd "$REG_DIR" && \
if command -v sha256sum >/dev/null 2>&1; then sha256sum -c SHA256SUMS >/dev/null
else shasum -a 256 -c SHA256SUMS >/dev/null; fi ) \
|| die "registries failed their SHA256SUMS check. Refusing to run with tampered registries."
ok "registries verified against SHA256SUMS (7 keys @ 13,014,000; 9 keys @ 13,600,000)"
else
note "WARNING: registries/SHA256SUMS not found; skipping registry integrity check"
fi
# --- 4. fetch upstream Besu at the named base + apply patches -----------------------------------
say "4/6 Fetching upstream Besu at $BASE_COMMIT and applying Aere patches"
# sanity: the short base in anchor/BASE.txt must be a prefix of BASE_COMMIT
if [ -f "$SCRIPT_DIR/anchor/BASE.txt" ]; then
SHORT="$(grep -oE 'd[0-9a-f]{6,}' "$SCRIPT_DIR/anchor/BASE.txt" | head -1 || true)"
case "$BASE_COMMIT" in "$SHORT"*) : ;; *) [ -z "$SHORT" ] || die "BASE_COMMIT does not match anchor/BASE.txt ($SHORT)";; esac
fi
if [ ! -d "$BUILD_DIR/.git" ]; then
note "cloning upstream Besu (blob:none partial clone; this fetches ~the working tree, not full history)"
git clone --filter=blob:none "$BESU_UPSTREAM_URL" "$BUILD_DIR"
git -C "$BUILD_DIR" checkout "$BASE_COMMIT"
else
ok "upstream Besu clone already present"
fi
HEAD_NOW="$(git -C "$BUILD_DIR" rev-parse HEAD)"
[ "$HEAD_NOW" = "$BASE_COMMIT" ] || die "Besu build dir is not at the named base commit
expected $BASE_COMMIT
got $HEAD_NOW
Delete $BUILD_DIR and re-run to get a pristine base."
ok "at base commit $BASE_COMMIT"
apply_patch() {
local patch="$SCRIPT_DIR/patches/$1"
[ -f "$patch" ] || die "patch not found: $patch"
# already applied? (reverse-apply check succeeds only if the change is present)
if git -C "$BUILD_DIR" apply --reverse --check "$patch" >/dev/null 2>&1; then
ok "already applied: $1"
return 0
fi
# will it apply cleanly? fail loudly if the base moved under it.
git -C "$BUILD_DIR" apply --check "$patch" \
|| die "patch does not apply cleanly on this base: $1
This means the Besu tree is not the pristine named base. Delete $BUILD_DIR and re-run."
git -C "$BUILD_DIR" apply "$patch"
ok "applied: $1"
}
for p in "${PATCHES[@]}"; do apply_patch "$p"; done
# --- 5. build ----------------------------------------------------------------------------------
say "5/6 Building the node (gradlew installDist)"
BESU_BIN="$BUILD_DIR/build/install/besu/bin/besu"
if [ -x "$BESU_BIN" ] && [ "$REBUILD" -eq 0 ]; then
ok "built node already present: $BESU_BIN (use --rebuild to force)"
else
note "this compiles Besu from source and can take many minutes on the first run"
( cd "$BUILD_DIR" && ./gradlew --no-daemon installDist -x spotlessJavaCheck -x test )
[ -x "$BESU_BIN" ] || die "build finished but $BESU_BIN is missing"
ok "built: $BESU_BIN"
fi
if [ "$NO_START" -eq 1 ]; then
say "Stopping before start (--no-start). The node is built at:"
note "$BESU_BIN"
exit 0
fi
# --- 6. start ----------------------------------------------------------------------------------
say "6/6 Starting the node"
if [ -z "$BOOTNODE" ]; then
# The two published bootnodes (RUN-A-NODE.md, bootnodes section): both public read hosts,
# neither in the validator set, one Besu and one Nethermind. --bootnode overrides them.
BOOTNODE="enode://7e8ff740b79bf28a6d7e46ea8d2317cea4a223e7af60be36e91e13dc7051f4cb29077631452e699a5e18e640a94fd6e370e9e51dd91ecba1f2ba9666e07ff01b@37.27.216.110:30303,enode://a4b90d4f5fc7758814d41acfe00f6a2516683f3d3052410091b894d014dbb2184d6158da76f72e84ca268c2ccde3590d5686f978868f3b6ad6af87e398179fb8@157.180.67.185:30303"
say " no --bootnode given; using the two published bootnodes from RUN-A-NODE.md"
fi
mkdir -p "$DATA_PATH"
# BESU_OPTS: the follower subset of chain 2800's post-quantum configuration.
#
# WHY THESE AND NOT THE VALIDATOR SET. A validator additionally sets, and a follower deliberately
# OMITS: aere.falcon.key (the signing key, not in this repo), aere.falcon.attachBlock /
# attachInterval / minAttachFutureMargin / validatorCount (these drive seal PRODUCTION only), and
# aere.falcon.forkBlock (a per-block Falcon rule that the shipped code retires at the anchor block,
# so it is inert; leaving it unset keeps the non-blocking baseline). What is below is exactly what
# the IMPORT (validation) path reads, verified against anchor/ source:
# - registries: aere.falcon.manifest is the head registry; it loads only WITH
# aere.falcon.anchor.address (the manifest source is pending until that on-chain anchor is
# observed), so anchor.address / anchor.block are import-relevant here, not attach knobs.
# aere.falcon.registry.history carries every earlier registry the chain was signed against.
# - anchor validation: aere.pq.anchorBlock / anchorInterval / anchorMinSeals / anchor.maxSeals / chainId.
# - the 1 Gwei base-fee floor active from block 10,141,734: aere.basefee.floor.forkBlock / value.
# A node syncing from block 0 starts into the GENESIS validator set, which is small (N=3). The
# startup threshold guard (PqAnchorThresholdGuard) refuses to arm a seal threshold at or above that
# set's QBFT quorum, because a proposer at that set size could never gather it; the schedule reaches
# K=3 (block 13,034,000) and K=6 (block 14,961,456), both unreachable at N=3, so a from-genesis node
# refuses to start (AERE-PQC-THRESHOLD-01) without the line below. The emergency ceiling lowers the
# EFFECTIVE threshold to what the genesis set supports (quorum(3) - 1 = 1) WITHOUT editing the
# published schedule, exactly as that guard's own message instructs (--Xaere-pq-anchor-min-seals-max).
# This does not weaken the post-quantum binding a follower checks: every anchor must still carry a
# valid validator Falcon seal that verifies against the registry, and its digest must still bind the
# certificate under the block hash; only the seal-COUNT floor (a proposer-liveness property relative
# to the current set, not a history-verification property) is relaxed to one. Real anchor blocks
# carry three to nine seals, so all import cleanly. A validator, which starts into the full set and
# proposes, does not set this.
ANCHOR_MIN_SEALS_CEILING="${AERE_ANCHOR_MIN_SEALS_CEILING:-1}"
REG="$REG_DIR"
export BESU_OPTS="\
-Daere.basefee.floor.forkBlock=10141734 \
-Daere.basefee.floor.value=1000000000 \
-Daere.pq.chainId=2800 \
-Daere.pq.anchorBlock=13014000 \
-Daere.pq.anchorInterval=32 \
-Daere.pq.anchor.maxSeals=9 \
-Daere.pq.anchorMinSeals=13014000:0,13034000:3,14961456:6 \
-Daere.pq.anchor.minSealsCeiling=$ANCHOR_MIN_SEALS_CEILING \
-Daere.falcon.anchor.address=0x5214F0e53899Dad805ef3AdF0624F5e49d2a5EB5 \
-Daere.falcon.anchor.block=13889290 \
-Daere.falcon.manifest=$REG/manifest-13600000.json \
-Daere.falcon.registry.history=$REG/manifest-13014000.json,$REG/registru-2800-v2-13600000.properties"
note "BESU_OPTS (follower subset):"
printf '%s\n' "$BESU_OPTS" | tr ' ' '\n' | sed 's/^/ /'
note ""
note "bootnode: $BOOTNODE"
note "data path: $DATA_PATH"
note "rpc: http://127.0.0.1:$RPC_PORT (eth_blockNumber to watch height)"
note ""
note "Starting FULL sync from block 0. This verifies history rather than trusting a peer for state."
note "Crossing block 13,014,000 (first anchor) and 13,034,000 (first enforced 3-seal threshold)"
note "without anchor rejections is the proof that the registries and patches are correct."
SYNC_MIN_PEERS_ARG=()
if [ -n "$SYNC_MIN_PEERS" ]; then
SYNC_MIN_PEERS_ARG=(--sync-min-peers="$SYNC_MIN_PEERS")
note "sync-min-peers: $SYNC_MIN_PEERS (Besu default is 5; lower this only on a young network with few reachable peers)"
fi
exec "$BESU_BIN" \
--genesis-file="$SCRIPT_DIR/genesis.json" \
--data-path="$DATA_PATH" \
--network-id="$NETWORK_ID" \
--sync-mode=FULL \
--data-storage-format=BONSAI \
--bootnodes="$BOOTNODE" \
--p2p-port="$P2P_PORT" \
"${SYNC_MIN_PEERS_ARG[@]}" \
--rpc-http-enabled --rpc-http-port="$RPC_PORT" \
--rpc-http-api=ETH,NET,WEB3,QBFT \
--min-gas-price=0