Skip to content
Open
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
95 changes: 95 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# run-morph-node

Morph L2 node operator toolkit. Provides Docker-based setup for running Morph nodes (standard and MPT), plus automated snapshot creation and distribution infrastructure.

## Project Structure

```
morph-node/ # Docker Compose setup for running a Morph node
Makefile # All node operations (run/stop/download-snapshot)
docker-compose.yml # Service definitions: geth + node/validator
.env # Mainnet config
.env_holesky # Holesky testnet config
.env_hoodi # Hoodi testnet config
.env_mpt # MPT-specific overrides (loaded on top of env)
entrypoint-geth.sh # Standard geth startup script
entrypoint-geth-mpt.sh # MPT geth startup script (--morph-mpt flag)

ops/snapshot/ # Snapshot automation scripts (runs on server via cron/pm2)
snapshot_make.py # Entry point: stop → snapshot → S3 upload → restart → update README
update_metadata.py # Fetches indexer API data, creates branch + PR via GitHub API
update_readme.py # In-memory README table insertion logic + Prometheus metrics
metrics_server.py # HTTP server exposing snapshot metrics on :6060/metrics
snapshot.env.example # Configuration reference

mainnet/hoodi/holesky/ # Chain genesis and config files (static, do not modify)
```

## Environments

| Environment | Makefile prefix | Snapshot CDN |
|-------------|-----------------|--------------|
| Mainnet | (no prefix) | snapshot.morphl2.io/mainnet |
| Hoodi testnet | `-hoodi` | snapshot.morphl2.io/hoodi |
| Holesky testnet (legacy) | `-holesky` | snapshot.morphl2.io/holesky |

MPT variants use an additional `--env-file .env_mpt` overlay.

## Common Operations

```bash
# Run a node
cd morph-node
make run-hoodi-node # hoodi standard
make run-mainnet-mpt-node # mainnet MPT

# Download snapshot
make download-and-decompress-hoodi-snapshot
make download-and-decompress-mainnet-mpt-snapshot

# Stop
make stop-node
make stop-validator
```

## Snapshot Automation

Runs on a server via pm2, triggered on configured days of the month.

**Full flow:**
1. Stop morph-node + morph-geth (pm2)
2. tar geth-data + node-data → upload to S3
3. Restart geth → wait for RPC → collect `base_height`
4. Restart morph-node
5. Query indexer API for `l1_msg_start_height` and `derivation_start_height`
6. Push updated README row + open PR via GitHub API (no git CLI needed)

**Configuration:** Copy `ops/snapshot/snapshot.env.example` to the server data directory, fill in `S3_BUCKET`, `GH_TOKEN`, `MORPH_HOME`, `GITHUB_REPOSITORY`.

**Dry run (safe, no writes):**
```bash
DRY_RUN=1 ENVIRONMENT=mainnet SNAPSHOT_NAME=test-1 BASE_HEIGHT=123 \
L1_MSG_HEIGHT=456 DERIV_HEIGHT=789 python3 ops/snapshot/update_metadata.py
```

## Code Conventions

- **Python**: stdlib only (no third-party deps), Python 3.9+. Use `urllib.request` for HTTP, not `requests`.
- **Shell scripts**: POSIX sh (`#!/bin/sh`), not bash. Use `set -e` for error handling.
- **Makefile**: Use `define`/`call` macros for repeated patterns. Always check for required tools before running.
- **Error handling**: Scripts must recover services if stopped (see `try/finally` pattern in `snapshot_make.py`).
- **Environment config**: Never hardcode paths or credentials. Always read from env vars with sensible defaults.

## Security

- **Never commit** `.env`, `snapshot.env`, or any file containing `GH_TOKEN`, `S3_BUCKET`, or AWS credentials.
- `GH_TOKEN` must be a Fine-grained PAT with only `Contents: Read/Write` and `Pull requests: Read/Write`.
- Snapshot automation opens PRs — it never merges directly to main.
- `jwt-secret.txt` is generated locally and never committed.

## Git Conventions

- Branch naming: `feat/`, `fix/`, `docs/` prefixes
- Snapshot automation branches: `snapshot/{environment}-{snapshot-name}`
- PRs require at least 1 approval before merging to main
- Commits are GPG-signed (configured in `~/.gitconfig`)
7 changes: 7 additions & 0 deletions morph-node/.env_mpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# MPT specific overrides (loaded after base env to override values)
GETH_ENTRYPOINT_FILE=./entrypoint-geth-mpt.sh
MPT_FORK_TIME=2000000000000

# MPT snapshot names
HOODI_MPT_SNAPSHOT_NAME=snapshot-20260211-1
MAINNET_MPT_SNAPSHOT_NAME=snapshot-20260211-1
19 changes: 19 additions & 0 deletions morph-node/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ JWT_SECRET_FILE_HOLESKY := $(JWT_SECRET_FILE)

JWT_SECRET_FILE_HOODI := $(JWT_SECRET_FILE)

include .env_mpt

generate-jwt:
@[ -f $(JWT_SECRET_FILE_MAINNET) ] || (echo "Generating $(JWT_SECRET_FILE_MAINNET)..." && openssl rand -hex 32 > $(JWT_SECRET_FILE_MAINNET) && echo "$(JWT_SECRET_FILE_MAINNET) created.")

Expand All @@ -31,6 +33,12 @@ run-holesky-node: generate-jwt-holesky
run-hoodi-node: generate-jwt-hoodi
docker-compose --env-file .env_hoodi up node &

run-hoodi-mpt-node: generate-jwt-hoodi
docker-compose --env-file .env_hoodi --env-file .env_mpt up node &

run-mainnet-mpt-node: generate-jwt
docker-compose --env-file .env --env-file .env_mpt up node &

stop-node:
docker stop morph-node morph-geth

Expand All @@ -47,6 +55,12 @@ run-holesky-validator: generate-jwt-holesky
run-hoodi-validator: generate-jwt-hoodi
docker-compose --env-file .env_hoodi up validator &

run-hoodi-mpt-validator: generate-jwt-hoodi
docker-compose --env-file .env_hoodi --env-file .env_mpt up validator &

run-mainnet-mpt-validator: generate-jwt
docker-compose --env-file .env --env-file .env_mpt up validator &

stop-validator:
docker stop validator-node morph-geth

Expand Down Expand Up @@ -93,6 +107,11 @@ download-and-decompress-hoodi-snapshot:
download-and-decompress-mainnet-snapshot:
$(call download-and-decompress,$(MAINNET_SNAPSHOT_NAME),https://snapshot.morphl2.io/mainnet)

download-and-decompress-hoodi-mpt-snapshot:
$(call download-and-decompress,$(HOODI_MPT_SNAPSHOT_NAME),https://snapshot.morphl2.io/hoodi)

download-and-decompress-mainnet-mpt-snapshot:
$(call download-and-decompress,$(MAINNET_MPT_SNAPSHOT_NAME),https://snapshot.morphl2.io/mainnet)



6 changes: 3 additions & 3 deletions morph-node/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: '3.8'
services:
geth:
container_name: morph-geth
image: ghcr.io/morph-l2/go-ethereum:2.1.1
image: ghcr.io/morph-l2/go-ethereum:2.1.2
restart: unless-stopped
ports:
- "8545:8545"
Expand All @@ -26,7 +26,7 @@ services:
depends_on:
geth:
condition: service_started
image: ghcr.io/morph-l2/node:0.4.10
image: ghcr.io/morph-l2/node:0.4.11
restart: unless-stopped
ports:
- "26656"
Expand All @@ -53,7 +53,7 @@ services:
depends_on:
geth:
condition: service_started
image: ghcr.io/morph-l2/node:0.4.10
image: ghcr.io/morph-l2/node:0.4.11
ports:
- "26660"
environment:
Expand Down
35 changes: 35 additions & 0 deletions morph-node/entrypoint-geth-mpt.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/sh

if [ ! -f /jwt-secret.txt ]; then
echo "Error: jwt-secret.txt not found. Please create it before starting the service."
exit 1
fi

MORPH_FLAG=${MORPH_FLAG:-"morph"}

COMMAND="geth \
--$MORPH_FLAG \
--morph-mpt
--datadir="./db" \
--verbosity=3 \
--http \
--http.corsdomain="*" \
--http.vhosts="*" \
--http.addr=0.0.0.0 \
--http.port=8545 \
--http.api=web3,debug,eth,txpool,net,morph,engine,admin \
--ws \
--ws.addr=0.0.0.0 \
--ws.port=8546 \
--ws.origins="*" \
--ws.api=web3,debug,eth,txpool,net,morph,engine,admin \
--authrpc.addr=0.0.0.0 \
--authrpc.port=8551 \
--authrpc.vhosts="*" \
--authrpc.jwtsecret="./jwt-secret.txt" \
--gcmode=archive \
--log.filename=./db/geth.log \
--metrics \
--metrics.addr=0.0.0.0"

eval $COMMAND
Loading