Skip to content

fix(devnet): peer the execution and consensus nodes, rename ha-geth to ha-el - #1053

Merged
panos-xyz merged 1 commit into
mainfrom
fix/devnet-cluster-el-interconnect
Sep 2, 2026
Merged

fix(devnet): peer the execution and consensus nodes, rename ha-geth to ha-el#1053
panos-xyz merged 1 commit into
mainfrom
fix/devnet-cluster-el-interconnect

Conversation

@panos-xyz

@panos-xyz panos-xyz commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Problem

The devnet execution-layer nodes were not actually peered with each other, and
the cluster could not reach a usable state at all.

Three separate causes, all of which fail silently:

  1. reth had no peers. docker-compose-reth.yml set --disable-discovery
    but never supplied --trusted-peers, and reth does not read geth's
    static-nodes.json. net_peerCount was 0x0 on every reth node. It also
    mounted no nodekey, so reth minted a random identity per datadir and no peer
    list could have been written in advance.
  2. The HA execution nodes never peered with each other, under either
    client. They mount static-nodes.json, which lists only morph-el-0 and
    morph-el-1, so the three of them only ever reached the two main nodes.
  3. The HA consensus nodes never peered with each other. setup_nodes.py
    overwrote the persistent_peers generated by
    tendermint testnet --populate-persistent-peers with a hardcoded list of
    node-0, node-1, node-2. Of those, node-1 runs with
    MORPH_NODE_DERIVATION_VERIFY_MODE=layer1 and never starts tendermint, and
    node-2 has no compose service, so each ha-node-* had exactly one
    reachable peer and none of each other. The sequencer hand-over waits for the
    block pool to report caught up, which never happens, so
    make devnet-up-cluster* stalled at height 0 — the exact failure mode
    already documented in ops/README.md.

The hardcoded IDs were also unfixable by hand: ops/docker/ has no
ha-node0/1/2 directories, so those nodes keep the random key tendermint
generates and their IDs differ on every setup.

Changes

Execution-layer peering. docker-compose-reth.yml now points every node at
the same nodekey* / ha-nodekey* file geth uses, via --p2p-secret-key, and
passes explicit --trusted-peers. Both clients therefore derive the same enode
for a given node. A new static-nodes-cluster.json gives the geth cluster path
the peers it was missing. Peering mirrors the existing geth layout:
morph-el-0/morph-el-1 know each other, and the HA nodes dial both plus each
other — which keeps ha-el-* names out of the non-cluster setup, where they do
not resolve.

ha-nodekey0/1/2 and nodekey2 had a trailing newline, which reth rejects with
malformed or out-of-range secret key (geth accepts it either way). Stripped,
so one file works for both clients.

Consensus-layer peering. setup_nodes.py now derives each node ID from the
node_key.json that actually ends up installed, and lists only the nodes that
run tendermint (node-0 and the three ha-node-*). Copying the key files moved
ahead of building the peer list, because overwriting a node_key.json changes
the node's identity. Tendermint RPC also binds 0.0.0.0 so the published ports
in the README endpoint table are reachable from the host.

Compose layering order. docker-compose-cluster.yml is now passed before
docker-compose-reth.yml. Later -f files win, so with the previous order the
cluster file's image: morph-geth:latest and geth entrypoint overrode the reth
override — make devnet-up-cluster-reth silently ran geth for ha-el-*.

Rename. ha-geth-0/1/2ha-el-0/1/2, since either client can back these
services. The .devnet/ha-el* data directories were already named this way.

Verification

make devnet-clean-build && make devnet-up-cluster-reth, then:

  • net_peerCount on all five execution nodes
  • /net_info on node-0 and the three ha-node-*
  • block height advancing

docker compose config was also used to confirm the reth override wins for
ha-el-* and that the geth cluster path is unchanged.

python3 -m pytest ops/devnet-morph/tests/test_devnet_config.py covers the
layering order, the rename, the cluster static-nodes file, the reth key/peer
wiring, the absence of trailing newlines in key files, and that node IDs are
derived after the keys are installed.

Summary by CodeRabbit

  • New Features

    • Added high-availability execution-layer services with Reth support.
    • Added static peering for execution-layer nodes and dynamic peer configuration for consensus-layer nodes.
    • Expanded devnet endpoint documentation and peering guidance.
    • Enabled RPC access across devnet containers.
  • Bug Fixes

    • Corrected compose-file precedence so Reth settings apply reliably in clustered deployments.
    • Updated service references and networking configuration for renamed execution-layer nodes.

…o ha-el

The devnet execution-layer nodes were not peered with each other, and the HA
cluster could not reach a usable state at all. Three independent causes, each
failing silently:

reth had no peers. docker-compose-reth.yml set --disable-discovery but never
supplied --trusted-peers, and reth does not read geth's static-nodes.json, so
net_peerCount was 0x0 on every reth node. It also mounted no nodekey, so reth
minted a random identity per datadir and no peer list could be written ahead of
time.

The HA execution nodes never peered with each other under either client: they
mount static-nodes.json, which lists only morph-el-0 and morph-el-1.

The HA consensus nodes never peered with each other either. setup_nodes.py
overwrote the persistent_peers generated by --populate-persistent-peers with a
hardcoded node-0/node-1/node-2 list. node-1 runs with
MORPH_NODE_DERIVATION_VERIFY_MODE=layer1 and never starts tendermint, and
node-2 has no compose service, so each ha-node-* had exactly one reachable peer
and none of each other. The sequencer hand-over waits for the block pool to
report caught up, which never happened, so make devnet-up-cluster* stalled at
height 0 - the failure mode already described in ops/README.md. Hardcoding the
IDs could not have worked anyway: ops/docker/ has no ha-node0/1/2 directories,
so those nodes keep the random key tendermint generates.

Changes:

- docker-compose-reth.yml points every node at the same nodekey*/ha-nodekey*
  file geth uses, via --p2p-secret-key, and passes explicit --trusted-peers, so
  both clients derive the same enode for a node.
- static-nodes-cluster.json gives the geth cluster path the peers it lacked.
- ha-nodekey0/1/2 and nodekey2 had a trailing newline, which reth rejects with
  "malformed or out-of-range secret key" while geth tolerates it. Stripped, so
  one file serves both clients.
- setup_nodes.py derives each node ID from the node_key.json that actually ends
  up installed, and lists only the nodes that run tendermint. Copying the keys
  moved ahead of building the peer list, since overwriting a node_key.json
  changes the node's identity. Tendermint RPC now binds 0.0.0.0 so the ports in
  the README endpoint table are reachable from the host.
- docker-compose-cluster.yml is layered before docker-compose-reth.yml. Later
  -f files win, so the previous order let the cluster file's geth image and
  entrypoint override the reth override, and devnet-up-cluster-reth silently ran
  geth for ha-el-*.
- ha-geth-0/1/2 renamed to ha-el-0/1/2, since either client can back these
  services. The .devnet/ha-el* data directories already used that name.

Verified with make devnet-clean-build && make devnet-up-cluster-reth: all five
execution nodes report 4 peers and identical height, node-0 and the three
ha-node-* report 3 peers each, the HA leader produces blocks, and the reth
enodes match the values the geth static-nodes files have always used.
@panos-xyz
panos-xyz requested a review from a team as a code owner August 31, 2026 07:37
@panos-xyz
panos-xyz requested review from dylanCai9 and removed request for a team August 31, 2026 07:37
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 34fb829e-fe97-4c28-af4b-ab82dc95e87a

📥 Commits

Reviewing files that changed from the base of the PR and between 5edbc69 and aefe28a.

📒 Files selected for processing (12)
  • Makefile
  • ops/README.md
  • ops/devnet-morph/devnet/__init__.py
  • ops/devnet-morph/devnet/setup_nodes.py
  • ops/devnet-morph/tests/test_devnet_config.py
  • ops/docker/docker-compose-cluster.yml
  • ops/docker/docker-compose-reth.yml
  • ops/docker/ha-nodekey0
  • ops/docker/ha-nodekey1
  • ops/docker/ha-nodekey2
  • ops/docker/nodekey2
  • ops/docker/static-nodes-cluster.json

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Devnet peering configuration

Layer / File(s) Summary
Compose overlay ordering and service renaming
Makefile, ops/devnet-morph/devnet/__init__.py, ops/docker/docker-compose-cluster.yml, ops/devnet-morph/tests/test_devnet_config.py, ops/README.md
The cluster overlay now precedes the Reth override. HA execution-layer services and RPC references use ha-el-* names.
Deterministic execution-layer peering
ops/docker/docker-compose-reth.yml, ops/docker/static-nodes-cluster.json, ops/docker/nodekey2, ops/docker/ha-nodekey*, ops/devnet-morph/tests/test_devnet_config.py, ops/README.md
Reth uses mounted node keys, disabled discovery, and --trusted-peers. Cluster geth services use the new static-node configuration.
Dynamic Tendermint peer generation
ops/devnet-morph/devnet/setup_nodes.py, ops/devnet-morph/tests/test_devnet_config.py, ops/README.md
Node keys are copied before peer IDs are derived. Tendermint persistent peers are generated from installed keys, and RPC binds to 0.0.0.0:26657.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: 🔵 Low · up to aefe2

The PR fixes devnet node peering and cluster startup, but it also exposes four Tendermint RPC endpoints through host-published ports without established access restrictions and can leave partial configuration after an interrupted setup. It is mergeable with explicit owner awareness and follow-up to constrain RPC access and make setup retries safely regenerate incomplete state.

Sequence Diagram(s)

sequenceDiagram
  participant compose_file_args
  participant docker-compose-cluster.yml
  participant docker-compose-reth.yml
  participant Reth services
  compose_file_args->>docker-compose-cluster.yml: add cluster overlay
  compose_file_args->>docker-compose-reth.yml: add Reth override last
  docker-compose-reth.yml->>Reth services: apply node keys and trusted peers
Loading
sequenceDiagram
  participant setup_nodes.py
  participant node_key.json
  participant Tendermint nodes
  participant RPC clients
  setup_nodes.py->>node_key.json: copy installed keys
  setup_nodes.py->>node_key.json: derive peer IDs
  setup_nodes.py->>Tendermint nodes: write persistent_peers
  Tendermint nodes->>RPC clients: serve RPC on 0.0.0.0:26657
Loading

Suggested reviewers: dylancai9, twcctop, tomatoishealthy, segueii

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 3 files. (9 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: execution and consensus node peering and the rename from ha-geth to ha-el.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 3 files. (9 skipped: 9 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/devnet-cluster-el-interconnect

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@panos-xyz
panos-xyz merged commit 1d0a4af into main Sep 2, 2026
7 checks passed
@panos-xyz
panos-xyz deleted the fix/devnet-cluster-el-interconnect branch September 2, 2026 09:20
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.

2 participants