Wire HDHomeRun observations and recover Forge OP Stack
This commit is contained in:
parent
8065860449
commit
0d86104762
18 changed files with 1613 additions and 58 deletions
55
scripts/e2e-hdhr-blockchain.sh
Executable file
55
scripts/e2e-hdhr-blockchain.sh
Executable 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
257
scripts/hetzner-robot-forge.sh
Executable 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue