#!/usr/bin/env bash # # validator-candidacy.sh - write the candidacy file a chain-2800 validator candidate sends to the # Foundation: the registry row, the enode, a proof of possession of all three keys, and a signed # measurement of this machine's clock against the chain head. PUBLIC data only. # # What it does, in order, on THIS machine: # 1. checks that the build is from the published patch series and that the three keys are in place, # owner-only (validator-keys.sh made the two post-quantum ones); # 2. asks YOUR running node (its local RPC) for its enode, and refuses if the node runs with another # key than /key: the consensus key must be the node key; # 3. measures the clock: the chain head's timestamp against this machine's clock, several times. Chain # 2800 rejects a header more than ONE second ahead of the node's clock # (TimestampBoundedByFutureParameter(1)): a node whose clock is behind refuses the head blocks and # disconnects its peers (BREACH_OF_PROTOCOL); a node whose clock is ahead makes the others refuse # ITS blocks. Refuses outside [-2 s ahead, 1 s behind]; # 4. writes the registry row with PqRegistryV3Tool row (your build, patch 0008): the id derived from # your Falcon key, both public keys, a proof of possession by each, the link signed by the node key, # and transport = the node key's address; # 5. signs the candidacy message with all three keys (validator/README.md, "The candidacy file"), # verifies its own signatures, and writes the candidacy file; # 6. checks that no private key value appears in anything it wrote or printed. # # Usage: # validator/validator-candidacy.sh --params --enode-host [options] # validator/validator-candidacy.sh --clock-only --params --enode-host [options] # (on the day of the vote: only a fresh, signed clock report, written to --out) # # --enode-host the public host or IP at which the validators reach your node (P2P) # --p2p-port your P2P port. Default: 30303 # --rpc your node's LOCAL JSON-RPC. Default: http://127.0.0.1:8545 # --build-dir Default: ./.besu-build next to run-node.sh # --data-path Default: ./aere-data next to run-node.sh # --keys-dir Default: $HOME/.aere-validator/keys # --samples clock samples, two seconds apart. Default: 12 # --out Default: ./aere-candidacy.json (./aere-clock-report.json with --clock-only) # -h | --help # set -euo pipefail . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/kit-common.sh" PARAMS=""; HOSTP=""; P2P=30303; RPC="http://127.0.0.1:8545"; BUILD_DIR="$BUNDLE_DIR/.besu-build"; DATA_PATH="$BUNDLE_DIR/aere-data" KEYS_DIR="${HOME}/.aere-validator/keys"; SAMPLES=12; OUT=""; CLOCK_ONLY=0 usage() { awk 'NR>=2 && /^#/{sub(/^# ?/,"");print;next} NR>=2{exit}' "${BASH_SOURCE[0]}"; exit 0; } while [ $# -gt 0 ]; do case "$1" in --params) PARAMS="${2:-}"; shift 2 ;; --enode-host) HOSTP="${2:-}"; shift 2 ;; --p2p-port) P2P="${2:-}"; shift 2 ;; --rpc) RPC="${2:-}"; shift 2 ;; --build-dir) BUILD_DIR="${2:-}"; shift 2 ;; --data-path) DATA_PATH="${2:-}"; shift 2 ;; --keys-dir) KEYS_DIR="${2:-}"; shift 2 ;; --samples) SAMPLES="${2:-}"; shift 2 ;; --out) OUT="${2:-}"; shift 2 ;; --clock-only) CLOCK_ONLY=1; shift ;; -h|--help) usage ;; *) die "unknown argument: $1 (try --help)" ;; esac done [ -n "$PARAMS" ] && [ -f "$PARAMS" ] || die "--params is required" [ -n "$HOSTP" ] || die "--enode-host is required" [ "$SAMPLES" -ge 3 ] 2>/dev/null || die "--samples must be a number >= 3" [ -n "$OUT" ] || { [ "$CLOCK_ONLY" = 1 ] && OUT="./aere-clock-report.json" || OUT="./aere-candidacy.json"; } [ -e "$OUT" ] && die "$OUT exists; it is never overwritten" command -v curl >/dev/null 2>&1 || die "curl not found" say "1/6 The build and the keys" kit_find_lib "$BUILD_DIR" read -r CHAIN BIND COUNT INDEX < <(kit_java params --params "$PARAMS" 2>/dev/null) || die "the admission parameters do not parse" [ -n "${INDEX:-}" ] || die "the admission parameters do not parse" ok "chain $CHAIN, epoch bound at $BIND, $COUNT rows, your index $INDEX" NK="$DATA_PATH/key"; FK="$KEYS_DIR/falcon-512.key"; SK="$KEYS_DIR/slh-dsa-sha2-128s.key" [ -f "$NK" ] && [ -f "$FK" ] && [ -f "$SK" ] || die "keys missing ($NK, $FK, $SK); run validator/validator-keys.sh first" need_private_dir "$KEYS_DIR"; need_private_file "$NK"; need_private_file "$FK"; need_private_file "$SK" W="$(mktemp -d "$KEYS_DIR/.candidacy.XXXXXX")"; trap 'rm -rf "$W"' EXIT kit_java ids --node-key-file "$NK" --falcon-key "$FK" --slh-key "$SK" > "$W/ids.json" 2> "$W/ids.log" || die "the keys do not form one identity (see $W/ids.log)" NODEPUB="$(grep -o '"nodePublicKey" : "0x[0-9a-f]*"' "$W/ids.json" | grep -o '0x[0-9a-f]*' | sed 's/^0x//' || true)" [ "${#NODEPUB}" = 128 ] || die "could not derive the node public key" rpc() { curl -s -m 10 -H 'content-type: application/json' --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":$2}" "$RPC" 2>/dev/null || true; } say "2/6 Your node: the key it runs with" R="$(rpc net_enode '[]')" RUNPUB="$(printf '%s' "$R" | grep -oE 'enode://[0-9a-f]{128}' | head -1 | sed 's#enode://##' || true)" [ -n "$RUNPUB" ] || die "your node does not answer net_enode at $RPC (start it with run-node.sh; the NET API is enabled there). A silent node is not a measurement" [ "$RUNPUB" = "$NODEPUB" ] || die "your running node uses ANOTHER node key than $NK. The consensus key must be the node key: a validator whose node key differs receives no consensus message. Start the node with --data-path $DATA_PATH" CID="$(rpc eth_chainId '[]' | grep -oE '"result":"0x[0-9a-f]+"' | grep -oE '0x[0-9a-f]+' || true)" [ -n "$CID" ] && [ "$((CID))" = "$CHAIN" ] || die "your node is not on chain $CHAIN (eth_chainId says ${CID:-nothing})" ok "the node at $RPC runs with the key in $NK (enode public key matches), chain $CHAIN" say "3/6 The clock, against the chain head ($SAMPLES samples)" SY="$(rpc eth_syncing '[]')" printf '%s' "$SY" | grep -q '"result":false' || die "your node is still syncing; measure the clock when it follows the head" DMIN=""; DMAX=""; HEAD=0 for i in $(seq 1 "$SAMPLES"); do B="$(rpc eth_getBlockByNumber '["latest",false]')" NOW="$(date +%s)" TS="$(printf '%s' "$B" | grep -oE '"timestamp":"0x[0-9a-f]+"' | grep -oE '0x[0-9a-f]+' | head -1 || true)" NUM="$(printf '%s' "$B" | grep -oE '"number":"0x[0-9a-f]+"' | grep -oE '0x[0-9a-f]+' | head -1 || true)" [ -n "$TS" ] && [ -n "$NUM" ] || die "the node did not return the head block (sample $i); an empty answer is not a measurement" D=$(( TS - NOW )); HEAD=$((NUM)) if [ -z "$DMIN" ] || [ "$D" -lt "$DMIN" ]; then DMIN=$D; fi if [ -z "$DMAX" ] || [ "$D" -gt "$DMAX" ]; then DMAX=$D; fi if [ "$i" -lt "$SAMPLES" ]; then sleep 2; fi done AT="$(date +%s)" note "head timestamp minus local clock: from $DMIN to $DMAX s over $SAMPLES samples (head $HEAD)" [ "$DMAX" -le 1 ] || die "your clock is BEHIND the chain by up to $DMAX s. A node more than one second behind rejects the head blocks (TimestampBoundedByFutureParameter(1)) and disconnects its peers with BREACH_OF_PROTOCOL. Fix the clock (NTP or chrony), then measure again" [ "$DMIN" -ge -2 ] || die "the head is $(( -DMIN )) s older than your clock: your clock is AHEAD, or your node is lagging. A validator whose clock runs ahead proposes blocks the others refuse. Fix the clock or let the node catch up, then measure again" ok "clock within the margin (at most 1 s behind, at most 2 s ahead)" if [ "$CLOCK_ONLY" = 1 ]; then say "4/4 The signed clock report" kit_java clock-report --chain-id "$CHAIN" --node-key-file "$NK" --enode-host "$HOSTP" --p2p-port "$P2P" \ --clock-measured-at "$AT" --clock-head "$HEAD" --clock-samples "$SAMPLES" --clock-dmin "$DMIN" --clock-dmax "$DMAX" \ --out "$OUT" > "$W/out.json" 2> "$W/out.log" || die "the clock report was not written (see the message above)" kit_no_private_in "$FK" "$SK" "$NK" "$OUT" "$W/out.json" "$W/out.log" sed 's/^/ /' "$W/out.json" printf '\n Send %s to the Foundation (it holds only public data and one signature by your node key).\n' "$OUT" exit 0 fi say "4/6 The registry row (PqRegistryV3Tool row, your build)" SLHPUB="$(grep -E '^pk=' "$SK" | cut -d= -f2 | tr -d '\r ' || true)" [ "${#SLHPUB}" = 64 ] || die "the SLH-DSA key file has no 32-byte public key" java -Xmx512m -cp "$LIB/*" org.hyperledger.besu.consensus.common.bft.tools.PqRegistryV3Tool row --chain-id "$CHAIN" --bind "$BIND" \ --count "$COUNT" --index "$INDEX" --falcon-key "$FK" --slh-key "$SK" --slh-pub "0x$SLHPUB" --ecdsa-key-file "$NK" --transport \ > "$W/row.json" 2> "$W/row.log" < /dev/null || die "PqRegistryV3Tool row failed (see the tool's message in $W/row.log before it is removed: $(head -c 300 "$W/row.log" | tr '\n' ' '))" ok "row written for index $INDEX" say "5/6 The candidacy proof and file" kit_java candidacy --params "$PARAMS" --row "$W/row.json" --falcon-key "$FK" --slh-key "$SK" --node-key-file "$NK" \ --enode-host "$HOSTP" --p2p-port "$P2P" --issued "$(date -u +%F)" --clock-measured-at "$AT" --clock-head "$HEAD" \ --clock-samples "$SAMPLES" --clock-dmin "$DMIN" --clock-dmax "$DMAX" --out "$OUT" > "$W/out.json" 2> "$W/out.log" \ || die "the candidacy was not written: $(grep -m1 REFUSED "$W/out.log" || head -c 300 "$W/out.log")" say "6/6 Nothing private in what was written" kit_no_private_in "$FK" "$SK" "$NK" "$OUT" "$W/row.json" "$W/row.log" "$W/out.json" "$W/out.log" "$W/ids.json" "$W/ids.log" sed 's/^/ /' "$W/out.json" cat <