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
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
151 changes: 151 additions & 0 deletions ops/snapshot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Snapshot Automation

> 中文版请见 [README.zh.md](./README.zh.md)

Automatically creates a node snapshot every two weeks and syncs the relevant parameters to README.md for users to download.

## Background

Manually creating snapshots is error-prone and tedious. This solution automates the entire process using a server-side cron job and the GitHub REST API — no GitHub Actions or git CLI required.

## Directory Structure

```
run-morph-node/
├── README.md # snapshot table is updated here
└── ops/snapshot/
├── README.md # this document
├── README.zh.md # Chinese version
├── snapshot_make.py # entry point: stop → snapshot → upload → restart → update README
├── update_metadata.py # fetches indexer API data and orchestrates the full update flow
├── update_readme.py # pure table-update logic (imported by update_metadata.py)
└── metrics_server.py # persistent HTTP server exposing metrics on :6060/metrics
```

## Workflow

```
Server cron job (1st and 15th of each month)
ops/snapshot/snapshot_make.py
[1] stop morph-node, morph-geth
[2] create snapshot (tar geth + node data)
[3] upload to S3
[4] restart morph-geth → wait for RPC → collect base_height
[5] restart morph-node
[6] call update_metadata.py
│ BASE_HEIGHT, SNAPSHOT_NAME
python3 update_metadata.py
┌─────────────────────────────────────────────────────┐
│ 1. call internal explorer-indexer API: │
│ GET /v1/batch/l1_msg_start_height/<base_height> │
│ GET /v1/batch/derivation_start_height/<base_height>│
│ 2. fetch README.md content via GitHub API │
│ 3. insert new snapshot row at top of table │
│ 4. create branch + push updated file via GitHub API │
│ 5. open PR via GitHub API │
└─────────────────────────────────────────────────────┘
```

## Triggers

| Method | Description |
|---|---|
| Scheduled | Server cron job on the 1st and 15th of each month |
| Manual | SSH into the server and run `snapshot_make.py` directly |

## Multi-environment Support

| Environment | Indexer API (internal) |
|---|---|
| mainnet | `explorer-indexer.morphl2.io` |
| hoodi | `explorer-indexer-hoodi.morphl2.io` |
| holesky | `explorer-indexer-holesky.morphl2.io` |

Each environment has its own node server with its own cron job. S3 paths and README table sections are automatically scoped by environment.

## Deployment

### 1. Clone the Repository on the Node Server

```bash
git clone https://github.com/morphl2/run-morph-node.git /data/run-morph-node
```

### 2. Create the Environment File

Copy the template into the same directory and fill in the values:

```bash
cd /data/run-morph-node/ops/snapshot
cp snapshot.env.example snapshot.env
# edit snapshot.env and fill in GH_TOKEN, S3_BUCKET, ENVIRONMENT, etc.
```

For multiple environments or snapshot types, use separate files:

```bash
cp snapshot.env.example snapshot-hoodi.env
cp snapshot.env.example snapshot-mainnet-mpt.env
```

All available variables are documented in [`snapshot.env.example`](./snapshot.env.example). These files must **not** be committed to git (add `*.env` to `.gitignore`).

Also recommended: enable **"Automatically delete head branches"** under repo Settings → General. Branches will be deleted automatically after a PR is merged.

### 3. Configure the Cron Job

Add one entry per environment / snapshot type:

```bash
crontab -e
```

```cron
REPO=/data/run-morph-node/ops/snapshot

# mainnet standard snapshot (uses default snapshot.env)
0 2 1,15 * * python3 $REPO/snapshot_make.py >> /var/log/snapshot-mainnet.log 2>&1

# mainnet mpt-snapshot
0 3 1,15 * * ENV_FILE=$REPO/snapshot-mainnet-mpt.env python3 $REPO/snapshot_make.py >> /var/log/snapshot-mainnet-mpt.log 2>&1

# hoodi
0 2 1,15 * * ENV_FILE=$REPO/snapshot-hoodi.env python3 $REPO/snapshot_make.py >> /var/log/snapshot-hoodi.log 2>&1
```

### 4. Start the Metrics Server

Run `metrics_server.py` as a persistent pm2 process so it survives server reboots:

```bash
pm2 startup # register pm2 itself as a system startup service (run once)
pm2 start python3 --name morph-snapshot-metrics -- /data/run-morph-node/ops/snapshot/metrics_server.py
pm2 save
```

Once running, the metrics endpoint is available at `http://<server-ip>:6060/metrics`.

Exposed metrics:

| Metric | Type | Description |
|---|---|---|
| `morph_snapshot_readme_update_status` | gauge | 1 = success, 0 = failure |
| `morph_snapshot_readme_update_timestamp_seconds` | gauge | Unix timestamp of the last run |

Labels: `environment` (mainnet / hoodi / holesky), `snapshot` (snapshot name)

> Default metrics file path: `/tmp/morph_snapshot_metrics.prom`
> Override via the `METRICS_FILE` environment variable — applies to both `update_readme.py` and `metrics_server.py`.

## Key Design Decisions

- **`base_height` is collected after geth restarts**: querying the RPC after the snapshot is created and geth is started alone gives the actual block state of the snapshot, which is more accurate than querying before the stop. `morph-node` is started only after the height is confirmed.
- **Fallback recovery on failure**: if the snapshot or upload fails, a fallback step in `snapshot_make.py` attempts to restart both processes to avoid prolonged service interruption.
- **No GitHub Actions or git CLI required**: `update_metadata.py` uses the GitHub REST API directly — the server only needs Python 3. The `GH_TOKEN` is the only credential needed.
- **New entries are inserted at the top of the table**: the latest snapshot always appears in the first row for quick access.
- **Changes are merged via PR, not direct push**: a new branch is created and a PR is opened, preserving review opportunity and preventing automated scripts from writing directly to the main branch.


Loading