run-node.sh folds the four manual steps (apply patches, build the fork, load the registries, set BESU_OPTS) into a single deterministic, idempotent command. It verifies the genesis sha256 and every registry against SHA256SUMS and refuses on mismatch; applies patches 0001,0003,0004,0005 on a pristine upstream checkout (0002 testnet excluded) with a reverse-apply idempotence check; builds with installDist; starts a follower with the correct BESU_OPTS. It holds no key. The registries a from-genesis follower needs were missing from this package, only the code that reads them was here, so nobody outside could cross block 13,014,000. Added them as public data (Falcon public keys, validator addresses, possession proofs; no private material), byte-identical across three independent validators, with SHA256SUMS and a provenance README. Secret gate passes. Proven from a fresh git clone in WSL, as a stranger: built (BUILD SUCCESSFUL), idempotent on re-run, started, loaded all three registries with verified possession proofs, block-0 hash equals the network genesis, and synced from block 0 with zero anchor rejections and zero stateroot mismatches. A real finding this test surfaced, fixed and documented: a from-genesis node starts into the genesis validator set N=3, where the published seal schedule (K=3 at 13,034,000, K=6 at 14,961,456) is unreachable, so PqAnchorThresholdGuard refuses to start. The fix baked in is the guard's own documented remedy, minSealsCeiling=1, which relaxes only the seal-count floor (a proposer-liveness property) without weakening the PQ binding: every anchor still needs a valid registry-verified Falcon seal. A validator does not set this. Honest limit: crossing 13,014,000 was not reached in-session because the two public bootnodes serve historical block bodies at only ~7 blk/s sustained (ETA ~19 days), and syncing bodies from validators would breach the read-only fleet rule. The crossing rests on these registries being byte-identical to those in the Aug 15 import proof that reached the live tip.
290 lines
14 KiB
Bash
290 lines
14 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 --bootnode <enode://...@host:port> [options]
|
|
#
|
|
# Options:
|
|
# --bootnode <enode> Peer to dial for discovery. Required to actually sync (see "Bootnode" below).
|
|
# --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
|
|
die "no --bootnode given. A node with no peers cannot sync.
|
|
Chain 2800 does not publish a public bootnode in this repository yet. Request a current enode by
|
|
mail (office@aere.network, subject \"bootnode\") and re-run with:
|
|
./run-node.sh --bootnode <enode://...@host:port>"
|
|
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
|