Advance forge rollout, Ethereum rails, and NBC sources
This commit is contained in:
parent
be26313225
commit
7d84510eac
88 changed files with 11230 additions and 302 deletions
|
|
@ -5,7 +5,7 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||
cd "${root}"
|
||||
|
||||
in_file="${1:-secrets/token.txt}"
|
||||
out_file="${2:-secrets/forge-token.age}"
|
||||
out_file="${2:-secrets/forgejo-api-token.age}"
|
||||
|
||||
rules_file="${EVERY_CHANNEL_AGE_RULES_FILE:-${root}/secrets.nix}"
|
||||
identity_file="${EVERY_CHANNEL_AGE_IDENTITY_FILE:-$HOME/.config/every.channel/keys/founder_ed25519}"
|
||||
|
|
|
|||
23
scripts/deploy-ecp-forge.sh
Executable file
23
scripts/deploy-ecp-forge.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${root}"
|
||||
|
||||
target="${EVERY_CHANNEL_FORGE_TARGET_HOST:-root@git.every.channel}"
|
||||
build_host="${EVERY_CHANNEL_FORGE_BUILD_HOST:-${target}}"
|
||||
identity_file="${EVERY_CHANNEL_FORGE_SSH_IDENTITY:-$HOME/.ssh/id_ed25519}"
|
||||
|
||||
if [[ ! -f "${identity_file}" ]]; then
|
||||
echo "error: SSH identity not found: ${identity_file}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
export NIX_SSHOPTS="-o BatchMode=yes -o IdentityAgent=none -o IdentitiesOnly=yes -i ${identity_file}"
|
||||
|
||||
exec nix run nixpkgs#nixos-rebuild -- \
|
||||
--flake "${root}#ecp-forge" \
|
||||
--target-host "${target}" \
|
||||
--build-host "${build_host}" \
|
||||
--use-remote-sudo \
|
||||
switch
|
||||
|
|
@ -8,12 +8,13 @@ cd "${root}"
|
|||
#
|
||||
# Auth token source order:
|
||||
# 1) EVERY_CHANNEL_FORGE_TOKEN / FORGE_TOKEN / CODEBERG_TOKEN env var
|
||||
# 2) `agenix -d secrets/forge-token.age` or `secrets/codeberg-token.age` (optional)
|
||||
# 3) `age -d -i <identity> secrets/forge-token.age` or `secrets/codeberg-token.age` (optional)
|
||||
# 2) `agenix -d secrets/forgejo-api-token.age` (preferred) / `secrets/forge-token.age` / `secrets/codeberg-token.age` (optional)
|
||||
# 3) `age -d -i <identity> secrets/forgejo-api-token.age` / `secrets/forge-token.age` / `secrets/codeberg-token.age` (optional)
|
||||
|
||||
host="${EVERY_CHANNEL_FORGE_HOST:-https://forge.every.channel}"
|
||||
host="${EVERY_CHANNEL_FORGE_HOST:-https://git.every.channel}"
|
||||
account="${EVERY_CHANNEL_FORGE_ACCOUNT:-every-channel}"
|
||||
token_file_primary="${EVERY_CHANNEL_FORGE_TOKEN_FILE:-secrets/forge-token.age}"
|
||||
token_file_primary="${EVERY_CHANNEL_FORGE_TOKEN_FILE:-secrets/forgejo-api-token.age}"
|
||||
token_file_secondary="${EVERY_CHANNEL_LEGACY_FORGE_TOKEN_FILE:-secrets/forge-token.age}"
|
||||
token_file_compat="${EVERY_CHANNEL_CODEBERG_TOKEN_FILE:-secrets/codeberg-token.age}"
|
||||
|
||||
rules_file="${EVERY_CHANNEL_AGE_RULES_FILE:-./secrets.nix}"
|
||||
|
|
@ -38,6 +39,9 @@ load_token_from_file() {
|
|||
if [[ -z "${token}" ]]; then
|
||||
token="$(load_token_from_file "${token_file_primary}" || true)"
|
||||
fi
|
||||
if [[ -z "${token}" ]]; then
|
||||
token="$(load_token_from_file "${token_file_secondary}" || true)"
|
||||
fi
|
||||
if [[ -z "${token}" ]]; then
|
||||
token="$(load_token_from_file "${token_file_compat}" || true)"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ set -euo pipefail
|
|||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${root}"
|
||||
|
||||
host="${EVERY_CHANNEL_FORGE_HOST:-https://forge.every.channel}"
|
||||
host="${EVERY_CHANNEL_FORGE_HOST:-https://git.every.channel}"
|
||||
repo="${EVERY_CHANNEL_FORGE_REPO:-every-channel/every.channel}"
|
||||
branch="${EVERY_CHANNEL_PROTECTED_BRANCH:-main}"
|
||||
required_checks_csv="${EVERY_CHANNEL_REQUIRED_CHECKS:-ci-gates / checks}"
|
||||
|
|
@ -13,7 +13,8 @@ require_signed_commits_raw="${EVERY_CHANNEL_REQUIRE_SIGNED_COMMITS:-true}"
|
|||
|
||||
rules_file="${EVERY_CHANNEL_AGE_RULES_FILE:-./secrets.nix}"
|
||||
identity_file="${EVERY_CHANNEL_AGE_IDENTITY_FILE:-$HOME/.config/every.channel/keys/founder_ed25519}"
|
||||
token_file_primary="${EVERY_CHANNEL_FORGE_TOKEN_FILE:-secrets/forge-token.age}"
|
||||
token_file_primary="${EVERY_CHANNEL_FORGE_TOKEN_FILE:-secrets/forgejo-api-token.age}"
|
||||
token_file_secondary="${EVERY_CHANNEL_LEGACY_FORGE_TOKEN_FILE:-secrets/forge-token.age}"
|
||||
token_file_compat="${EVERY_CHANNEL_CODEBERG_TOKEN_FILE:-secrets/codeberg-token.age}"
|
||||
|
||||
token="${EVERY_CHANNEL_FORGE_TOKEN:-${FORGE_TOKEN:-${CODEBERG_TOKEN:-}}}"
|
||||
|
|
@ -35,6 +36,9 @@ load_token_from_file() {
|
|||
if [[ -z "${token}" ]]; then
|
||||
token="$(load_token_from_file "${token_file_primary}" || true)"
|
||||
fi
|
||||
if [[ -z "${token}" ]]; then
|
||||
token="$(load_token_from_file "${token_file_secondary}" || true)"
|
||||
fi
|
||||
if [[ -z "${token}" ]]; then
|
||||
token="$(load_token_from_file "${token_file_compat}" || true)"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ set -euo pipefail
|
|||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${root}"
|
||||
|
||||
host="${EVERY_CHANNEL_FORGE_HOST:-https://forge.every.channel}"
|
||||
host="${EVERY_CHANNEL_FORGE_HOST:-https://git.every.channel}"
|
||||
repo="${EVERY_CHANNEL_FORGE_REPO:-every-channel/every.channel}"
|
||||
secret_name="${EVERY_CHANNEL_FORGE_AGE_SECRET_NAME:-AGE_FORGE_SSH_KEY}"
|
||||
key_path="${1:-$HOME/.config/every.channel/keys/founder_ed25519}"
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ set -euo pipefail
|
|||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${root}"
|
||||
|
||||
host="${EVERY_CHANNEL_FORGE_HOST:-https://forge.every.channel}"
|
||||
host="${EVERY_CHANNEL_FORGE_HOST:-https://git.every.channel}"
|
||||
repo="${EVERY_CHANNEL_FORGE_REPO:-every-channel/every.channel}"
|
||||
enabled_raw="${EVERY_CHANNEL_FORGE_ACTIONS_ENABLED:-false}"
|
||||
|
||||
rules_file="${EVERY_CHANNEL_AGE_RULES_FILE:-./secrets.nix}"
|
||||
identity_file="${EVERY_CHANNEL_AGE_IDENTITY_FILE:-$HOME/.config/every.channel/keys/founder_ed25519}"
|
||||
token_file_primary="${EVERY_CHANNEL_FORGE_TOKEN_FILE:-secrets/forge-token.age}"
|
||||
token_file_primary="${EVERY_CHANNEL_FORGE_TOKEN_FILE:-secrets/forgejo-api-token.age}"
|
||||
token_file_secondary="${EVERY_CHANNEL_LEGACY_FORGE_TOKEN_FILE:-secrets/forge-token.age}"
|
||||
token_file_compat="${EVERY_CHANNEL_CODEBERG_TOKEN_FILE:-secrets/codeberg-token.age}"
|
||||
|
||||
token="${EVERY_CHANNEL_FORGE_TOKEN:-${FORGE_TOKEN:-${CODEBERG_TOKEN:-}}}"
|
||||
|
|
@ -32,6 +33,9 @@ load_token_from_file() {
|
|||
if [[ -z "${token}" ]]; then
|
||||
token="$(load_token_from_file "${token_file_primary}" || true)"
|
||||
fi
|
||||
if [[ -z "${token}" ]]; then
|
||||
token="$(load_token_from_file "${token_file_secondary}" || true)"
|
||||
fi
|
||||
if [[ -z "${token}" ]]; then
|
||||
token="$(load_token_from_file "${token_file_compat}" || true)"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||
cd "${root}"
|
||||
|
||||
primary_remote="${EVERY_CHANNEL_PRIMARY_REMOTE:-origin}"
|
||||
primary_url="${EVERY_CHANNEL_PRIMARY_GIT_URL:-git@forge.every.channel:every-channel/every.channel.git}"
|
||||
primary_url="${EVERY_CHANNEL_PRIMARY_GIT_URL:-ssh://forgejo@git.every.channel:2222/every-channel/every.channel.git}"
|
||||
|
||||
codeberg_remote="${EVERY_CHANNEL_CODEBERG_REMOTE:-mirror-codeberg}"
|
||||
codeberg_url="${EVERY_CHANNEL_CODEBERG_GIT_URL:-git@codeberg.org:every-channel/every.channel.git}"
|
||||
|
|
|
|||
147
scripts/netboot-build-ipxe.sh
Executable file
147
scripts/netboot-build-ipxe.sh
Executable file
|
|
@ -0,0 +1,147 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${root}"
|
||||
|
||||
netboot_root="${EVERY_CHANNEL_NETBOOT_ROOT:-tmp/netboot}"
|
||||
tftp_dir="${netboot_root}/tftp"
|
||||
netboot_hostname="${EVERY_CHANNEL_NETBOOT_HOSTNAME:-boot.every.channel}"
|
||||
http_port="${EVERY_CHANNEL_NETBOOT_HTTP_PORT:-8080}"
|
||||
ipxe_repo="${EVERY_CHANNEL_NETBOOT_IPXE_REPO:-https://github.com/ipxe/ipxe.git}"
|
||||
ipxe_ref="${EVERY_CHANNEL_NETBOOT_IPXE_REF:-}"
|
||||
output_name="${EVERY_CHANNEL_NETBOOT_IPXE_FILENAME:-ec-ipxe.efi}"
|
||||
use_docker="${EVERY_CHANNEL_NETBOOT_IPXE_USE_DOCKER:-auto}"
|
||||
docker_image="${EVERY_CHANNEL_NETBOOT_IPXE_DOCKER_IMAGE:-ubuntu:24.04}"
|
||||
docker_platform="${EVERY_CHANNEL_NETBOOT_IPXE_DOCKER_PLATFORM:-linux/amd64}"
|
||||
chain_token="${EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN:-}"
|
||||
|
||||
need_cmd() {
|
||||
local name="$1"
|
||||
if ! command -v "${name}" >/dev/null 2>&1; then
|
||||
echo "error: required command not found: ${name}" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
validate_chain_token() {
|
||||
local value="$1"
|
||||
if [[ -z "${value}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ ! "${value}" =~ ^[A-Za-z0-9._~-]{16,128}$ ]]; then
|
||||
echo "error: EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN must match [A-Za-z0-9._~-]{16,128}" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
validate_chain_token "${chain_token}"
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "${tmp_dir}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
repo_dir="${tmp_dir}/ipxe"
|
||||
embed_script="${tmp_dir}/embed.ipxe"
|
||||
build_output="${tmp_dir}/ipxe.efi"
|
||||
|
||||
chain_url="http://${netboot_hostname}:${http_port}/netboot.ipxe"
|
||||
if [[ -n "${chain_token}" ]]; then
|
||||
chain_url="${chain_url}?token=${chain_token}"
|
||||
fi
|
||||
|
||||
cat > "${embed_script}" <<EOF
|
||||
#!ipxe
|
||||
dhcp
|
||||
chain ${chain_url} || shell
|
||||
EOF
|
||||
|
||||
bool_norm() {
|
||||
local raw
|
||||
raw="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
|
||||
case "${raw}" in
|
||||
''|auto) echo "auto" ;;
|
||||
true|1|yes|y|on) echo "true" ;;
|
||||
false|0|no|n|off) echo "false" ;;
|
||||
*)
|
||||
echo "error: invalid boolean value '${1}'" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
build_native() {
|
||||
need_cmd git
|
||||
need_cmd make
|
||||
if ! command -v cc >/dev/null 2>&1 && ! command -v gcc >/dev/null 2>&1; then
|
||||
echo "error: required C compiler not found (need cc or gcc)" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! command -v objcopy >/dev/null 2>&1; then
|
||||
echo "error: required tool not found: objcopy" >&2
|
||||
return 1
|
||||
fi
|
||||
git clone --depth 1 "${ipxe_repo}" "${repo_dir}" >/dev/null
|
||||
if [[ -n "${ipxe_ref}" ]]; then
|
||||
git -C "${repo_dir}" fetch --depth 1 origin "${ipxe_ref}" >/dev/null
|
||||
git -C "${repo_dir}" checkout -q FETCH_HEAD
|
||||
fi
|
||||
make -C "${repo_dir}/src" -s "bin-x86_64-efi/ipxe.efi" "EMBED=${embed_script}"
|
||||
cp -f "${repo_dir}/src/bin-x86_64-efi/ipxe.efi" "${build_output}"
|
||||
}
|
||||
|
||||
build_docker() {
|
||||
need_cmd docker
|
||||
docker run --rm \
|
||||
--platform "${docker_platform}" \
|
||||
-v "${tmp_dir}:/work" \
|
||||
-e IPXE_REPO="${ipxe_repo}" \
|
||||
-e IPXE_REF="${ipxe_ref}" \
|
||||
-w /work \
|
||||
"${docker_image}" \
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
apt-get update >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates git make gcc binutils perl mtools >/dev/null
|
||||
git clone --depth 1 "${IPXE_REPO}" ipxe >/dev/null
|
||||
if [ -n "${IPXE_REF}" ]; then
|
||||
git -C ipxe fetch --depth 1 origin "${IPXE_REF}" >/dev/null
|
||||
git -C ipxe checkout -q FETCH_HEAD
|
||||
fi
|
||||
make -C ipxe/src -s "bin-x86_64-efi/ipxe.efi" "EMBED=/work/embed.ipxe"
|
||||
'
|
||||
cp -f "${tmp_dir}/ipxe/src/bin-x86_64-efi/ipxe.efi" "${build_output}"
|
||||
}
|
||||
|
||||
use_docker="$(bool_norm "${use_docker}")"
|
||||
build_ok="false"
|
||||
if [[ "${use_docker}" != "true" ]]; then
|
||||
if build_native; then
|
||||
build_ok="true"
|
||||
fi
|
||||
fi
|
||||
if [[ "${build_ok}" != "true" ]]; then
|
||||
if [[ "${use_docker}" == "false" ]]; then
|
||||
exit 2
|
||||
fi
|
||||
build_docker
|
||||
fi
|
||||
|
||||
mkdir -p "${tftp_dir}"
|
||||
cp -f "${build_output}" "${tftp_dir}/${output_name}"
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "${tftp_dir}/${output_name}" > "${tftp_dir}/${output_name}.sha256"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "${tftp_dir}/${output_name}" > "${tftp_dir}/${output_name}.sha256"
|
||||
fi
|
||||
|
||||
echo "ok: built embedded iPXE binary: ${tftp_dir}/${output_name}"
|
||||
echo "ok: chains to ${chain_url}"
|
||||
if [[ -n "${chain_token}" ]]; then
|
||||
echo "ok: chain token enabled"
|
||||
fi
|
||||
echo "hint: in UniFi set DHCP boot filename to ${output_name}"
|
||||
144
scripts/netboot-http-server.py
Executable file
144
scripts/netboot-http-server.py
Executable file
|
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Minimal HTTP server for netboot artifacts with optional CIDR/token controls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import pathlib
|
||||
import urllib.parse
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Union
|
||||
|
||||
|
||||
ALLOWED_FILES = {
|
||||
"kernel": "application/octet-stream",
|
||||
"bzImage": "application/octet-stream",
|
||||
"initrd": "application/octet-stream",
|
||||
"netboot.ipxe": "text/plain; charset=utf-8",
|
||||
}
|
||||
|
||||
Network = Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
|
||||
|
||||
|
||||
class NetbootHTTPServer(ThreadingHTTPServer):
|
||||
def __init__(
|
||||
self,
|
||||
server_address: tuple[str, int],
|
||||
root: pathlib.Path,
|
||||
allowed_networks: list[Network],
|
||||
netboot_token: str | None,
|
||||
) -> None:
|
||||
super().__init__(server_address, NetbootRequestHandler)
|
||||
self.root = root
|
||||
self.allowed_networks = allowed_networks
|
||||
self.netboot_token = netboot_token
|
||||
|
||||
|
||||
class NetbootRequestHandler(BaseHTTPRequestHandler):
|
||||
server_version = "ec-netboot/1.0"
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def do_GET(self) -> None:
|
||||
self._serve_file(include_body=True)
|
||||
|
||||
def do_HEAD(self) -> None:
|
||||
self._serve_file(include_body=False)
|
||||
|
||||
def _serve_file(self, include_body: bool) -> None:
|
||||
if not self._client_allowed():
|
||||
self._send_error(HTTPStatus.FORBIDDEN, "client not allowed")
|
||||
return
|
||||
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
path = parsed.path.lstrip("/")
|
||||
if path not in ALLOWED_FILES:
|
||||
self._send_error(HTTPStatus.NOT_FOUND, "file not found")
|
||||
return
|
||||
|
||||
if path == "netboot.ipxe" and self.server.netboot_token:
|
||||
query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True)
|
||||
token = query.get("token", [""])[0]
|
||||
if token != self.server.netboot_token:
|
||||
self._send_error(HTTPStatus.FORBIDDEN, "missing or invalid token")
|
||||
return
|
||||
|
||||
if path == "bzImage":
|
||||
file_path = self.server.root / "kernel"
|
||||
else:
|
||||
file_path = self.server.root / path
|
||||
if not file_path.is_file():
|
||||
self._send_error(HTTPStatus.NOT_FOUND, "file not found")
|
||||
return
|
||||
|
||||
stat = file_path.stat()
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", ALLOWED_FILES[path])
|
||||
self.send_header("Content-Length", str(stat.st_size))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
|
||||
if not include_body:
|
||||
return
|
||||
|
||||
with file_path.open("rb") as handle:
|
||||
while True:
|
||||
chunk = handle.read(64 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
|
||||
def _client_allowed(self) -> bool:
|
||||
networks = self.server.allowed_networks
|
||||
if not networks:
|
||||
return True
|
||||
|
||||
try:
|
||||
client_ip = ipaddress.ip_address(self.client_address[0])
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return any(client_ip in network for network in networks)
|
||||
|
||||
def _send_error(self, code: HTTPStatus, message: str) -> None:
|
||||
payload = (message + "\n").encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--bind-ip", required=True)
|
||||
parser.add_argument("--port", type=int, required=True)
|
||||
parser.add_argument("--root", required=True)
|
||||
parser.add_argument("--allow-cidr", action="append", default=[])
|
||||
parser.add_argument("--netboot-token", default="")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
root = pathlib.Path(args.root).resolve()
|
||||
if not root.is_dir():
|
||||
raise SystemExit(f"error: root directory does not exist: {root}")
|
||||
|
||||
allowed_networks = []
|
||||
for cidr in args.allow_cidr:
|
||||
try:
|
||||
allowed_networks.append(ipaddress.ip_network(cidr, strict=False))
|
||||
except ValueError as exc:
|
||||
raise SystemExit(f"error: invalid CIDR '{cidr}': {exc}") from exc
|
||||
|
||||
token = args.netboot_token or None
|
||||
server = NetbootHTTPServer((args.bind_ip, args.port), root, allowed_networks, token)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -14,6 +14,10 @@ proxy_subnet="${EVERY_CHANNEL_NETBOOT_PROXY_SUBNET:-}"
|
|||
netboot_hostname="${EVERY_CHANNEL_NETBOOT_HOSTNAME:-}"
|
||||
http_port="${EVERY_CHANNEL_NETBOOT_HTTP_PORT:-8080}"
|
||||
dnsmasq_port="${EVERY_CHANNEL_NETBOOT_DNS_PORT:-0}"
|
||||
proxy_dhcp="${EVERY_CHANNEL_NETBOOT_PROXY_DHCP:-true}"
|
||||
tftp_boot_filename="${EVERY_CHANNEL_NETBOOT_TFTP_BOOT_FILENAME:-ipxe.efi}"
|
||||
http_allowed_cidrs="${EVERY_CHANNEL_NETBOOT_HTTP_ALLOWED_CIDRS:-}"
|
||||
chain_token="${EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN:-}"
|
||||
|
||||
need_cmd() {
|
||||
local name="$1"
|
||||
|
|
@ -26,10 +30,43 @@ need_cmd() {
|
|||
need_cmd dnsmasq
|
||||
need_cmd python3
|
||||
|
||||
bool_norm() {
|
||||
local raw
|
||||
raw="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
|
||||
case "${raw}" in
|
||||
''|true|1|yes|y|on) echo "true" ;;
|
||||
false|0|no|n|off) echo "false" ;;
|
||||
*)
|
||||
echo "error: invalid boolean value '${1}'" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
trim_ws() {
|
||||
local value="$1"
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
printf '%s' "${value}"
|
||||
}
|
||||
|
||||
validate_chain_token() {
|
||||
local value="$1"
|
||||
if [[ -z "${value}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ ! "${value}" =~ ^[A-Za-z0-9._~-]{16,128}$ ]]; then
|
||||
echo "error: EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN must match [A-Za-z0-9._~-]{16,128}" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
echo "error: netboot-serve requires root (TFTP + ProxyDHCP ports)." >&2
|
||||
echo "hint: run with sudo and pass env vars, for example:" >&2
|
||||
echo " sudo EVERY_CHANNEL_NETBOOT_LISTEN_IP=10.20.30.2 EVERY_CHANNEL_NETBOOT_INTERFACE=eth0 EVERY_CHANNEL_NETBOOT_PROXY_SUBNET=10.20.30.0/24 EVERY_CHANNEL_NETBOOT_HOSTNAME=boot.every.channel ./scripts/netboot-serve.sh" >&2
|
||||
echo "hint: run with sudo and pass env vars. Example (UniFi-only):" >&2
|
||||
echo " sudo EVERY_CHANNEL_NETBOOT_LISTEN_IP=10.20.30.2 EVERY_CHANNEL_NETBOOT_INTERFACE=eth0 EVERY_CHANNEL_NETBOOT_HOSTNAME=boot.every.channel EVERY_CHANNEL_NETBOOT_PROXY_DHCP=false EVERY_CHANNEL_NETBOOT_TFTP_BOOT_FILENAME=ec-ipxe.efi ./scripts/netboot-serve.sh" >&2
|
||||
echo "hint: Example (ProxyDHCP):" >&2
|
||||
echo " sudo EVERY_CHANNEL_NETBOOT_LISTEN_IP=10.20.30.2 EVERY_CHANNEL_NETBOOT_INTERFACE=eth0 EVERY_CHANNEL_NETBOOT_PROXY_SUBNET=10.20.30.0/24 EVERY_CHANNEL_NETBOOT_HOSTNAME=boot.every.channel EVERY_CHANNEL_NETBOOT_PROXY_DHCP=true ./scripts/netboot-serve.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
|
@ -41,15 +78,25 @@ if [[ -z "${interface_name}" ]]; then
|
|||
echo "error: set EVERY_CHANNEL_NETBOOT_INTERFACE (interface on NUC VLAN)" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -z "${proxy_subnet}" ]]; then
|
||||
echo "error: set EVERY_CHANNEL_NETBOOT_PROXY_SUBNET (for example 10.20.30.0/24)" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -z "${netboot_hostname}" ]]; then
|
||||
netboot_hostname="${listen_ip}"
|
||||
fi
|
||||
proxy_dhcp="$(bool_norm "${proxy_dhcp}")"
|
||||
validate_chain_token "${chain_token}"
|
||||
if [[ "${proxy_dhcp}" == "true" && -z "${proxy_subnet}" ]]; then
|
||||
echo "error: set EVERY_CHANNEL_NETBOOT_PROXY_SUBNET (for example 10.20.30.0/24) when proxy mode is enabled" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -z "${http_allowed_cidrs}" && "${proxy_dhcp}" == "true" ]]; then
|
||||
http_allowed_cidrs="${proxy_subnet}"
|
||||
fi
|
||||
|
||||
for required in "${http_dir}/kernel" "${http_dir}/initrd" "${http_dir}/netboot.ipxe" "${tftp_dir}/ipxe.efi"; do
|
||||
netboot_chain_url="http://${netboot_hostname}:${http_port}/netboot.ipxe"
|
||||
if [[ -n "${chain_token}" ]]; then
|
||||
netboot_chain_url="${netboot_chain_url}?token=${chain_token}"
|
||||
fi
|
||||
|
||||
for required in "${http_dir}/kernel" "${http_dir}/initrd" "${http_dir}/netboot.ipxe" "${tftp_dir}/${tftp_boot_filename}"; do
|
||||
if [[ ! -f "${required}" ]]; then
|
||||
echo "error: missing required staged file: ${required}" >&2
|
||||
echo "hint: run ./scripts/netboot-stage.sh first" >&2
|
||||
|
|
@ -75,25 +122,73 @@ listen-address=${listen_ip}
|
|||
log-dhcp
|
||||
enable-tftp
|
||||
tftp-root=${tftp_dir}
|
||||
EOF
|
||||
|
||||
if [[ "${proxy_dhcp}" == "true" ]]; then
|
||||
cat >> "${run_dir}/dnsmasq.conf" <<EOF
|
||||
dhcp-range=${proxy_subnet},proxy
|
||||
dhcp-userclass=set:ipxe,iPXE
|
||||
dhcp-match=set:efi64,option:client-arch,7
|
||||
dhcp-match=set:efi64,option:client-arch,9
|
||||
dhcp-option=66,${netboot_hostname}
|
||||
dhcp-boot=tag:!ipxe,tag:efi64,ipxe.efi
|
||||
dhcp-boot=tag:ipxe,tag:efi64,http://${netboot_hostname}:${http_port}/netboot.ipxe
|
||||
dhcp-boot=tag:!ipxe,ipxe.efi
|
||||
dhcp-boot=tag:ipxe,http://${netboot_hostname}:${http_port}/netboot.ipxe
|
||||
dhcp-boot=tag:!ipxe,tag:efi64,${tftp_boot_filename}
|
||||
dhcp-boot=tag:ipxe,tag:efi64,${netboot_chain_url}
|
||||
dhcp-boot=tag:!ipxe,${tftp_boot_filename}
|
||||
dhcp-boot=tag:ipxe,${netboot_chain_url}
|
||||
EOF
|
||||
fi
|
||||
|
||||
python3 -m http.server "${http_port}" --bind "${listen_ip}" --directory "${http_dir}" >/tmp/every-channel-netboot-http.log 2>&1 &
|
||||
http_server_script="${root}/scripts/netboot-http-server.py"
|
||||
if [[ ! -f "${http_server_script}" ]]; then
|
||||
echo "error: missing HTTP server helper: ${http_server_script}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
http_args=(python3 "${http_server_script}" --bind-ip "${listen_ip}" --port "${http_port}" --root "${http_dir}")
|
||||
if [[ -n "${chain_token}" ]]; then
|
||||
http_args+=(--netboot-token "${chain_token}")
|
||||
fi
|
||||
|
||||
if [[ -n "${http_allowed_cidrs}" ]]; then
|
||||
IFS=',' read -r -a cidr_raw <<< "${http_allowed_cidrs}"
|
||||
for raw in "${cidr_raw[@]}"; do
|
||||
cidr="$(trim_ws "${raw}")"
|
||||
if [[ -n "${cidr}" ]]; then
|
||||
http_args+=(--allow-cidr "${cidr}")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
http_log="${run_dir}/http.log"
|
||||
"${http_args[@]}" >"${http_log}" 2>&1 &
|
||||
http_pid="$!"
|
||||
sleep 0.2
|
||||
if ! kill -0 "${http_pid}" >/dev/null 2>&1; then
|
||||
echo "error: HTTP server failed to start; see ${http_log}" >&2
|
||||
cat "${http_log}" >&2 || true
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "ok: HTTP serving ${http_dir} on http://${listen_ip}:${http_port}/"
|
||||
echo "ok: advertised netboot host: ${netboot_hostname}"
|
||||
echo "ok: TFTP serving ${tftp_dir} on ${listen_ip}:69"
|
||||
echo "ok: ProxyDHCP active for ${proxy_subnet} on interface ${interface_name}"
|
||||
echo "ok: Use normal Unifi DHCP for IP assignment; do not configure Unifi DHCP bootfile while proxy mode is active."
|
||||
echo "ok: TFTP boot filename: ${tftp_boot_filename}"
|
||||
if [[ -n "${chain_token}" ]]; then
|
||||
echo "ok: chain token enabled"
|
||||
fi
|
||||
if [[ -n "${http_allowed_cidrs}" ]]; then
|
||||
echo "ok: HTTP allowed CIDRs: ${http_allowed_cidrs}"
|
||||
else
|
||||
echo "warning: HTTP CIDR allowlist is disabled; set EVERY_CHANNEL_NETBOOT_HTTP_ALLOWED_CIDRS to lock this down"
|
||||
fi
|
||||
if [[ "${proxy_dhcp}" == "true" ]]; then
|
||||
echo "ok: ProxyDHCP active for ${proxy_subnet} on interface ${interface_name}"
|
||||
echo "ok: Use normal Unifi DHCP for IP assignment; do not configure Unifi DHCP bootfile while proxy mode is active."
|
||||
else
|
||||
echo "ok: ProxyDHCP disabled."
|
||||
echo "ok: Configure UniFi DHCP option 66=${netboot_hostname}, option 67=${tftp_boot_filename}"
|
||||
fi
|
||||
echo "ok: chain URL: ${netboot_chain_url}"
|
||||
echo
|
||||
echo "Press Ctrl+C to stop."
|
||||
dnsmasq --no-daemon --conf-file="${run_dir}/dnsmasq.conf"
|
||||
|
|
|
|||
|
|
@ -4,14 +4,20 @@ set -euo pipefail
|
|||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${root}"
|
||||
|
||||
forge_host="${EVERY_CHANNEL_FORGE_HOST:-https://forge.every.channel}"
|
||||
forge_host="${EVERY_CHANNEL_FORGE_HOST:-https://git.every.channel}"
|
||||
forge_repo="${EVERY_CHANNEL_FORGE_REPO:-every-channel/every.channel}"
|
||||
release_tag="${EVERY_CHANNEL_NETBOOT_RELEASE_TAG:-}"
|
||||
local_tarball="${EVERY_CHANNEL_NETBOOT_TARBALL:-}"
|
||||
out_root="${EVERY_CHANNEL_NETBOOT_ROOT:-tmp/netboot}"
|
||||
ipxe_efi_url="${EVERY_CHANNEL_IPXE_EFI_URL:-https://boot.ipxe.org/snponly.efi}"
|
||||
ipxe_efi_path="${EVERY_CHANNEL_IPXE_EFI_PATH:-}"
|
||||
ipxe_efi_filename="${EVERY_CHANNEL_IPXE_EFI_FILENAME:-ipxe.efi}"
|
||||
netboot_hostname="${EVERY_CHANNEL_NETBOOT_HOSTNAME:-boot.every.channel}"
|
||||
http_port="${EVERY_CHANNEL_NETBOOT_HTTP_PORT:-8080}"
|
||||
verify_release_checksums="${EVERY_CHANNEL_NETBOOT_VERIFY_RELEASE_CHECKSUMS:-true}"
|
||||
allow_remote_ipxe="${EVERY_CHANNEL_NETBOOT_ALLOW_REMOTE_IPXE:-false}"
|
||||
ipxe_efi_sha256="${EVERY_CHANNEL_IPXE_EFI_SHA256:-}"
|
||||
chain_token="${EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN:-}"
|
||||
token="${EVERY_CHANNEL_FORGE_TOKEN:-${FORGE_TOKEN:-${CODEBERG_TOKEN:-}}}"
|
||||
|
||||
need_cmd() {
|
||||
|
|
@ -26,6 +32,54 @@ need_cmd curl
|
|||
need_cmd tar
|
||||
need_cmd python3
|
||||
|
||||
bool_norm() {
|
||||
local raw
|
||||
raw="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
|
||||
case "${raw}" in
|
||||
''|true|1|yes|y|on) echo "true" ;;
|
||||
false|0|no|n|off) echo "false" ;;
|
||||
*)
|
||||
echo "error: invalid boolean value '${1}'" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_sha256() {
|
||||
local file_path="$1"
|
||||
local expected="$2"
|
||||
local actual
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
actual="$(sha256sum "${file_path}" | awk '{print $1}')"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
actual="$(shasum -a 256 "${file_path}" | awk '{print $1}')"
|
||||
else
|
||||
echo "error: checksum verification requested but no sha256 tool is available" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "${actual}" != "${expected}" ]]; then
|
||||
echo "error: checksum mismatch for ${file_path}" >&2
|
||||
exit 2
|
||||
fi
|
||||
echo "$(basename "${file_path}"): OK"
|
||||
}
|
||||
|
||||
validate_chain_token() {
|
||||
local value="$1"
|
||||
if [[ -z "${value}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ ! "${value}" =~ ^[A-Za-z0-9._~-]{16,128}$ ]]; then
|
||||
echo "error: EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN must match [A-Za-z0-9._~-]{16,128}" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
allow_remote_ipxe="$(bool_norm "${allow_remote_ipxe}")"
|
||||
validate_chain_token "${chain_token}"
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "${tmp_dir}"
|
||||
|
|
@ -34,6 +88,8 @@ trap cleanup EXIT
|
|||
|
||||
archive_path="${tmp_dir}/netboot.tar.gz"
|
||||
release_asset_url=""
|
||||
release_asset_name=""
|
||||
checksum_asset_url=""
|
||||
|
||||
if [[ -n "${local_tarball}" ]]; then
|
||||
if [[ ! -f "${local_tarball}" ]]; then
|
||||
|
|
@ -56,7 +112,7 @@ else
|
|||
release_json="${tmp_dir}/release.json"
|
||||
curl -fsSL "${auth_args[@]}" "${release_endpoint}" -o "${release_json}"
|
||||
|
||||
release_asset_url="$(
|
||||
parsed_assets="$(
|
||||
python3 - "${release_json}" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
|
@ -77,9 +133,20 @@ if not candidates:
|
|||
|
||||
# Pick newest by release ordering if API already sorted; otherwise prefer largest id.
|
||||
chosen = sorted(candidates, key=lambda x: x.get("id", 0))[-1]
|
||||
checksum_asset = None
|
||||
for asset in assets:
|
||||
if asset.get("name") == "SHA256SUMS.txt":
|
||||
checksum_asset = asset
|
||||
break
|
||||
|
||||
print(chosen.get("name", ""))
|
||||
print(chosen.get("browser_download_url", ""))
|
||||
print("" if checksum_asset is None else checksum_asset.get("browser_download_url", ""))
|
||||
PY
|
||||
)"
|
||||
release_asset_name="$(printf '%s\n' "${parsed_assets}" | sed -n '1p')"
|
||||
release_asset_url="$(printf '%s\n' "${parsed_assets}" | sed -n '2p')"
|
||||
checksum_asset_url="$(printf '%s\n' "${parsed_assets}" | sed -n '3p')"
|
||||
|
||||
if [[ -z "${release_asset_url}" ]]; then
|
||||
echo "error: unable to find x86_64 netboot asset in release" >&2
|
||||
|
|
@ -87,6 +154,28 @@ PY
|
|||
fi
|
||||
|
||||
curl -fsSL "${auth_args[@]}" -o "${archive_path}" "${release_asset_url}"
|
||||
|
||||
verify_release_checksums_lc="$(printf '%s' "${verify_release_checksums}" | tr '[:upper:]' '[:lower:]')"
|
||||
if [[ "${verify_release_checksums_lc}" != "false" && -n "${checksum_asset_url}" ]]; then
|
||||
checksum_file="${tmp_dir}/SHA256SUMS.txt"
|
||||
curl -fsSL "${auth_args[@]}" -o "${checksum_file}" "${checksum_asset_url}"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
(
|
||||
cd "${tmp_dir}"
|
||||
grep -F " ${release_asset_name}" "${checksum_file}" | sha256sum -c -
|
||||
)
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
expected="$(grep -F " ${release_asset_name}" "${checksum_file}" | awk '{print $1}')"
|
||||
actual="$(shasum -a 256 "${archive_path}" | awk '{print $1}')"
|
||||
if [[ -z "${expected}" || "${expected}" != "${actual}" ]]; then
|
||||
echo "error: checksum mismatch for ${release_asset_name}" >&2
|
||||
exit 2
|
||||
fi
|
||||
echo "${release_asset_name}: OK"
|
||||
else
|
||||
echo "warning: no sha256 tool available; skipping checksum verification"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
http_dir="${out_root}/http"
|
||||
|
|
@ -103,23 +192,47 @@ for required in kernel initrd netboot.ipxe; do
|
|||
fi
|
||||
done
|
||||
|
||||
curl -fsSL -o "${tftp_dir}/ipxe.efi" "${ipxe_efi_url}"
|
||||
if [[ -n "${ipxe_efi_path}" ]]; then
|
||||
if [[ ! -f "${ipxe_efi_path}" ]]; then
|
||||
echo "error: iPXE file path not found: ${ipxe_efi_path}" >&2
|
||||
exit 2
|
||||
fi
|
||||
ipxe_dest="${tftp_dir}/${ipxe_efi_filename}"
|
||||
src_resolved="$(realpath -m "${ipxe_efi_path}")"
|
||||
dst_resolved="$(realpath -m "${ipxe_dest}")"
|
||||
# When embedded iPXE already wrote to the destination path, skip self-copy.
|
||||
if [[ "${src_resolved}" != "${dst_resolved}" ]]; then
|
||||
cp -f "${ipxe_efi_path}" "${ipxe_dest}"
|
||||
fi
|
||||
else
|
||||
if [[ "${allow_remote_ipxe}" != "true" ]]; then
|
||||
echo "error: remote iPXE download is disabled by default" >&2
|
||||
echo "hint: build local iPXE with ./scripts/netboot-build-ipxe.sh and set EVERY_CHANNEL_IPXE_EFI_PATH" >&2
|
||||
echo "hint: if you must download from URL, set EVERY_CHANNEL_NETBOOT_ALLOW_REMOTE_IPXE=true" >&2
|
||||
exit 2
|
||||
fi
|
||||
curl -fsSL -o "${tftp_dir}/${ipxe_efi_filename}" "${ipxe_efi_url}"
|
||||
fi
|
||||
if [[ -n "${ipxe_efi_sha256}" ]]; then
|
||||
check_sha256 "${tftp_dir}/${ipxe_efi_filename}" "${ipxe_efi_sha256}"
|
||||
fi
|
||||
cp -f "${http_dir}/netboot.ipxe" "${tftp_dir}/netboot.ipxe"
|
||||
|
||||
cat > "${tftp_dir}/bootstrap.ipxe" <<'EOF'
|
||||
chain_url="http://${netboot_hostname}:${http_port}/netboot.ipxe"
|
||||
if [[ -n "${chain_token}" ]]; then
|
||||
chain_url="${chain_url}?token=${chain_token}"
|
||||
fi
|
||||
|
||||
cat > "${tftp_dir}/bootstrap.ipxe" <<EOF
|
||||
#!ipxe
|
||||
dhcp
|
||||
chain http://__NETBOOT_HOST__:__HTTP_PORT__/netboot.ipxe
|
||||
chain ${chain_url}
|
||||
EOF
|
||||
sed -i.bak \
|
||||
-e "s#__NETBOOT_HOST__#${netboot_hostname}#g" \
|
||||
-e "s#__HTTP_PORT__#${http_port}#g" \
|
||||
"${tftp_dir}/bootstrap.ipxe"
|
||||
rm -f "${tftp_dir}/bootstrap.ipxe.bak"
|
||||
|
||||
echo "ok: staged netboot content"
|
||||
echo "ok: http root: ${http_dir}"
|
||||
echo "ok: tftp root: ${tftp_dir}"
|
||||
echo "ok: boot filename: ${ipxe_efi_filename}"
|
||||
echo "ok: netboot hostname: ${netboot_hostname}"
|
||||
echo "ok: netboot http port: ${http_port}"
|
||||
if [[ -n "${release_asset_url}" ]]; then
|
||||
|
|
@ -127,4 +240,7 @@ if [[ -n "${release_asset_url}" ]]; then
|
|||
else
|
||||
echo "ok: source asset: ${local_tarball}"
|
||||
fi
|
||||
echo "hint: run sudo ./scripts/netboot-serve.sh to expose HTTP+TFTP+ProxyDHCP"
|
||||
if [[ -n "${chain_token}" ]]; then
|
||||
echo "ok: chain token enabled"
|
||||
fi
|
||||
echo "hint: run sudo ./scripts/netboot-serve.sh to expose HTTP+TFTP (+ optional ProxyDHCP)"
|
||||
|
|
|
|||
127
scripts/op-stack/anvil-reality-smoke.sh
Executable file
127
scripts/op-stack/anvil-reality-smoke.sh
Executable file
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
remote="${EVERY_CHANNEL_REALITY_SMOKE_REMOTE:-root@git.every.channel}"
|
||||
remote_jsonl="${EVERY_CHANNEL_REALITY_SMOKE_JSONL:-/var/lib/every-channel/manifests/la-cbs/video0.m4s.jsonl}"
|
||||
remote_identity_file="${EVERY_CHANNEL_REALITY_SMOKE_SSH_IDENTITY_FILE:-${HOME}/.ssh/id_ed25519}"
|
||||
anvil_port="${EVERY_CHANNEL_REALITY_SMOKE_ANVIL_PORT:-8545}"
|
||||
results_dir="${EVERY_CHANNEL_REALITY_SMOKE_RESULTS_DIR:-./test-results}"
|
||||
|
||||
anvil_pk0="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
||||
anvil_pk1="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
|
||||
anvil_pk2="0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
|
||||
|
||||
mkdir -p "${results_dir}"
|
||||
anvil_log="${results_dir}/anvil-reality-smoke.log"
|
||||
deploy_json="${results_dir}/observation-ledger-deploy.json"
|
||||
result_json="${results_dir}/anvil-reality-smoke.json"
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${anvil_pid:-}" ]]; then
|
||||
kill "${anvil_pid}" >/dev/null 2>&1 || true
|
||||
wait "${anvil_pid}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
anvil --port "${anvil_port}" >"${anvil_log}" 2>&1 &
|
||||
anvil_pid=$!
|
||||
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -fsS -X POST -H 'content-type: application/json' \
|
||||
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
|
||||
"http://127.0.0.1:${anvil_port}" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
rpc_url="http://127.0.0.1:${anvil_port}"
|
||||
owner_file="$(mktemp)"
|
||||
printf '%s\n' "${anvil_pk0}" >"${owner_file}"
|
||||
|
||||
EVERY_CHANNEL_RPC_URL="${rpc_url}" \
|
||||
EVERY_CHANNEL_PRIVATE_KEY_FILE="${owner_file}" \
|
||||
EVERY_CHANNEL_OBSERVATION_DEPLOY_OUT="${deploy_json}" \
|
||||
./scripts/op-stack/deploy-observation-ledger.sh >/dev/null
|
||||
|
||||
registry="$(jq -r '.registry' <"${deploy_json}")"
|
||||
ledger="$(jq -r '.ledger' <"${deploy_json}")"
|
||||
|
||||
witness1="$(cast wallet address --private-key "${anvil_pk1}")"
|
||||
witness2="$(cast wallet address --private-key "${anvil_pk2}")"
|
||||
|
||||
cast send "${registry}" "addWitness(address)" "${witness1}" --rpc-url "${rpc_url}" --private-key "${anvil_pk0}" >/dev/null
|
||||
cast send "${registry}" "addWitness(address)" "${witness2}" --rpc-url "${rpc_url}" --private-key "${anvil_pk0}" >/dev/null
|
||||
|
||||
remote_jsonl_escaped="$(printf '%q' "${remote_jsonl}")"
|
||||
entry="$(ssh \
|
||||
-i "${remote_identity_file}" \
|
||||
-o IdentityAgent=none \
|
||||
-o IdentitiesOnly=yes \
|
||||
-o BatchMode=yes \
|
||||
-o ConnectTimeout=10 \
|
||||
"${remote}" \
|
||||
"head -n 1 -- ${remote_jsonl_escaped}")"
|
||||
if [[ -z "${entry}" ]]; then
|
||||
echo "no remote archive entry found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
broadcast_name="$(jq -r '.broadcast_name' <<<"${entry}")"
|
||||
track_name="$(jq -r '.track_name' <<<"${entry}")"
|
||||
group_sequence="$(jq -r '.group_sequence' <<<"${entry}")"
|
||||
received_unix_ms="$(jq -r '.received_unix_ms' <<<"${entry}")"
|
||||
blake3_hex="$(jq -r '.blake3' <<<"${entry}")"
|
||||
locator_material="$(jq -c '{relay_url,broadcast_name,track_name,group_sequence,cas_path}' <<<"${entry}")"
|
||||
|
||||
stream_hash="$(cast keccak "${broadcast_name}:${track_name}")"
|
||||
epoch_hash="$(cast keccak "${broadcast_name}:${track_name}:${group_sequence}")"
|
||||
parent_hash="0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
data_root="0x${blake3_hex}"
|
||||
locator_hash="$(cast keccak "${locator_material}")"
|
||||
|
||||
observation_tuple="(${stream_hash},${epoch_hash},${parent_hash},${data_root},${locator_hash},${received_unix_ms},${group_sequence})"
|
||||
cast send "${ledger}" \
|
||||
"proposeObservation((bytes32,bytes32,bytes32,bytes32,bytes32,uint64,uint64))" \
|
||||
"${observation_tuple}" \
|
||||
--rpc-url "${rpc_url}" \
|
||||
--private-key "${anvil_pk1}" >/dev/null
|
||||
|
||||
observation_hash="$(cast call "${ledger}" \
|
||||
"hashObservationHeader((bytes32,bytes32,bytes32,bytes32,bytes32,uint64,uint64))(bytes32)" \
|
||||
"${observation_tuple}" \
|
||||
--rpc-url "${rpc_url}")"
|
||||
slot_hash="$(cast call "${ledger}" \
|
||||
"observationSlot(bytes32,bytes32)(bytes32)" \
|
||||
"${stream_hash}" "${epoch_hash}" \
|
||||
--rpc-url "${rpc_url}")"
|
||||
|
||||
cast send "${ledger}" "attestObservation(bytes32)" "${observation_hash}" \
|
||||
--rpc-url "${rpc_url}" \
|
||||
--private-key "${anvil_pk2}" >/dev/null
|
||||
|
||||
finalized="$(cast call "${ledger}" "finalizedObservationBySlot(bytes32)(bytes32)" "${slot_hash}" --rpc-url "${rpc_url}")"
|
||||
|
||||
cat >"${result_json}" <<EOF
|
||||
{
|
||||
"remote": "${remote}",
|
||||
"remote_jsonl": "${remote_jsonl}",
|
||||
"broadcast_name": "${broadcast_name}",
|
||||
"track_name": "${track_name}",
|
||||
"group_sequence": ${group_sequence},
|
||||
"stream_hash": "${stream_hash}",
|
||||
"epoch_hash": "${epoch_hash}",
|
||||
"observation_hash": "${observation_hash}",
|
||||
"finalized_hash": "${finalized}",
|
||||
"registry": "${registry}",
|
||||
"ledger": "${ledger}"
|
||||
}
|
||||
EOF
|
||||
|
||||
if [[ "${finalized}" != "${observation_hash}" ]]; then
|
||||
echo "observation did not finalize" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "${result_json}"
|
||||
46
scripts/op-stack/deploy-observation-ledger.sh
Executable file
46
scripts/op-stack/deploy-observation-ledger.sh
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
rpc_url="${EVERY_CHANNEL_RPC_URL:?set EVERY_CHANNEL_RPC_URL}"
|
||||
private_key_file="${EVERY_CHANNEL_PRIVATE_KEY_FILE:?set EVERY_CHANNEL_PRIVATE_KEY_FILE}"
|
||||
quorum="${EVERY_CHANNEL_OBSERVATION_QUORUM:-2}"
|
||||
out="${EVERY_CHANNEL_OBSERVATION_DEPLOY_OUT:-./test-results/observation-ledger-deploy.json}"
|
||||
|
||||
private_key="$(tr -d '\r\n' <"${private_key_file}")"
|
||||
private_key="${private_key#0x}"
|
||||
|
||||
mkdir -p "$(dirname "${out}")"
|
||||
|
||||
forge build >/dev/null
|
||||
|
||||
owner="$(cast wallet address --private-key "0x${private_key}")"
|
||||
|
||||
registry_output="$(forge create contracts/EveryChannelWitnessRegistry.sol:EveryChannelWitnessRegistry \
|
||||
--rpc-url "${rpc_url}" \
|
||||
--private-key "0x${private_key}" \
|
||||
--broadcast \
|
||||
--constructor-args "${owner}")"
|
||||
registry_address="$(printf '%s\n' "${registry_output}" | awk '/Deployed to:/ { print $3 }' | tail -n1)"
|
||||
|
||||
ledger_output="$(forge create contracts/EveryChannelObservationLedger.sol:EveryChannelObservationLedger \
|
||||
--rpc-url "${rpc_url}" \
|
||||
--private-key "0x${private_key}" \
|
||||
--broadcast \
|
||||
--constructor-args "${registry_address}" "${quorum}")"
|
||||
ledger_address="$(printf '%s\n' "${ledger_output}" | awk '/Deployed to:/ { print $3 }' | tail -n1)"
|
||||
|
||||
if [[ -z "${registry_address}" || -z "${ledger_address}" ]]; then
|
||||
echo "failed to deploy contracts" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat >"${out}" <<EOF
|
||||
{
|
||||
"owner": "${owner}",
|
||||
"registry": "${registry_address}",
|
||||
"ledger": "${ledger_address}",
|
||||
"quorum": ${quorum}
|
||||
}
|
||||
EOF
|
||||
|
||||
printf '%s\n' "${out}"
|
||||
49
scripts/op-stack/download-op-deployer.sh
Executable file
49
scripts/op-stack/download-op-deployer.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
tag="${EVERY_CHANNEL_OP_DEPLOYER_TAG:-op-deployer/v0.6.0-rc.3}"
|
||||
out="${EVERY_CHANNEL_OP_DEPLOYER_OUT:-./op-deployer}"
|
||||
|
||||
log() {
|
||||
printf '[op-deployer] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
detect_platform() {
|
||||
local os arch
|
||||
case "$(uname -s)" in
|
||||
Darwin) os="darwin" ;;
|
||||
Linux) os="linux" ;;
|
||||
*) echo "unsupported-os" ; return 1 ;;
|
||||
esac
|
||||
case "$(uname -m)" in
|
||||
aarch64|arm64) arch="arm64" ;;
|
||||
x86_64|amd64) arch="amd64" ;;
|
||||
*) echo "unsupported-arch" ; return 1 ;;
|
||||
esac
|
||||
printf '%s-%s\n' "$os" "$arch"
|
||||
}
|
||||
|
||||
platform="$(detect_platform)"
|
||||
if [[ "$platform" == unsupported-* ]]; then
|
||||
log "unsupported platform: $(uname -s)/$(uname -m)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
version="${tag#op-deployer/v}"
|
||||
archive="op-deployer-${version}-${platform}.tar.gz"
|
||||
url="https://github.com/ethereum-optimism/optimism/releases/download/${tag}/${archive}"
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
log "downloading ${tag} for ${platform}"
|
||||
curl -fsSL "$url" -o "${tmpdir}/archive.tar.gz"
|
||||
tar -xzf "${tmpdir}/archive.tar.gz" -C "$tmpdir"
|
||||
|
||||
candidate="$(find "$tmpdir" -type f -name 'op-deployer*' -perm -u+x | head -n 1)"
|
||||
if [[ -z "$candidate" ]]; then
|
||||
log "failed to locate extracted op-deployer binary"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -m 0755 "$candidate" "$out"
|
||||
log "wrote ${out}"
|
||||
245
scripts/op-stack/setup-rollup.sh
Executable file
245
scripts/op-stack/setup-rollup.sh
Executable file
|
|
@ -0,0 +1,245 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="${EVERY_CHANNEL_OP_STACK_ROOT:?set EVERY_CHANNEL_OP_STACK_ROOT}"
|
||||
private_key_file="${EVERY_CHANNEL_OP_STACK_PRIVATE_KEY_FILE:?set EVERY_CHANNEL_OP_STACK_PRIVATE_KEY_FILE}"
|
||||
l1_rpc_url="${EVERY_CHANNEL_OP_STACK_L1_RPC_URL:-https://ethereum-sepolia-rpc.publicnode.com}"
|
||||
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}"
|
||||
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}"
|
||||
skip_apply="${EVERY_CHANNEL_OP_STACK_SKIP_APPLY:-false}"
|
||||
challenger_prestate_file="${EVERY_CHANNEL_OP_STACK_CHALLENGER_PRESTATE_FILE:-}"
|
||||
|
||||
deployer_dir="${root}/deployer"
|
||||
sequencer_dir="${root}/sequencer"
|
||||
batcher_dir="${root}/batcher"
|
||||
proposer_dir="${root}/proposer"
|
||||
challenger_dir="${root}/challenger"
|
||||
dispute_mon_dir="${root}/dispute-mon"
|
||||
challenger_prestate_name=""
|
||||
|
||||
log() {
|
||||
printf '[op-stack] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
log "missing required command: $1"
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
|
||||
trimmed_file_contents() {
|
||||
tr -d '\r\n' <"$1"
|
||||
}
|
||||
|
||||
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
|
||||
key, value, path = sys.argv[1:]
|
||||
text = Path(path).read_text()
|
||||
needle = f"{key} = "
|
||||
out = []
|
||||
replaced = False
|
||||
for line in text.splitlines():
|
||||
if line.startswith(needle):
|
||||
out.append(f'{key} = "{value}"')
|
||||
replaced = True
|
||||
else:
|
||||
out.append(line)
|
||||
if not replaced:
|
||||
raise SystemExit(f"failed to replace {key}")
|
||||
Path(path).write_text("\n".join(out) + "\n")
|
||||
PY
|
||||
}
|
||||
|
||||
mkdir -p "${root}/bin" "$deployer_dir" "$sequencer_dir" "$batcher_dir" "$proposer_dir" \
|
||||
"$challenger_dir" "$challenger_dir/data" "$dispute_mon_dir"
|
||||
|
||||
require_cmd cast
|
||||
require_cmd curl
|
||||
require_cmd jq
|
||||
require_cmd openssl
|
||||
|
||||
if [[ ! -x "$op_deployer_bin" ]]; then
|
||||
if [[ -z "$download_script" ]]; then
|
||||
log "op-deployer missing and no download script configured"
|
||||
exit 2
|
||||
fi
|
||||
log "downloading op-deployer via ${download_script}"
|
||||
EVERY_CHANNEL_OP_DEPLOYER_TAG="$download_tag" \
|
||||
EVERY_CHANNEL_OP_DEPLOYER_OUT="$op_deployer_bin" \
|
||||
"$download_script"
|
||||
fi
|
||||
|
||||
private_key="$(trimmed_file_contents "$private_key_file")"
|
||||
private_key="${private_key#0x}"
|
||||
if [[ ${#private_key} -ne 64 ]]; then
|
||||
log "private key file must contain exactly 32 bytes of hex"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
operator_address="$(cast wallet address --private-key "0x${private_key}")"
|
||||
|
||||
cat > "${deployer_dir}/.env" <<EOF
|
||||
L1_RPC_URL=${l1_rpc_url}
|
||||
PRIVATE_KEY=${private_key}
|
||||
EOF
|
||||
|
||||
if [[ ! -d "${deployer_dir}/.deployer" ]]; then
|
||||
"$op_deployer_bin" init \
|
||||
--l1-chain-id 11155111 \
|
||||
--l2-chain-ids "${chain_id}" \
|
||||
--workdir "${deployer_dir}/.deployer" \
|
||||
--intent-type standard-overrides
|
||||
fi
|
||||
|
||||
intent="${deployer_dir}/.deployer/intent.toml"
|
||||
chain_id_hex="$(printf '0x%064x' "${chain_id}")"
|
||||
set_toml_value "id" "${chain_id_hex}" "${intent}"
|
||||
for key in \
|
||||
baseFeeVaultRecipient \
|
||||
l1FeeVaultRecipient \
|
||||
sequencerFeeVaultRecipient \
|
||||
operatorFeeVaultRecipient \
|
||||
chainFeesRecipient \
|
||||
systemConfigOwner \
|
||||
unsafeBlockSigner \
|
||||
batcher \
|
||||
proposer \
|
||||
challenger
|
||||
do
|
||||
set_toml_value "${key}" "${operator_address}" "${intent}"
|
||||
done
|
||||
|
||||
python - "${intent}" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
path = Path(sys.argv[1])
|
||||
text = path.read_text()
|
||||
if 'fundDevAccounts = false' not in text:
|
||||
text = text.replace('fundDevAccounts = true', 'fundDevAccounts = false')
|
||||
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}"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${deployer_dir}/.deployer/state.json" ]]; then
|
||||
log "state.json missing; 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"
|
||||
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}")"
|
||||
|
||||
if [[ -z "${system_config_proxy}" || "${system_config_proxy}" == "null" ]]; then
|
||||
log "failed to extract systemConfigProxyAddress from state.json"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${dispute_game_factory}" || "${dispute_game_factory}" == "null" ]]; then
|
||||
log "failed to extract disputeGameFactoryProxyAddress from state.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${challenger_prestate_file}" ]]; then
|
||||
if [[ ! -f "${challenger_prestate_file}" ]]; then
|
||||
log "challenger prestate file does not exist: ${challenger_prestate_file}"
|
||||
exit 1
|
||||
fi
|
||||
challenger_prestate_name="$(basename "${challenger_prestate_file}")"
|
||||
cp "${challenger_prestate_file}" "${challenger_dir}/${challenger_prestate_name}"
|
||||
chmod 0640 "${challenger_dir}/${challenger_prestate_name}"
|
||||
fi
|
||||
|
||||
cat > "${sequencer_dir}/.env" <<EOF
|
||||
L1_RPC_URL=${l1_rpc_url}
|
||||
L1_BEACON_URL=${l1_beacon_url}
|
||||
PRIVATE_KEY=${private_key}
|
||||
P2P_ADVERTISE_IP=${p2p_advertise_ip}
|
||||
L2_CHAIN_ID=${chain_id}
|
||||
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
|
||||
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
|
||||
GAME_FACTORY_ADDRESS=${dispute_game_factory}
|
||||
PRIVATE_KEY=${private_key}
|
||||
PROPOSAL_INTERVAL=3600s
|
||||
EOF
|
||||
|
||||
cp "${sequencer_dir}/genesis.json" "${challenger_dir}/genesis.json"
|
||||
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
|
||||
GAME_FACTORY_ADDRESS=${dispute_game_factory}
|
||||
PRIVATE_KEY=${private_key}
|
||||
EOF
|
||||
|
||||
if [[ -n "${challenger_prestate_name}" ]]; then
|
||||
cat >> "${challenger_dir}/.env" <<EOF
|
||||
CANNON_PRESTATE=/workspace/${challenger_prestate_name}
|
||||
EOF
|
||||
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_HONEST_ACTORS=${operator_address}
|
||||
OP_DISPUTE_MON_GAME_FACTORY_ADDRESS=${dispute_game_factory}
|
||||
OP_DISPUTE_MON_MONITOR_INTERVAL=10s
|
||||
OP_DISPUTE_MON_NETWORK=ecp-sepolia
|
||||
OP_DISPUTE_MON_LOG_LEVEL=debug
|
||||
OP_DISPUTE_MON_LOG_FORMAT=logfmt
|
||||
OP_DISPUTE_MON_METRICS_ADDR=0.0.0.0
|
||||
OP_DISPUTE_MON_METRICS_ENABLED=true
|
||||
OP_DISPUTE_MON_METRICS_PORT=7300
|
||||
EOF
|
||||
|
||||
cat > "${root}/deployment.json" <<EOF
|
||||
{
|
||||
"chain_id": ${chain_id},
|
||||
"operator_address": "${operator_address}",
|
||||
"l1_rpc_url": "${l1_rpc_url}",
|
||||
"l1_beacon_url": "${l1_beacon_url}",
|
||||
"p2p_advertise_ip": "${p2p_advertise_ip}",
|
||||
"system_config_proxy": "${system_config_proxy}",
|
||||
"dispute_game_factory": "${dispute_game_factory}",
|
||||
"l1_standard_bridge": "${l1_standard_bridge}"
|
||||
}
|
||||
EOF
|
||||
|
||||
log "wrote bootstrap outputs under ${root}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue