Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
prover/
.git/
.claude/
contracts/node_modules/
gas-oracle/app/target/
node/build/
Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@ export MORPH_RETH_RUSTFLAGS
export MORPH_RETH_DOCKER_TARGET
export MORPH_RETH_ENTRYPOINT
DEVNET_COMPOSE_FILES := -f docker-compose-devnet.yml
DEVNET_CLEAN_COMPOSE_FILES := -f docker-compose-devnet.yml -f docker-compose-cluster.yml -f docker-compose-reth.yml
DEVNET_CLEAN_COMPOSE_FILES := -f docker-compose-devnet.yml -f docker-compose-cluster.yml -f docker-compose-reth.yml -f docker-compose-cluster-reth.yml

# The cluster topology is layered before the execution-client override, so that
# docker-compose-reth.yml has the last word on the ha-el-* image, entrypoint and
# the reth files have the last word on the ha-el-* image, entrypoint and
# command. Swapping these two leaves the cluster nodes running geth even when
# reth was asked for, because later -f files win.
ifneq ($(DEVNET_CLUSTER_ENABLED),)
Expand All @@ -175,6 +175,11 @@ ifeq ($(EXECUTION_CLIENT),geth)
DEVNET_EXECUTION_DEPS := submodules
else ifeq ($(EXECUTION_CLIENT),reth)
DEVNET_COMPOSE_FILES += -f docker-compose-reth.yml
# The ha-el-* reth overrides are kept out of docker-compose-reth.yml because
# compose starts any service a later -f file introduces, cluster or not.
ifneq ($(DEVNET_CLUSTER_ENABLED),)
DEVNET_COMPOSE_FILES += -f docker-compose-cluster-reth.yml
endif
ifeq ($(MORPH_RETH_BUILD_FROM_SOURCE),true)
DEVNET_EXECUTION_DEPS := reth
else
Expand Down
15 changes: 11 additions & 4 deletions ops/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,17 @@ looking anywhere else.
`ha-node` admin API: `9501` / `9601` / `9701`.

The execution-layer services are named `el` rather than `geth` because either
client can back them: `docker-compose-cluster.yml` defines them as geth, and
`docker-compose-reth.yml` overrides them to reth. That override only works
because the cluster file is layered *before* the reth file — later `-f` files
win, so the reverse order silently leaves the cluster on geth.
client can back them: `docker-compose-cluster.yml` defines `ha-el-*` as geth,
and `docker-compose-cluster-reth.yml` overrides them to reth. That override only
works because the cluster file is layered *before* the reth files — later `-f`
files win, so the reverse order silently leaves the cluster on geth.

The `ha-el-*` reth overrides need their own file rather than a section of
`docker-compose-reth.yml`: compose starts every service a later `-f` file
introduces, whether or not an earlier file declared it, so overrides living in
the shared reth file also came up in the non-cluster devnet — without the
`/genesis.json` and `/jwt-secret.txt` mounts that only the cluster file
provides, which made them exit with `Invalid value '/genesis.json' for --chain`.

## Execution-layer peering

Expand Down
12 changes: 9 additions & 3 deletions ops/devnet-morph/devnet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,19 @@ def compose_file_args(execution_client, cluster=False):
"""Return docker-compose -f flags for the chosen L2 execution client."""
args = ['-f', 'docker-compose-devnet.yml']
# The cluster topology comes before the execution-client override so that
# docker-compose-reth.yml gets the last word on the ha-el-* image,
# entrypoint and command. Later -f files win, so reversing these two leaves
# the cluster nodes on geth even when reth was requested.
# the reth files get the last word on the ha-el-* image, entrypoint and
# command. Later -f files win, so reversing these two leaves the cluster
# nodes on geth even when reth was requested.
if cluster:
args.extend(['-f', 'docker-compose-cluster.yml'])
if execution_client == 'reth':
args.extend(['-f', 'docker-compose-reth.yml'])
# The ha-el-* reth overrides sit in their own file: compose starts any
# service a later -f file introduces, so parking them in
# docker-compose-reth.yml started them in the non-cluster devnet too,
# without the mounts that only the cluster file supplies.
if cluster:
args.extend(['-f', 'docker-compose-cluster-reth.yml'])
return args


Expand Down
49 changes: 46 additions & 3 deletions ops/devnet-morph/tests/test_devnet_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ def test_root_dockerignore_excludes_generated_build_outputs(self):
"gas-oracle/app/target/",
"node/build/",
"ops/docker/.devnet/",
# Claude Code keeps its worktrees here; on a machine that has used
# one, this is the single largest directory in the repo and every
# image build would ship it as build context.
".claude/",
):
self.assertIn(generated_path, dockerignore)

Expand All @@ -43,7 +47,9 @@ def test_devnet_clean_removes_compose_project_volumes(self):
makefile = (REPO_ROOT / "Makefile").read_text()

self.assertIn(
"DEVNET_CLEAN_COMPOSE_FILES := -f docker-compose-devnet.yml -f docker-compose-cluster.yml -f docker-compose-reth.yml",
"DEVNET_CLEAN_COMPOSE_FILES := -f docker-compose-devnet.yml "
"-f docker-compose-cluster.yml -f docker-compose-reth.yml "
"-f docker-compose-cluster-reth.yml",
makefile,
)
self.assertIn("docker compose $(DEVNET_CLEAN_COMPOSE_FILES) down --volumes --remove-orphans", makefile)
Expand Down Expand Up @@ -152,14 +158,47 @@ def test_reth_compose_configures_deterministic_peering(self):
for service, key in (
("morph-el-0", "nodekey0"),
("morph-el-1", "nodekey1"),
):
self.assertIn(f"{service}:", compose)
self.assertIn(f'"${{PWD}}/{key}:/p2p-secret.key"', compose)
self.assertEqual(compose.count("--p2p-secret-key=/p2p-secret.key"), 2)
self.assertEqual(compose.count("--trusted-peers="), 2)

def test_reth_compose_leaves_cluster_services_to_the_cluster_reth_file(self):
"""compose starts every service a later -f file introduces, even one no
earlier file declared. ha-el-* overrides parked in the shared reth file
therefore came up in the non-cluster devnet as well, missing the
/genesis.json and /jwt-secret.txt mounts that only
docker-compose-cluster.yml supplies, and exited with
"Invalid value '/genesis.json' for --chain"."""
compose = (DOCKER_DIR / "docker-compose-reth.yml").read_text()

for service in ("ha-el-0", "ha-el-1", "ha-el-2"):
self.assertNotIn(f"{service}:", compose)
for key in ("ha-nodekey0", "ha-nodekey1", "ha-nodekey2"):
self.assertNotIn(key, compose)

def test_cluster_reth_compose_overrides_the_ha_execution_clients(self):
cluster_reth = DOCKER_DIR / "docker-compose-cluster-reth.yml"

self.assertTrue(cluster_reth.exists())
compose = cluster_reth.read_text()

self.assertIn("${MORPH_RETH_IMAGE:-ghcr.io/morph-l2/morph-reth:latest}", compose)
self.assertIn("${MORPH_RETH_ENTRYPOINT:-/usr/local/bin/morph-reth}", compose)
for service, key in (
("ha-el-0", "ha-nodekey0"),
("ha-el-1", "ha-nodekey1"),
("ha-el-2", "ha-nodekey2"),
):
self.assertIn(f"{service}:", compose)
self.assertIn(f'"${{PWD}}/{key}:/p2p-secret.key"', compose)
self.assertEqual(compose.count("--p2p-secret-key=/p2p-secret.key"), 5)
self.assertEqual(compose.count("--trusted-peers="), 5)
self.assertEqual(compose.count("--p2p-secret-key=/p2p-secret.key"), 3)
self.assertEqual(compose.count("--trusted-peers="), 3)
# Nothing here may redefine the plain devnet's execution clients, or
# they would be reconfigured only in cluster mode.
for service in ("morph-el-0:", "morph-el-1:"):
self.assertNotIn(f" {service}", compose)

def test_execution_client_keys_have_no_trailing_newline(self):
"""reth rejects a key file with a trailing newline ("malformed or
Expand Down Expand Up @@ -209,6 +248,8 @@ def test_compose_file_args_can_enable_cluster_mode(self):
sys.path.remove(str(DEVNET_PACKAGE))

self.assertEqual(devnet.compose_file_args("geth"), ["-f", "docker-compose-devnet.yml"])
# The ha-el-* reth overrides must not reach the non-cluster devnet:
# compose would start those services without the cluster's mounts.
self.assertEqual(
devnet.compose_file_args("reth"),
["-f", "docker-compose-devnet.yml", "-f", "docker-compose-reth.yml"],
Expand All @@ -229,6 +270,8 @@ def test_compose_file_args_can_enable_cluster_mode(self):
"docker-compose-cluster.yml",
"-f",
"docker-compose-reth.yml",
"-f",
"docker-compose-cluster-reth.yml",
],
)

Expand Down
97 changes: 97 additions & 0 deletions ops/docker/docker-compose-cluster-reth.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# reth overrides for the HA cluster's execution clients, layered on top of
# docker-compose-cluster.yml, which defines ha-el-* as geth.
#
# These live in their own file rather than in docker-compose-reth.yml because
# compose creates any service a later -f file introduces, whether or not an
# earlier file declared it. Keeping the ha-el-* overrides in the reth file made
# them appear in the non-cluster devnet too, where they started without the
# /genesis.json and /jwt-secret.txt mounts that only the cluster file provides
# and died with "Invalid value '/genesis.json' for --chain". So this file is
# passed only when both --cluster and reth are asked for.
#
# See docker-compose-reth.yml for why discovery is off, why every node is handed
# a fixed --p2p-secret-key, and how the enodes below were derived. Peering here
# mirrors static-nodes-cluster.json: each ha-el-* dials the other two plus both
# morph-el-*.
#
# The command list is spelled out per service because --trusted-peers takes one
# comma separated value and YAML cannot concatenate an anchor with extra items.

x-reth-service: &reth-service
image: ${MORPH_RETH_IMAGE:-ghcr.io/morph-l2/morph-reth:latest}
user: "0:0"
entrypoint:
- ${MORPH_RETH_ENTRYPOINT:-/usr/local/bin/morph-reth}

services:
ha-el-0:
<<: *reth-service
command:
- node
- --chain=/genesis.json
- --datadir=/db
- --http
- --http.addr=0.0.0.0
- --http.port=8545
- --http.api=web3,debug,eth,txpool,net,trace,admin,reth
- --ws
- --ws.addr=0.0.0.0
- --ws.port=8546
- --ws.api=web3,debug,eth,txpool,net,trace,admin,reth
- --authrpc.addr=0.0.0.0
- --authrpc.port=8551
- --authrpc.jwtsecret=/jwt-secret.txt
- --nat=none
- --disable-discovery
- --p2p-secret-key=/p2p-secret.key
- --trusted-peers=enode://73edbabab8727eba67dd5c2964c610decf28ec73e72d5bc395cc5be3c61ebd76cf72ada700b10926c3d2f9e86d64fafb92c6d02deb48c16439040b7c37f86c2d@ha-el-1:30303,enode://4a1a481e53b57ea351a08d945443cdff9cb7911e9121e8240efa99e4214582c0e383a30706349ce1be88b81d0f6344c401823a6624569e20d26880774279c41c@ha-el-2:30303,enode://58e698ea2dd8a76e0cb185d13c1faabf223b60c89fef988c8b89496571056d6c2922109537bb291cd87f2ec09a23ac37d59bde2c7a4885d07b7b641cadff2921@morph-el-0:30303,enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303
volumes:
- "${PWD}/ha-nodekey0:/p2p-secret.key"

ha-el-1:
<<: *reth-service
command:
- node
- --chain=/genesis.json
- --datadir=/db
- --http
- --http.addr=0.0.0.0
- --http.port=8545
- --http.api=web3,debug,eth,txpool,net,trace,admin,reth
- --ws
- --ws.addr=0.0.0.0
- --ws.port=8546
- --ws.api=web3,debug,eth,txpool,net,trace,admin,reth
- --authrpc.addr=0.0.0.0
- --authrpc.port=8551
- --authrpc.jwtsecret=/jwt-secret.txt
- --nat=none
- --disable-discovery
- --p2p-secret-key=/p2p-secret.key
- --trusted-peers=enode://343bbe0507fd72946e8d57d5b42630dc905aed7e512945eb2dfd99a0d27bb1c7e84ba621c3408242d713a972a581374ea83a4d134651c373c4f2e8425ba99857@ha-el-0:30303,enode://4a1a481e53b57ea351a08d945443cdff9cb7911e9121e8240efa99e4214582c0e383a30706349ce1be88b81d0f6344c401823a6624569e20d26880774279c41c@ha-el-2:30303,enode://58e698ea2dd8a76e0cb185d13c1faabf223b60c89fef988c8b89496571056d6c2922109537bb291cd87f2ec09a23ac37d59bde2c7a4885d07b7b641cadff2921@morph-el-0:30303,enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303
volumes:
- "${PWD}/ha-nodekey1:/p2p-secret.key"

ha-el-2:
<<: *reth-service
command:
- node
- --chain=/genesis.json
- --datadir=/db
- --http
- --http.addr=0.0.0.0
- --http.port=8545
- --http.api=web3,debug,eth,txpool,net,trace,admin,reth
- --ws
- --ws.addr=0.0.0.0
- --ws.port=8546
- --ws.api=web3,debug,eth,txpool,net,trace,admin,reth
- --authrpc.addr=0.0.0.0
- --authrpc.port=8551
- --authrpc.jwtsecret=/jwt-secret.txt
- --nat=none
- --disable-discovery
- --p2p-secret-key=/p2p-secret.key
- --trusted-peers=enode://343bbe0507fd72946e8d57d5b42630dc905aed7e512945eb2dfd99a0d27bb1c7e84ba621c3408242d713a972a581374ea83a4d134651c373c4f2e8425ba99857@ha-el-0:30303,enode://73edbabab8727eba67dd5c2964c610decf28ec73e72d5bc395cc5be3c61ebd76cf72ada700b10926c3d2f9e86d64fafb92c6d02deb48c16439040b7c37f86c2d@ha-el-1:30303,enode://58e698ea2dd8a76e0cb185d13c1faabf223b60c89fef988c8b89496571056d6c2922109537bb291cd87f2ec09a23ac37d59bde2c7a4885d07b7b641cadff2921@morph-el-0:30303,enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303
volumes:
- "${PWD}/ha-nodekey2:/p2p-secret.key"
81 changes: 4 additions & 77 deletions ops/docker/docker-compose-reth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
# keys below are derived from those same files.
#
# Peering mirrors the geth static-nodes layout: morph-el-0 and morph-el-1 know
# only each other, while the HA nodes dial both of them and each other. That
# keeps ha-el-* out of the non-cluster setup, where those names do not resolve.
# only each other. The ha-el-* overrides live in docker-compose-cluster-reth.yml
# instead of here, because compose creates every service a later -f file names,
# so overrides parked in this file would also start in the non-cluster devnet,
# where the cluster's mounts are absent and those hostnames do not resolve.
#
# The command list is spelled out per service because --trusted-peers takes one
# comma separated value and YAML cannot concatenate an anchor with extra items.
Expand Down Expand Up @@ -70,78 +72,3 @@ services:
- --trusted-peers=enode://58e698ea2dd8a76e0cb185d13c1faabf223b60c89fef988c8b89496571056d6c2922109537bb291cd87f2ec09a23ac37d59bde2c7a4885d07b7b641cadff2921@morph-el-0:30303
volumes:
- "${PWD}/nodekey1:/p2p-secret.key"

# The ha-el-* overrides below only take effect when
# docker-compose-cluster.yml is passed as well; otherwise these services do
# not exist and compose ignores them.
ha-el-0:
<<: *reth-service
command:
- node
- --chain=/genesis.json
- --datadir=/db
- --http
- --http.addr=0.0.0.0
- --http.port=8545
- --http.api=web3,debug,eth,txpool,net,trace,admin,reth
- --ws
- --ws.addr=0.0.0.0
- --ws.port=8546
- --ws.api=web3,debug,eth,txpool,net,trace,admin,reth
- --authrpc.addr=0.0.0.0
- --authrpc.port=8551
- --authrpc.jwtsecret=/jwt-secret.txt
- --nat=none
- --disable-discovery
- --p2p-secret-key=/p2p-secret.key
- --trusted-peers=enode://73edbabab8727eba67dd5c2964c610decf28ec73e72d5bc395cc5be3c61ebd76cf72ada700b10926c3d2f9e86d64fafb92c6d02deb48c16439040b7c37f86c2d@ha-el-1:30303,enode://4a1a481e53b57ea351a08d945443cdff9cb7911e9121e8240efa99e4214582c0e383a30706349ce1be88b81d0f6344c401823a6624569e20d26880774279c41c@ha-el-2:30303,enode://58e698ea2dd8a76e0cb185d13c1faabf223b60c89fef988c8b89496571056d6c2922109537bb291cd87f2ec09a23ac37d59bde2c7a4885d07b7b641cadff2921@morph-el-0:30303,enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303
volumes:
- "${PWD}/ha-nodekey0:/p2p-secret.key"

ha-el-1:
<<: *reth-service
command:
- node
- --chain=/genesis.json
- --datadir=/db
- --http
- --http.addr=0.0.0.0
- --http.port=8545
- --http.api=web3,debug,eth,txpool,net,trace,admin,reth
- --ws
- --ws.addr=0.0.0.0
- --ws.port=8546
- --ws.api=web3,debug,eth,txpool,net,trace,admin,reth
- --authrpc.addr=0.0.0.0
- --authrpc.port=8551
- --authrpc.jwtsecret=/jwt-secret.txt
- --nat=none
- --disable-discovery
- --p2p-secret-key=/p2p-secret.key
- --trusted-peers=enode://343bbe0507fd72946e8d57d5b42630dc905aed7e512945eb2dfd99a0d27bb1c7e84ba621c3408242d713a972a581374ea83a4d134651c373c4f2e8425ba99857@ha-el-0:30303,enode://4a1a481e53b57ea351a08d945443cdff9cb7911e9121e8240efa99e4214582c0e383a30706349ce1be88b81d0f6344c401823a6624569e20d26880774279c41c@ha-el-2:30303,enode://58e698ea2dd8a76e0cb185d13c1faabf223b60c89fef988c8b89496571056d6c2922109537bb291cd87f2ec09a23ac37d59bde2c7a4885d07b7b641cadff2921@morph-el-0:30303,enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303
volumes:
- "${PWD}/ha-nodekey1:/p2p-secret.key"

ha-el-2:
<<: *reth-service
command:
- node
- --chain=/genesis.json
- --datadir=/db
- --http
- --http.addr=0.0.0.0
- --http.port=8545
- --http.api=web3,debug,eth,txpool,net,trace,admin,reth
- --ws
- --ws.addr=0.0.0.0
- --ws.port=8546
- --ws.api=web3,debug,eth,txpool,net,trace,admin,reth
- --authrpc.addr=0.0.0.0
- --authrpc.port=8551
- --authrpc.jwtsecret=/jwt-secret.txt
- --nat=none
- --disable-discovery
- --p2p-secret-key=/p2p-secret.key
- --trusted-peers=enode://343bbe0507fd72946e8d57d5b42630dc905aed7e512945eb2dfd99a0d27bb1c7e84ba621c3408242d713a972a581374ea83a4d134651c373c4f2e8425ba99857@ha-el-0:30303,enode://73edbabab8727eba67dd5c2964c610decf28ec73e72d5bc395cc5be3c61ebd76cf72ada700b10926c3d2f9e86d64fafb92c6d02deb48c16439040b7c37f86c2d@ha-el-1:30303,enode://58e698ea2dd8a76e0cb185d13c1faabf223b60c89fef988c8b89496571056d6c2922109537bb291cd87f2ec09a23ac37d59bde2c7a4885d07b7b641cadff2921@morph-el-0:30303,enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303
volumes:
- "${PWD}/ha-nodekey2:/p2p-secret.key"