Skip to content

flashbox-l1: replace lighthouse with sync-proxy - #197

Draft
MoeMahhouk wants to merge 6 commits into
mainfrom
moe/flashbox-l1-sync-proxy
Draft

flashbox-l1: replace lighthouse with sync-proxy#197
MoeMahhouk wants to merge 6 commits into
mainfrom
moe/flashbox-l1-sync-proxy

Conversation

@MoeMahhouk

@MoeMahhouk MoeMahhouk commented Aug 25, 2026

Copy link
Copy Markdown
Member

Summary

Removes the in-image Lighthouse beacon node from flashbox-l1 and replaces it with sync-proxy in mirror mode. Consensus is now provided by an external sync cluster (CL + reference EL + nginx mirror) in our
own VPC, which mirrors its Engine API calls to every flashbox; sync-proxy on the host receives them on :8552 and drives the searcher EL on :8551 exactly as lighthouse did.

image

Changes

Build

  • mkosi.build: Go make_git_package build of sync-proxy, pinned to commit 870715b (tip of the mirror-mode/metrics PR stack: feat: Add optional Prometheus metrics endpoint sync-proxy#24 see below). Drops the Rust toolchain, the C build deps and libsnappy1v5 that only lighthouse needed. Image shrinks 312 MB → 285 MB.

JWT

  • engine-jwt.service fetches the JWT secret shared with the sync cluster from Vault (ENGINE_API_JWT_SECRET, hex-64, in the existing node/<suffix> blob) into /tmp/jwt.hex; the container bind mount at /secrets/jwt.hex is unchanged.
    searcher-container.service Requires= it so podman never sees a missing file. QEMU dev images use a fixed test secret. Bounded retry (~5 min), then fail loudly.
  • vault.sh: factor vault_read_secret out of vault_fetch so observability and the JWT fetch share the Vault login. No behavior change for metrics.
  • sync-proxy itself is JWT-agnostic: it passes the CL's Authorization header through and the searcher's EL validates it against the same shared secret.

Service / logs

  • sync-proxy.service: -addr 0.0.0.0:8552 -builders http://localhost:8551 -mirror-mode -metrics-addr 127.0.0.1:9106. Logs to /persistent/sync_proxy_logs (rotated via /etc/logrotate.d, which the common logrotate.conf now includes) and bind-mounted
    read-only into the container at /var/log/sync-proxy, replacing /var/log/lighthouse.
  • sync-proxy-init.service (oneshot) creates the log dir — systemd opens
    StandardOutput=append: before any ExecStartPre, so the unit can't create its own.

Firewall

  • Drop the CL P2P 9000 rules (in/out) and the netns 9000 drops.
  • ALWAYS_IN tcp/8552 from SYNC_PROXY_ALLOWED_SRC (currently 10.0.0.0/8, see TODO); always-on because engine drive must survive maintenance mode.
  • Container netns drops 8552 (the podman subnet is "internal" too). Verified in QEMU: the container reaches the host on 22 but not on 8552.
  • toggle's netns sanity check now expects the tcp/8552 drop instead of the 9000 drops (shared with flashbox-l2)

Searcher UX

  • searchersh restart-lighthouserestart-sync-proxy; sudoers updated.

Observability

  • Scrape job sync-proxy (keeps syncproxy_.* locally only) and remote-written booleans flashbox:searcher_receiving_engine_calls (EL accepted a forwarded newPayload/forkchoiceUpdated in the last 5m — distinguishes EL down / hanging / rejecting the JWT from healthy) and flashbox:sync_proxy_is_up. promtool-validated.

Docs: readme firewall table, netns rules, searcher contract, boot order.

Testing

  • Dev image builds clean; rootfs inspected: binary reports version 870715b with -mirror-mode/-metrics-addr, units wanted by minimal.target, no lighthouse leftovers.
  • QEMU e2e (21/21 on a clean boot): all four units come up in order; JWT and log mounts present in the container; a JWT-validating mock EL inside the sandbox received a forwarded engine_newPayloadV1 signed with the shared secret (success counter 0→1), a wrong-secret token was rejected by the EL (http_error 0→1), non-engine calls are never forwarded, ACK latency ~1.5 ms, logs on /persistent readable from the container.
  • Not testable off-GCP (needs real setup with devops): Vault fetch path, Prometheus recording rule → remote write.
  • toggle round-trip verified in QEMU: production switch accepted, 8552 still serving engine calls in production.

Follow-ups / coordination with devops

  • Add ENGINE_API_JWT_SECRET (hex-64) to the Vault node/<suffix> blob, same value the sync cluster's CL uses.
  • Narrow SYNC_PROXY_ALLOWED_SRC in firewall-config to the sync cluster subnet / mirror IP.
  • Bump the sync-proxy pin in mkosi.build from 870715b to a release tag once flashbots/sync-proxy PR stack (bounded timeouts, mirror mode, metrics) are merged.
  • Sync cluster deployment (CL + nginx mirror + reference EL), GCP VPC firewall for :8552, image release and per-searcher rollout; searchers must switch restart-lighthouserestart-sync-proxy and /var/log/lighthouse/var/log/sync-proxy.

No consensus client runs inside the image anymore. An external sync cluster
(CL + reference EL + nginx mirror in our VPC) mirrors its Engine API calls to
each flashbox; sync-proxy on the host receives them on :8552 in mirror mode
(immediate empty ACK, async forward) and drives the searcher EL on :8551.

- build sync-proxy reproducibly via make_git_package (Go), drop the Rust
  toolchain/C deps and libsnappy that only lighthouse needed
- engine-jwt.service fetches the JWT secret shared with the sync cluster from
  Vault (ENGINE_API_JWT_SECRET) into /tmp/jwt.hex; the container Requires it
  so the bind mount never sees a missing file. QEMU devmode uses a fixed
  test secret. sync-proxy itself is JWT-agnostic (pure pass-through).
- factor vault_read_secret out of vault.sh so both observability and the JWT
  fetch share the Vault login
- sync-proxy logs go to /persistent/sync_proxy_logs (rotated via
  /etc/logrotate.d) and are mounted read-only into the container at
  /var/log/sync-proxy, replacing /var/log/lighthouse

sync-proxy is pinned to commit 870715b pending upstream merge of the
mirror-mode/metrics PRs.
- drop the CL P2P (9000) in/out rules and the matching netns drops
- accept sync-proxy :8552 inbound in ALWAYS_IN (engine drive must survive
  maintenance mode) from internal source addresses only; the container netns
  drops :8552 since the podman subnet is internal too
- searchersh restart-lighthouse -> restart-sync-proxy, sudoers updated
Scrape sync-proxy metrics locally and derive the remote-written booleans
flashbox:searcher_receiving_engine_calls (EL accepted a forwarded
newPayload/forkchoiceUpdated in the last 5m) and flashbox:sync_proxy_is_up.
Raw syncproxy_* series never leave the box.
systemd opens StandardOutput=append: before running any Exec*, so having
ExecStartPre create /persistent/sync_proxy_logs failed with
'Failed to set up standard output', and the container then failed to bind
mount the missing directory. sync-proxy-init.service now creates it after
/persistent is mounted; sync-proxy.service and searcher-container.service
Require it. Found by the QEMU e2e run.
…L P2P

toggle refuses to enter production unless the container netns carries the
expected DROP rules. Those still named the CL P2P port 9000, which
init-container.sh no longer sets, so production toggle would have been
refused. Check the tcp/8552 sync-proxy drop instead.
- move vault.sh into the common module: fetch-engine-jwt (flashbox-l1) and
  flashbox-observability-setup both source it, so it must not depend on the
  observability module being included
- flashbox:searcher_receiving_engine_calls: 'or vector(0)' so the boolean is
  an explicit 0 before the first-ever successful forward instead of absent
- fix the retry-window comment in fetch-engine-jwt, document the container's
  hard dependency on the JWT fetch and the sync-proxy logrotate policy in
  the readme, replace the lighthouse example in DEVELOPMENT.md

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we decide to just hardcode the JWT, then we can remove such dependency because this would cause the searcher container to simply fail to initialize and load in case the vault is not reachable our timeouts more than 5 minutes.
Hardcoding the JWT secrets would simplify also the changes and reduce the need for the engine-jwt service and vault extraction/parsing additions. CC @ilyaluk @pablin-10 , deferring to you to decide on what is best from operations perspective. We already discussed that this is not a security concern in this particular use-case/product

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant