aere-node/run-node.sh

304 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 published bootnode is 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="e5038e6a1437bd85fa51294609edf2126a997f14f4fa7a65a0d39bd0aa24d2e5" # fleet genesis since 2026-09-10 (third config.pqRegistryHash entry at 18,082,816); 361709dc... before
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 published bootnode (RUN-A-NODE.md, bootnodes section): a public read host, not in the validator
# set. A second address was published from 2026-08-25 to 2026-09-11 and withdrawn. --bootnode overrides it.
BOOTNODE="enode://7e8ff740b79bf28a6d7e46ea8d2317cea4a223e7af60be36e91e13dc7051f4cb29077631452e699a5e18e640a94fd6e370e9e51dd91ecba1f2ba9666e07ff01b@37.27.216.110:30303"
say " no --bootnode given; using the published bootnode 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 far more than one seal (measured 2026-09-12 on the live chain: 14 to 20 in total, seven to ten
# per half since the certificate became hybrid), so all import cleanly. With the ceiling set the node
# shouts one ERROR per anchor by design (AERE-PQC-EMG-CEILING-01); RUN-A-NODE.md says what that is and
# what a real refusal looks like instead. 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"
# >>> OPTIUNI DERIVATE DIN LANTUL VIU (aduce-optiunile-urmaritorului.sh) - nu se scriu de mana
export BESU_OPTS="\
-Daere.basefee.floor.forkBlock=10141734 \
-Daere.basefee.floor.value=1000000000 \
-Daere.falcon.anchor.address=0xC01Bb2843EFAF92D8389F14aC1dD26dbeAf9144C \
-Daere.falcon.anchor.block=13889290 \
-Daere.falcon.attachBlock=13889296 \
-Daere.falcon.attachInterval=32 \
-Daere.falcon.forkBlock=14050000 \
-Daere.falcon.manifest=$REG/manifest-18082816.json \
-Daere.falcon.minAttachFutureMargin=5 \
-Daere.falcon.registry.history=$REG/manifest-13014000.json,$REG/registru-2800-v2-13600000.properties \
-Daere.falcon.validatorCount=10 \
-Daere.pq.anchorBlock=13014000 \
-Daere.pq.anchorInterval=32 \
-Daere.pq.anchorIntervalSchedule=17225968:128 \
-Daere.pq.anchor.maxSeals=9 \
-Daere.pq.anchorMinSeals=13014000:0,13034000:3,14961456:6,17102384:0,17102416:6 \
-Daere.pq.anchorV2Block=17047600 \
-Daere.pq.chainId=2800 \
-Daere.pq.hybridRegistry=$REG/hibrid-10.properties \
-Daere.pq.schemeSchedule=17047568:falcon-512+slh-dsa-sha2-128s \
-Daere.pq.anchor.minSealsCeiling=$ANCHOR_MIN_SEALS_CEILING"
# <<< OPTIUNI DERIVATE
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