aere-node/validator/validator-candidacy.sh

146 lines
9.8 KiB
Bash
Executable File

#!/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 <data-path>/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 <admission-params.json> --enode-host <host> [options]
# validator/validator-candidacy.sh --clock-only --params <admission-params.json> --enode-host <host> [options]
# (on the day of the vote: only a fresh, signed clock report, written to --out)
#
# --enode-host <host> the public host or IP at which the validators reach your node (P2P)
# --p2p-port <n> your P2P port. Default: 30303
# --rpc <url> your node's LOCAL JSON-RPC. Default: http://127.0.0.1:8545
# --build-dir <dir> Default: ./.besu-build next to run-node.sh
# --data-path <dir> Default: ./aere-data next to run-node.sh
# --keys-dir <dir> Default: $HOME/.aere-validator/keys
# --samples <n> clock samples, two seconds apart. Default: 12
# --out <file> 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 <admission-params.json> is required"
[ -n "$HOSTP" ] || die "--enode-host <your public P2P host or IP> 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 <<EOF
Send $OUT to the Foundation. It holds your registry row, your enode, three signatures and a
signed clock measurement: public data only. The Foundation verifies it independently
(verifica-candidatura) before your row is put into a registry epoch.
EOF