#!/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 Override the published bootnodes with your own peer for discovery. # --data-path Node data directory. Default: ./aere-data next to this script. # --build-dir Where upstream Besu is cloned and built. Default: ./.besu-build next to this script. # --p2p-port P2P (TCP+UDP) port. Default: 30303. # --rpc-port JSON-RPC HTTP port. Default: 8545. # --sync-min-peers 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 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" # >>> 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