Wire HDHomeRun observations and recover Forge OP Stack

This commit is contained in:
every.channel 2026-05-03 20:24:04 -07:00
parent 8065860449
commit 0d86104762
No known key found for this signature in database
18 changed files with 1613 additions and 58 deletions

55
scripts/e2e-hdhr-blockchain.sh Executable file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${root}"
host="${EVERY_CHANNEL_E2E_HDHR_HOST:-}"
channel="${EVERY_CHANNEL_E2E_HDHR_CHANNEL:-}"
usage() {
cat >&2 <<'EOF'
usage:
scripts/e2e-hdhr-blockchain.sh --host <HDHR_HOST> --channel <CHANNEL>
notes:
- starts a local Anvil chain
- deploys the observation registry and ledger with quorum=1
- runs ec-node against the HDHomeRun source
- verifies that the published manifest observation finalizes on-chain
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--host)
host="${2:-}"
shift 2
;;
--channel)
channel="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "error: unknown arg: $1" >&2
usage
exit 2
;;
esac
done
if [[ -z "${host}" || -z "${channel}" ]]; then
echo "error: --host and --channel are required" >&2
usage
exit 2
fi
export EVERY_CHANNEL_E2E_HDHR_HOST="${host}"
export EVERY_CHANNEL_E2E_HDHR_CHANNEL="${channel}"
nix develop --accept-flake-config -c \
bash -lc 'cargo test -p ec-node --test e2e_hdhr_blockchain -- --ignored --nocapture'

257
scripts/hetzner-robot-forge.sh Executable file
View file

@ -0,0 +1,257 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${root}"
robot_base="${EVERY_CHANNEL_ROBOT_API_BASE:-https://robot-ws.your-server.de}"
server="${EVERY_CHANNEL_ROBOT_SERVER:-2800441}"
host_ip="${EVERY_CHANNEL_FORGE_IP:-95.216.114.54}"
host_name="${EVERY_CHANNEL_FORGE_HOSTNAME:-git.every.channel}"
op_item="${EVERY_CHANNEL_ROBOT_OP_ITEM:-Hetzner Robot}"
op_vault="${EVERY_CHANNEL_ROBOT_OP_VAULT:-}"
ssh_identity="${EVERY_CHANNEL_FORGE_SSH_IDENTITY:-$HOME/.ssh/id_ed25519}"
ssh_pub="${EVERY_CHANNEL_FORGE_SSH_PUBLIC_KEY:-${ssh_identity}.pub}"
usage() {
cat <<EOF
Usage: $0 <command>
Commands:
probe Check public HTTPS and SSH reachability for ${host_name}.
server Query Robot server metadata.
status Query Robot reset and rescue status.
rescue-status Query Robot rescue status.
activate-rescue Activate Linux rescue mode for ${server}.
reset [type] Execute a Robot reset, default: hw.
recover [type] Activate rescue mode, then execute a reset.
wait-ssh Wait until TCP/22 answers on ${host_ip}.
Credentials:
Set EVERY_CHANNEL_ROBOT_USER and EVERY_CHANNEL_ROBOT_PASSWORD, or sign in with
1Password CLI and keep the existing item named "${op_item}" available.
Optional:
EVERY_CHANNEL_ROBOT_OP_ITEM default: Hetzner Robot
EVERY_CHANNEL_ROBOT_OP_VAULT optional 1Password vault scope
EVERY_CHANNEL_ROBOT_AUTHORIZED_KEY_FINGERPRINT
EVERY_CHANNEL_ROBOT_PRINT_SENSITIVE=1 print Robot-generated rescue password
EVERY_CHANNEL_ROBOT_RESCUE_OS=linux
EVERY_CHANNEL_ROBOT_RESCUE_KEYBOARD=us
EVERY_CHANNEL_ROBOT_RESET_TYPE=hw
EOF
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "error: required command not found: $1" >&2
exit 2
fi
}
op_field() {
local field="$1"
if [[ -n "${op_vault}" ]]; then
op item get "${op_item}" --vault "${op_vault}" --fields "label=${field}" --reveal
else
op item get "${op_item}" --fields "label=${field}" --reveal
fi
}
load_robot_auth() {
robot_user="${EVERY_CHANNEL_ROBOT_USER:-${HETZNER_ROBOT_USER:-}}"
robot_password="${EVERY_CHANNEL_ROBOT_PASSWORD:-${HETZNER_ROBOT_PASSWORD:-}}"
if [[ -z "${robot_user}" || -z "${robot_password}" ]]; then
require_cmd op
robot_user="${robot_user:-$(op_field username)}"
robot_password="${robot_password:-$(op_field password)}"
fi
if [[ -z "${robot_user}" || -z "${robot_password}" ]]; then
echo "error: Robot credentials are not available" >&2
echo "hint: run 'op signin' or export EVERY_CHANNEL_ROBOT_USER and EVERY_CHANNEL_ROBOT_PASSWORD" >&2
exit 2
fi
}
robot_curl() {
local method="$1"
local path="$2"
shift 2
local config
config="$(mktemp "${TMPDIR:-/tmp}/ec-robot-curl.XXXXXX")"
chmod 600 "${config}"
cleanup_config() {
rm -f "${config}"
}
trap cleanup_config RETURN
{
printf 'url = "%s%s"\n' "${robot_base}" "${path}"
printf 'request = "%s"\n' "${method}"
printf 'user = "%s:%s"\n' "${robot_user}" "${robot_password}"
printf 'silent\nshow-error\nfail\n'
} >"${config}"
curl --config "${config}" "$@"
}
mask_sensitive_json() {
if [[ "${EVERY_CHANNEL_ROBOT_PRINT_SENSITIVE:-0}" == "1" ]]; then
cat
return
fi
if command -v jq >/dev/null 2>&1; then
jq 'walk(if type == "object" and has("password") then .password = "<redacted>" else . end)'
else
sed -E 's/"password"[[:space:]]*:[[:space:]]*"[^"]*"/"password":"<redacted>"/g'
fi
}
authorized_key_fingerprint() {
if [[ -n "${EVERY_CHANNEL_ROBOT_AUTHORIZED_KEY_FINGERPRINT:-}" ]]; then
printf '%s\n' "${EVERY_CHANNEL_ROBOT_AUTHORIZED_KEY_FINGERPRINT}"
return
fi
if [[ -f "${ssh_pub}" ]] && command -v ssh-keygen >/dev/null 2>&1; then
ssh-keygen -E md5 -lf "${ssh_pub}" | awk '{print $2}' | sed 's/^MD5://'
fi
}
probe() {
echo "== HTTPS via DNS =="
curl --max-time 12 -I -sS "https://${host_name}/" || true
echo
echo "== HTTPS via pinned IP =="
curl --max-time 12 -I -sS --resolve "${host_name}:443:${host_ip}" "https://${host_name}/" || true
echo
echo "== SSH =="
ssh -o ConnectTimeout=12 -o BatchMode=yes -o IdentityAgent=none -o IdentitiesOnly=yes \
-i "${ssh_identity}" "root@${host_ip}" echo ssh-ok || true
}
server_metadata() {
load_robot_auth
robot_curl GET "/server/${server}" | mask_sensitive_json
}
reset_status() {
robot_curl GET "/reset/${server}" | mask_sensitive_json
}
rescue_status() {
load_robot_auth
robot_curl GET "/boot/${server}/rescue" | mask_sensitive_json
}
status() {
load_robot_auth
echo "== reset =="
reset_status
echo
echo "== rescue =="
robot_curl GET "/boot/${server}/rescue" | mask_sensitive_json
}
activate_rescue() {
load_robot_auth
local rescue_os="${EVERY_CHANNEL_ROBOT_RESCUE_OS:-linux}"
local keyboard="${EVERY_CHANNEL_ROBOT_RESCUE_KEYBOARD:-us}"
local key_fingerprint
key_fingerprint="$(authorized_key_fingerprint || true)"
local args=(--data-urlencode "os=${rescue_os}" --data-urlencode "keyboard=${keyboard}")
if [[ -n "${key_fingerprint}" ]]; then
args+=(--data-urlencode "authorized_key[]=${key_fingerprint}")
fi
robot_curl POST "/boot/${server}/rescue" "${args[@]}" | mask_sensitive_json
}
reset_server() {
load_robot_auth
local reset_type="${1:-${EVERY_CHANNEL_ROBOT_RESET_TYPE:-hw}}"
robot_curl POST "/reset/${server}" --data-urlencode "type=${reset_type}" | mask_sensitive_json
}
recover() {
local reset_type="${1:-${EVERY_CHANNEL_ROBOT_RESET_TYPE:-hw}}"
echo "== activate rescue =="
activate_rescue
echo
echo "== reset ${reset_type} =="
reset_server "${reset_type}"
echo
echo "Rescue boot requested. Run '$0 wait-ssh' to watch for SSH on ${host_ip}."
}
wait_ssh() {
local deadline="${EVERY_CHANNEL_FORGE_WAIT_SECONDS:-300}"
local started
started="$(date +%s)"
while true; do
if command -v nc >/dev/null 2>&1; then
if nc -z -w 5 "${host_ip}" 22 >/dev/null 2>&1; then
echo "ssh port is reachable on ${host_ip}:22"
return 0
fi
elif ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null "root@${host_ip}" true >/dev/null 2>&1; then
echo "ssh is reachable on ${host_ip}"
return 0
fi
if (( "$(date +%s)" - started >= deadline )); then
echo "error: timed out waiting for SSH on ${host_ip}:22" >&2
return 1
fi
sleep 5
done
}
cmd="${1:-}"
case "${cmd}" in
""|-h|--help|help)
usage
;;
probe)
probe
;;
server)
require_cmd curl
server_metadata
;;
status)
require_cmd curl
status
;;
rescue-status)
require_cmd curl
rescue_status
;;
activate-rescue)
require_cmd curl
activate_rescue
;;
reset)
require_cmd curl
reset_server "${2:-}"
;;
recover)
require_cmd curl
recover "${2:-}"
;;
wait-ssh)
wait_ssh
;;
*)
echo "error: unknown command: ${cmd}" >&2
usage >&2
exit 2
;;
esac

View file

@ -7,6 +7,8 @@ l1_rpc_url="${EVERY_CHANNEL_OP_STACK_L1_RPC_URL:-https://ethereum-sepolia-rpc.pu
l1_beacon_url="${EVERY_CHANNEL_OP_STACK_L1_BEACON_URL:-https://ethereum-sepolia-beacon-api.publicnode.com}"
chain_id="${EVERY_CHANNEL_OP_STACK_CHAIN_ID:-245245}"
p2p_advertise_ip="${EVERY_CHANNEL_OP_STACK_P2P_ADVERTISE_IP:-127.0.0.1}"
l2_rpc_url="${EVERY_CHANNEL_OP_STACK_L2_RPC_URL:-http://127.0.0.1:28545}"
rollup_rpc_url="${EVERY_CHANNEL_OP_STACK_ROLLUP_RPC_URL:-http://127.0.0.1:28547}"
op_deployer_bin="${EVERY_CHANNEL_OP_DEPLOYER_BIN:-${root}/bin/op-deployer}"
download_script="${EVERY_CHANNEL_OP_DEPLOYER_DOWNLOAD_SCRIPT:-}"
download_tag="${EVERY_CHANNEL_OP_DEPLOYER_TAG:-op-deployer/v0.6.0-rc.3}"
@ -36,14 +38,36 @@ trimmed_file_contents() {
tr -d '\r\n' <"$1"
}
normalize_rollup_config() {
local path="$1"
python - "$path" <<'PY'
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
data = json.loads(path.read_text())
system_config = data.setdefault("genesis", {}).setdefault("system_config", {})
system_config.pop("daFootprintGasScalar", None)
chain_op_config = data.get("chain_op_config", {})
denominator = chain_op_config.get("eip1559DenominatorCanyon") or chain_op_config.get("eip1559Denominator")
elasticity = chain_op_config.get("eip1559Elasticity")
if (
isinstance(denominator, int)
and isinstance(elasticity, int)
and system_config.get("eip1559Params") in (None, "0x", "0x0000000000000000")
):
system_config["eip1559Params"] = f"0x{denominator:08x}{elasticity:08x}"
path.write_text(json.dumps(data, indent=2, sort_keys=False) + "\n")
PY
}
set_toml_value() {
local key="$1"
local value="$2"
local file="$3"
if ! grep -q "^${key} = " "$file"; then
log "missing key ${key} in ${file}"
exit 1
fi
python - "$key" "$value" "$file" <<'PY'
import sys
from pathlib import Path
@ -53,13 +77,15 @@ needle = f"{key} = "
out = []
replaced = False
for line in text.splitlines():
if line.startswith(needle):
out.append(f'{key} = "{value}"')
stripped = line.lstrip()
if stripped.startswith(needle):
indent = line[:len(line) - len(stripped)]
out.append(f'{indent}{key} = "{value}"')
replaced = True
else:
out.append(line)
if not replaced:
raise SystemExit(f"failed to replace {key}")
raise SystemExit(f"missing key {key} in {path}")
Path(path).write_text("\n".join(out) + "\n")
PY
}
@ -133,24 +159,33 @@ if 'fundDevAccounts = false' not in text:
path.write_text(text)
PY
if [[ "${skip_apply}" != "true" && ! -f "${deployer_dir}/.deployer/state.json" ]]; then
"$op_deployer_bin" apply \
--workdir "${deployer_dir}/.deployer" \
--l1-rpc-url "${l1_rpc_url}" \
--private-key "${private_key}"
state_json="${deployer_dir}/.deployer/state.json"
if [[ "${skip_apply}" != "true" ]]; then
if [[ ! -f "${state_json}" ]] || ! jq -e \
'.appliedIntent != null and .opChainDeployments != null' \
<"${state_json}" >/dev/null 2>&1
then
"$op_deployer_bin" apply \
--workdir "${deployer_dir}/.deployer" \
--l1-rpc-url "${l1_rpc_url}" \
--private-key "${private_key}"
fi
fi
if [[ ! -f "${deployer_dir}/.deployer/state.json" ]]; then
log "state.json missing; bootstrap did not complete"
if [[ ! -f "${state_json}" ]] || ! jq -e \
'.appliedIntent != null and .opChainDeployments != null' \
<"${state_json}" >/dev/null 2>&1
then
log "state.json missing or unapplied; bootstrap did not complete"
exit 1
fi
"$op_deployer_bin" inspect genesis --workdir "${deployer_dir}/.deployer" "${chain_id_hex}" >"${sequencer_dir}/genesis.json"
"$op_deployer_bin" inspect rollup --workdir "${deployer_dir}/.deployer" "${chain_id_hex}" >"${sequencer_dir}/rollup.json"
normalize_rollup_config "${sequencer_dir}/rollup.json"
openssl rand -hex 32 >"${sequencer_dir}/jwt.txt"
chmod 0600 "${sequencer_dir}/jwt.txt"
state_json="${deployer_dir}/.deployer/state.json"
system_config_proxy="$(jq -r '.opChainDeployments[0].systemConfigProxyAddress // .opChainDeployments[0].SystemConfigProxy // empty' <"${state_json}")"
dispute_game_factory="$(jq -r '.opChainDeployments[0].disputeGameFactoryProxyAddress // .opChainDeployments[0].DisputeGameFactoryProxy // empty' <"${state_json}")"
l1_standard_bridge="$(jq -r '.opChainDeployments[0].l1StandardBridgeProxyAddress // .opChainDeployments[0].L1StandardBridgeProxy // empty' <"${state_json}")"
@ -184,15 +219,14 @@ EOF
cat > "${batcher_dir}/.env" <<EOF
L1_RPC_URL=${l1_rpc_url}
L2_RPC_URL=http://127.0.0.1:8545
ROLLUP_RPC_URL=http://127.0.0.1:8547
L2_RPC_URL=${l2_rpc_url}
ROLLUP_RPC_URL=${rollup_rpc_url}
PRIVATE_KEY=${private_key}
BATCH_INBOX_ADDRESS=${system_config_proxy}
EOF
cat > "${proposer_dir}/.env" <<EOF
L1_RPC_URL=${l1_rpc_url}
ROLLUP_RPC_URL=http://127.0.0.1:8547
ROLLUP_RPC_URL=${rollup_rpc_url}
GAME_FACTORY_ADDRESS=${dispute_game_factory}
PRIVATE_KEY=${private_key}
PROPOSAL_INTERVAL=3600s
@ -203,8 +237,8 @@ cp "${sequencer_dir}/rollup.json" "${challenger_dir}/rollup.json"
cat > "${challenger_dir}/.env" <<EOF
L1_RPC_URL=${l1_rpc_url}
L1_BEACON_URL=${l1_beacon_url}
L2_RPC_URL=http://127.0.0.1:8545
ROLLUP_RPC_URL=http://127.0.0.1:8547
L2_RPC_URL=${l2_rpc_url}
ROLLUP_RPC_URL=${rollup_rpc_url}
GAME_FACTORY_ADDRESS=${dispute_game_factory}
PRIVATE_KEY=${private_key}
EOF
@ -217,7 +251,7 @@ fi
cat > "${dispute_mon_dir}/.env" <<EOF
OP_DISPUTE_MON_L1_ETH_RPC=${l1_rpc_url}
OP_DISPUTE_MON_ROLLUP_RPC=http://127.0.0.1:8547
OP_DISPUTE_MON_ROLLUP_RPC=${rollup_rpc_url}
OP_DISPUTE_MON_HONEST_ACTORS=${operator_address}
OP_DISPUTE_MON_GAME_FACTORY_ADDRESS=${dispute_game_factory}
OP_DISPUTE_MON_MONITOR_INTERVAL=10s