146 lines
9.8 KiB
Bash
Executable File
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
|