network(ethp2p): enable with just ZEAM_ETHP2P=1 — auto-derive endpoints + per-node runtime TLS cert - #1049
Conversation
…evnet cert Revives #1045 on top of main (which already carries the ethp2p adapter via #1046 and the env-config layer via #1047). Adds the pieces that were only on the old #1045 branch, so an operator can enable ethp2p with just `ZEAM_ETHP2P=1` — no manual port/cert wiring: - `buildEthp2pConfig` derives the ethp2p listen address and static peers from the node's OWN libp2p QUIC addresses, shifted by `ZEAM_ETHP2P_PORT_OFFSET` (default +1, the ethlambda "ethp2p = gossipsub port + 1" convention). Every field keeps explicit-env precedence (`ZEAM_ETHP2P_LISTEN`, `ZEAM_ETHP2P_STATIC_PEERS`, ...) over the derived default. - Identity is the node's `node_key` (unique per node; retained by the RS engine and outlives the process — so the shared TLS cert below is NOT the peer id and cannot cause identity collisions). - Bundled self-signed devnet TLS cert/key at `/app/resources/ethp2p/{cert,key}.pem` (shipped via the existing `COPY resources/`), overridable with `ZEAM_ETHP2P_SERVER_CERT` / `ZEAM_ETHP2P_SERVER_KEY`. - Owned strings (listen_addr, static_peers) are freed via `freeEthp2pConfig` after `beam_node.init` — the adapter's `start` binds/dials synchronously and retains only `local_peer_id` (= long-lived `node_key`). Deliberately does NOT include the old #1045 `pkgs/node/src/node.zig` hunk, which predated and would revert the blocks_by_range sync-wedge fix (#1046). Runtime activation unchanged and doubly gated: `-Dethp2p=true` build AND `ZEAM_ETHP2P` truthy. Default build/test unaffected (adapter comptime-excluded); `-Dethp2p=true` compiles.
…d keypair
Follow-up to review feedback: shipping a single self-signed cert/key for all
nodes is wrong — libp2p mints its QUIC TLS cert at runtime from the node's
identity, and ethp2p must do the same.
- Remove the committed resources/ethp2p/{cert,key}.pem.
- Add EthLibp2p.generateAuxQuicCertPems() — mints a fresh self-signed cert bound
to the node's secp256k1 host identity via the SAME facility the primary libp2p
QUIC transport uses (libp2p_tls_cert.generate + a fresh ephemeral cert key).
Each call is unique per node and per process; nothing is shipped or shared.
- buildEthp2pConfig now, when it listens and no explicit cert env is set,
generates the cert at startup and writes the PEMs under the data dir
(ethp2p wants file paths — it has no in-memory PEM entry point). Explicit
ZEAM_ETHP2P_SERVER_CERT / _KEY still override. Cert/key paths are heap-owned
and freed by freeEthp2pConfig after beam_node.init.
The runtime cert can't be *literally* reused: the primary transport's cert is
generated in EthLibp2p.run(), which happens after beam_node.init sets up the
ethp2p listener — so at that point no cert exists yet. Generating a dedicated
per-node cert from the same host identity via the same facility gives the
identical security property (unique, runtime, never committed).
zig build -Dethp2p=true ✅ · default zig build ✅ · zig fmt --check ✅
8b7a904 to
ee7da05
Compare
|
Updated per review: dropped the committed shared |
The genesis peer list (nodes.yaml) always contains this node itself, so the derived ethp2p static_peers included our own listen port. connect() is synchronous and blocks start() in its handshake-poll loop; a self-dial never completes because the server-side accept only runs later in tick(). That stalled the dial loop before it reached the real peers, so no ethp2p peer ever connected. Skip any connect-peer whose libp2p QUIC port matches our own.
Local 2-node ethp2p smoke — runtime cert validatedRan a local 2-node smoke on Result: the runtime-generated per-node TLS cert completes a real, stable ethp2p QUIC handshake. Both nodes reached and held Fix included in this push (32dce7f)Found and fixed a self-dial bug: the genesis peer list ( Known limitation (not blocking; follow-up)Static-peer dials are synchronous and one-shot at |
|
I reviewed #1049 adversarially. I am not approving yet; I found one blocking security issue. Blocking finding:
Other checks looked fine:
Validation I ran:
Once the PEM private key is created with owner-only permissions, I expect this to be good to merge from my side. |
Review follow-up (zclawz on #1051): createFile's .permissions only applies when the file is CREATED. A key.pem that an already-merged-#1049 node wrote with the default 0o666 keeps that lax mode when this code rewrites it in place (truncate=true), so the fix only covered fresh installs. Explicitly setPermissions(0o600) on the key after open, while the file is still empty (before the key bytes are written) so the secret is never briefly present at a looser mode. The public cert passes null and keeps the umask'd create default — it must not be force-set to 0o666 (world-writable). Verified: a pre-existing key.pem at 0o666 is tightened to 0o600 in place after the node runs (cert stays untouched).
* ethp2p: write the runtime TLS private key owner-only (0o600) Per review (zclawz on #1049): the generated ethp2p private-key PEM was written with default file permissions (0o666 before umask), leaving it group/world-readable under common umasks. A copy of key.pem plus the signed cert is enough to impersonate this node's ethp2p QUIC listener for the cert lifetime, which defeats this PR's whole point (no shared/committed keypair). Thread a permissions arg through ethp2pWritePem and create the key with 0o600; the cert (public) keeps the default. 0o600 carries no group/other bits so the file is owner-only regardless of umask. Verified on disk: key.pem = -rw------- (600), cert.pem = -rw-r--r-- (644). * ethp2p: enforce 0o600 on the key even when key.pem already exists Review follow-up (zclawz on #1051): createFile's .permissions only applies when the file is CREATED. A key.pem that an already-merged-#1049 node wrote with the default 0o666 keeps that lax mode when this code rewrites it in place (truncate=true), so the fix only covered fresh installs. Explicitly setPermissions(0o600) on the key after open, while the file is still empty (before the key bytes are written) so the secret is never briefly present at a looser mode. The public cert passes null and keeps the umask'd create default — it must not be force-set to 0o666 (world-writable). Verified: a pre-existing key.pem at 0o666 is tightened to 0o600 in place after the node runs (cert stays untouched).
|
Thanks for the update @ch4r10t33r! Good to see the key permissions hardened to |
|
Thanks for the heads up, @ch4r10t33r! Glad the key-permission fix landed in #1051 — |
Continues #1045 (GitHub won't reopen a closed PR whose branch was force-pushed). Same branch, rebased onto current
main, scoped to only the pieces not already merged via #1046/#1047 — now with the TLS cert generated at runtime (per the review feedback below).The ethp2p adapter and env-config layer already landed on
main; this adds what makes it turnkey.What this adds (on top of
main)buildEthp2pConfigderives the ethp2p listen address + static peers from the node's own libp2p QUIC addresses, shifted byZEAM_ETHP2P_PORT_OFFSET(default +1, the ethlambda convention). SetZEAM_ETHP2P=1and it wires itself; explicit env still overrides.EthLibp2p.generateAuxQuicCertPems()mints a fresh self-signed cert bound to the node's secp256k1 host identity via the samelibp2p_tls_cert.generatefacility the primary QUIC transport uses. The PEMs are written under the node's data dir (the adapter consumes cert/key by path — it has no in-memory PEM entry point) and overridable viaZEAM_ETHP2P_SERVER_CERT/_KEY.Addressing the review
resources/ethp2p/{cert,key}.pem— removed. Certs are now unique per node and per process, exactly like libp2p mints its own.EthLibp2p.run(), which runs afterbeam_node.initsets up the ethp2p listener — so at that point no cert exists yet. Generating a dedicated per-node cert from the same host identity via the same facility gives the identical security property without reordering init.node_key; the cert is only the QUIC listener's TLS server identity.node.zighunk that predated/would revert the sync-wedge fix (sync: stop wedging on a false "peer does not support blocks_by_range" #1046). CI's-Dethp2p=truecompile step (from cli(ethp2p): unique per-node identity + operator-configurable peering + CI coverage #1047) covers it.Invocation
Doubly gated (
-Dethp2p=truebuild ANDZEAM_ETHP2P); default build/test unaffected.Validation
zig build -Dethp2p=true✅ · defaultzig build✅ ·zig fmt --check✅generateAuxQuicCertPemsis a direct reuse of the provenstartQuicTransportcert-gen path; memory ownership audited (listen/peers/cert/key paths freed afterinit;node_keyretained by the engine, outlives the process).