diff --git a/README.md b/README.md index 55eb691..12c85a2 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ - **Dockerized Deployment**: Simplifies the process of setting up Morph nodes using Docker containers. - **Network Support**: Provides configurations for both Mainnet and Hoodi testnet environments. +- **Execution Client Choice**: Run the node with either **geth** (`go-ethereum`) or **reth** (`morph-reth`) as the execution client. - **Snapshot Synchronization**: Supports synchronizing node data from snapshots to expedite the setup process. ## Prerequisites @@ -67,6 +68,33 @@ Before setting up a Morph node, ensure you have the following installed: └── data // data directory from snapshot/node ``` + - **Reth uses a different snapshot** (the `*-reth-*` rows in the snapshot + table) with a different execution-client layout. Its tarball contains a + `reth-data/` tree plus the same `data/` for the node. `static-nodes.json` + is not in the snapshot; the `quickstart-*-reth-*` / `setup-snapshot-data-reth` + helpers copy it from `geth-data/` into `reth-data/` for `--trusted-peers`. + For example, if the reth snapshot folder is named `snapshot-archive-reth-20260902-1`, + move the directories into the `MORPH_HOME` directories: + ``` + mv ${MORPH_HOME}/snapshot-archive-reth-20260902-1/reth-data/* ${MORPH_HOME}/reth-data + mv ${MORPH_HOME}/snapshot-archive-reth-20260902-1/data/* ${MORPH_HOME}/node-data/data + ``` + The folder structure will be like + ``` + └── ${MORPH_HOME} + ├── reth-data // data directory for reth + │ ├── db // from snapshot/reth-data + │ ├── static_files // from snapshot/reth-data + │ ├── rocksdb // from snapshot/reth-data + │ ├── morph // from snapshot/reth-data + │ └── static-nodes.json // copied from geth-data, parsed into --trusted-peers + └── node-data // data directory for node + ├── config + │ ├── config.toml + │ └── genesis.json + └── data // data directory from snapshot/node + ``` + 5. **Run the Node**: @@ -84,6 +112,71 @@ Before setting up a Morph node, ensure you have the following installed: - This command will set up and run the node based on the configurations specified in your .env file. +### Running with reth instead of geth + +The node can run with **reth** (`morph-reth`) as its execution client instead of +geth. Reth uses a **separate execution-client data directory** (`${MORPH_HOME}/reth-data`) +and, importantly, a **different snapshot** than geth (see the [Snapshot Information](#snapshot-information) +table — reth snapshots are the `*-reth-*` rows). The `morph-node` (derivation) side +is identical; only the execution client and its snapshot differ. + +- Start a reth-backed node with Docker Compose: + + ```bash + make run-reth-node # mainnet + make run-hoodi-reth-node # hoodi + ``` + + Stop / remove: + + ```bash + make stop-reth-node + make rm-reth-node + ``` + +- Or use a one-command quickstart (downloads the reth snapshot, places the data, and runs): + + ```bash + make quickstart-mainnet-reth-node # mainnet, Docker + make quickstart-hoodi-reth-node # hoodi, Docker + ``` + +- **Binary mode** (build `morph-reth` from source with `cargo`, then run the binaries directly): + + ```bash + make build-reth-all # builds morph-reth + morphnode into ./bin + make run-reth-node-binary # mainnet + make run-hoodi-reth-node-binary # hoodi + make stop-binary # stops geth/reth/morphnode + # or one-command: make quickstart-mainnet-reth-node-binary / quickstart-hoodi-reth-node-binary + ``` + + `build-reth` clones `morph-reth` into `../morph-reth` and checks out a pinned + tag `v$(RETH_VERSION)` (default `1.3.0`, kept in sync with the image in + `docker-compose.reth.yml`) so the binary and Docker paths run the same version. + Bump it in one place with `make set-versions RETH_VERSION=1.4.0 ...` (rewrites the + compose image tag), or build a one-off with `make build-reth RETH_VERSION=1.4.0`. + +`morph-reth` reads the boot nodes from `static-nodes.json` and passes them to +`--trusted-peers` (discovery is disabled). The snapshot does **not** contain +`static-nodes.json`, so the snapshot setup copies it from `${MORPH_HOME}/geth-data/static-nodes.json` +into `${MORPH_HOME}/reth-data/`. The reth launch command used by the entrypoint is: + +```bash +morph-reth node \ + --chain \ + --datadir $RETH_DATA_DIR \ + --http --http.addr 0.0.0.0 --http.port 8545 --http.corsdomain '*' \ + --http.api web3,eth,txpool,net,trace \ + --ws --ws.addr 0.0.0.0 --ws.port 8546 --ws.origins '*' \ + --ws.api web3,eth,txpool,net,trace \ + --authrpc.addr 0.0.0.0 --authrpc.port 8551 --authrpc.jwtsecret /jwt-secret.txt \ + --disable-discovery --nat none \ + --metrics 0.0.0.0:6060 \ + --log.file.directory $LOG_DIR --log.file.filter info \ + --trusted-peers $TRUSTED_PEERS # parsed from static-nodes.json +``` + ### Running as a validator (batch verification mode) There is no separate validator service anymore — every node self-verifies L1 batches. The @@ -114,6 +207,17 @@ There is no separate validator container anymore — both paths run the same `mo The table below provides the node snapshot data and corresponding download URLs. Ensure `DERIVATION_START_HEIGHT`, `L1_MSG_START_HEIGHT`, and `L2_BASE_HEIGHT` in `.env`/`.env_hoodi` match the selected snapshot. +> **geth vs reth snapshots:** rows whose name contains `reth` (e.g. `snapshot-archive-reth-*`) +> are for the **reth** execution client and must be used with the `*-reth-*` make targets; +> the other rows are **geth** snapshots. The two are not interchangeable. Set +> `MAINNET_SNAPSHOT_NAME`/`HOODI_SNAPSHOT_NAME` for geth and +> `MAINNET_RETH_SNAPSHOT_NAME`/`HOODI_RETH_SNAPSHOT_NAME` for reth in the env files. +> +> Because the two clients use different snapshots, the height variables are also split: +> the geth path uses `DERIVATION_START_HEIGHT`/`L1_MSG_START_HEIGHT`/`L2_BASE_HEIGHT`, and +> the reth path uses `RETH_DERIVATION_START_HEIGHT`/`RETH_L1_MSG_START_HEIGHT`/`RETH_L2_BASE_HEIGHT`. +> Ensure each set matches the height row of its selected snapshot. + **For mainnet** (reth is currently in an internal testing phase and is not yet recommended for production use): | Snapshot Name | Derivation Start Height | L1 Msg Start Height | L2 Base Height | diff --git a/hoodi/reth-data/.gitignore b/hoodi/reth-data/.gitignore new file mode 100644 index 0000000..905bda3 --- /dev/null +++ b/hoodi/reth-data/.gitignore @@ -0,0 +1,6 @@ +# Snapshot- and runtime-generated reth data. Only static-nodes.json (parsed into +# --trusted-peers) and this file are tracked; everything else is produced by the +# reth snapshot setup or by running the node. +* +!.gitignore +!static-nodes.json diff --git a/hoodi/reth-data/static-nodes.json b/hoodi/reth-data/static-nodes.json new file mode 100644 index 0000000..5432c3f --- /dev/null +++ b/hoodi/reth-data/static-nodes.json @@ -0,0 +1,5 @@ +[ +"enode://8efa3da017d3eeb9db761e17c10121ee3ee6d258045ac4fba42549552376547f818a31e933b9dd904528aaadc9b6b457e9d1970e3cbbad42d7a0171686cbd994@13.159.40.158:30303", +"enode://884f27d218751e1f64eec36f7a5bad2bd03006d0a73b9557e36bb90847db5cb8cc2f86cb88121cecaf8599779ec088a9670fec8ee8611885591a97b52064f171@18.177.181.171:30303", +"enode://cc0a69714111d69bb6f664c06d4d8a560019259ba9828ffd5714d465bc334652354b1660b6c058c50821f790d2f76a005bf0a3c74b55cce44b7e10180130fac3@13.230.212.70:30303" +] \ No newline at end of file diff --git a/mainnet/reth-data/.gitignore b/mainnet/reth-data/.gitignore new file mode 100644 index 0000000..905bda3 --- /dev/null +++ b/mainnet/reth-data/.gitignore @@ -0,0 +1,6 @@ +# Snapshot- and runtime-generated reth data. Only static-nodes.json (parsed into +# --trusted-peers) and this file are tracked; everything else is produced by the +# reth snapshot setup or by running the node. +* +!.gitignore +!static-nodes.json diff --git a/mainnet/reth-data/static-nodes.json b/mainnet/reth-data/static-nodes.json new file mode 100644 index 0000000..c6c1408 --- /dev/null +++ b/mainnet/reth-data/static-nodes.json @@ -0,0 +1,5 @@ +[ +"enode://53496aab21ab73f261551d823340b417ecd2cf69652ceba670162b990429e75c4dd6eb622dd96978d65f70c9db9964b3f477886c0b62297a51266168d367bab2@52.193.82.123:30303", +"enode://58773a8b58e70cda8d39ed7500b56f176b22c5d4faab88348a0835f333f25ae3b3a45bf7f3e9fd08245d1ff57d38d01540571c0221be2404ce93568f8472c3a0@52.197.80.227:30303", +"enode://86faa94f8221ae100c6f3aebc2f79941431ce2178f1f4374a73511230846bc26df4c336139e766668f5ba8daea56fdce74f61ecc31a2cba3788e70a2d15f5258@18.177.252.130:30303" +] \ No newline at end of file diff --git a/morph-node/.env b/morph-node/.env index 6bd22c1..6032a50 100644 --- a/morph-node/.env +++ b/morph-node/.env @@ -4,6 +4,21 @@ JWT_SECRET_FILE=${MORPH_HOME}/jwt-secret.txt GETH_ENTRYPOINT_FILE=./entrypoint-geth.sh MAINNET_SNAPSHOT_NAME=snapshot-20260701-1 +## Reth execution client (alternative to geth). Reth uses a SEPARATE snapshot +## (reth-data/ tree) and a separate data directory (${RETH_HOME}/reth-data), +## but shares the same network home dir and node-data with the geth flow. +RETH_HOME=${MORPH_HOME} +RETH_CHAIN=mainnet +RETH_ENTRYPOINT_FILE=./entrypoint-reth.sh +MAINNET_RETH_SNAPSHOT_NAME=snapshot-archive-reth-20260902-1 +# Reth uses a SEPARATE snapshot from geth, so it needs its own heights. These must +# match MAINNET_RETH_SNAPSHOT_NAME above (see the README snapshot table). The reth +# compose/binary paths use these instead of the shared DERIVATION_*/L1_MSG_*/L2_BASE_* +# below (which track the geth snapshot). +RETH_DERIVATION_START_HEIGHT=25885664 +RETH_L1_MSG_START_HEIGHT=25880870 +RETH_L2_BASE_HEIGHT=26137782 + ## Environment variables for the node L1_CHAIN_ID=1 L1_ETH_RPC=https://eth.drpc.org diff --git a/morph-node/.env_hoodi b/morph-node/.env_hoodi index 971d5c1..e874bef 100644 --- a/morph-node/.env_hoodi +++ b/morph-node/.env_hoodi @@ -4,6 +4,21 @@ JWT_SECRET_FILE=${MORPH_HOME}/jwt-secret.txt GETH_ENTRYPOINT_FILE=./entrypoint-geth.sh HOODI_SNAPSHOT_NAME=snapshot-20260630-1 +## Reth execution client (alternative to geth). Reth uses a SEPARATE snapshot +## (reth-data/ tree) and a separate data directory (${RETH_HOME}/reth-data), +## but shares the same network home dir and node-data with the geth flow. +RETH_HOME=${MORPH_HOME} +RETH_CHAIN=hoodi +RETH_ENTRYPOINT_FILE=./entrypoint-reth.sh +HOODI_RETH_SNAPSHOT_NAME=snapshot-archive-reth-20260901-1 +# Reth uses a SEPARATE snapshot from geth, so it needs its own heights. These must +# match HOODI_RETH_SNAPSHOT_NAME above (see the README snapshot table). The reth +# compose/binary paths use these instead of the shared DERIVATION_*/L1_MSG_*/L2_BASE_* +# below (which track the geth snapshot). +RETH_DERIVATION_START_HEIGHT=3533250 +RETH_L1_MSG_START_HEIGHT=3529538 +RETH_L2_BASE_HEIGHT=8346700 + ## Environment variables for the node L1_CHAIN_ID=560048 L1_ETH_RPC=${your_layer1_execution_client_rpc_url} diff --git a/morph-node/Makefile b/morph-node/Makefile index 26c36d1..6432f66 100644 --- a/morph-node/Makefile +++ b/morph-node/Makefile @@ -2,11 +2,18 @@ include .env # Capture mainnet JWT path before .env_hoodi overwrites MORPH_HOME (:= forces immediate expansion) JWT_SECRET_FILE_MAINNET := $(JWT_SECRET_FILE) +# Capture mainnet reth snapshot name for the same reason. +MAINNET_RETH_SNAPSHOT_NAME := $(MAINNET_RETH_SNAPSHOT_NAME) include .env_hoodi JWT_SECRET_FILE_HOODI := $(JWT_SECRET_FILE) +# morph-reth version to build/run. Keep this in sync with the image tag in +# docker-compose.reth.yml (v$(RETH_VERSION)). Override on the command line, e.g. +# `make build-reth RETH_VERSION=1.4.0`. `?=` lets an env var take precedence. +RETH_VERSION ?= 1.3.0 + 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.") @@ -30,6 +37,24 @@ stop-node: rm-node: docker rm morph-node morph-geth +## reth node management (reth is an alternative execution client to geth; it uses +## a SEPARATE snapshot — see the README snapshot table — and its own reth-data dir). +run-reth-node: generate-jwt + @echo "You are about to run a Mainnet node with the reth execution client." + @{ [ -t 0 ] || { echo "Error: interactive confirmation required (run in a TTY)."; exit 1; }; printf "Continue? [Y/n] "; read confirm; if [ "$$confirm" = "n" ] || [ "$$confirm" = "N" ]; then exit 1; fi; } + docker-compose -f docker-compose.reth.yml --env-file .env up node & + +run-hoodi-reth-node: generate-jwt-hoodi + @echo "You are about to run a Hoodi node with the reth execution client." + @{ [ -t 0 ] || { echo "Error: interactive confirmation required (run in a TTY)."; exit 1; }; printf "Continue? [Y/n] "; read confirm; if [ "$$confirm" = "n" ] || [ "$$confirm" = "N" ]; then exit 1; fi; } + docker-compose -f docker-compose.reth.yml --env-file .env_hoodi up node & + +stop-reth-node: + docker stop morph-node morph-reth + +rm-reth-node: + docker rm morph-node morph-reth + ## Validator commands, kept for backward compatibility. A "validator" is now just ## the single node run in layer1 batch-verification mode (--derivation.verify-mode=layer1: ## pull the L1 beacon blob and derive via the engine). There is no separate validator @@ -57,14 +82,17 @@ submodule-init: @echo "Submodules ready." # Update submodule versions and docker-compose.yml image tags in one shot. -# Usage: make set-versions GETH_VERSION=2.2.1 NODE_VERSION=0.5.3 +# Usage: make set-versions GETH_VERSION=2.2.1 NODE_VERSION=0.5.3 [RETH_VERSION=1.4.0] +# RETH_VERSION also rewrites the morph-reth image tag in docker-compose.reth.yml so +# the Docker and binary reth paths stay on the same version (defaults to $(RETH_VERSION)). set-versions: cd ../go-ethereum && git fetch --tags && git checkout morph-v$(GETH_VERSION) cd ../morph && git fetch --tags && git checkout v$(NODE_VERSION) sed -i'' 's|go-ethereum:[0-9.]*|go-ethereum:$(GETH_VERSION)|g' docker-compose.yml sed -i'' 's|morph-l2/node:[0-9.]*|morph-l2/node:$(NODE_VERSION)|g' docker-compose.yml + sed -i'' 's|morph-reth:v[0-9.]*|morph-reth:v$(RETH_VERSION)|g' docker-compose.reth.yml cd .. && git add go-ethereum morph - @echo "Done. Commit with: git commit -m 'chore: bump geth=$(GETH_VERSION) node=$(NODE_VERSION)'" + @echo "Done. Commit with: git commit -m 'chore: bump geth=$(GETH_VERSION) node=$(NODE_VERSION) reth=$(RETH_VERSION)'" build-geth: submodule-init @if [ -f ./bin/geth ]; then \ @@ -88,9 +116,33 @@ build-morphnode: submodule-init echo "morphnode binary copied to morph-node/bin/morphnode"; \ fi +## Build morph-reth from source (requires the Rust toolchain / cargo). morph-reth +## lives in its own repo (https://github.com/morph-l2/morph-reth), not a submodule, +## so we clone it under ../morph-reth on first build. +build-reth: + @if [ -f ./bin/morph-reth ]; then \ + echo "morph-reth binary already exists at ./bin/morph-reth, skipping build."; \ + else \ + echo "Building morph-reth v$(RETH_VERSION) from source..."; \ + if [ ! -d ../morph-reth ]; then \ + echo "Cloning morph-reth..."; \ + git clone https://github.com/morph-l2/morph-reth.git ../morph-reth || { echo "Error: failed to clone morph-reth."; exit 1; }; \ + fi; \ + echo "Checking out v$(RETH_VERSION) (pinned to match docker-compose.reth.yml)..."; \ + (cd ../morph-reth && git fetch --tags && git checkout v$(RETH_VERSION)) || { echo "Error: failed to checkout morph-reth v$(RETH_VERSION)."; exit 1; }; \ + (cd ../morph-reth && cargo build --release) || { echo "Error: morph-reth build failed."; exit 1; }; \ + mkdir -p ./bin; \ + cp ../morph-reth/target/release/morph-reth ./bin/morph-reth || { echo "Error: failed to copy morph-reth binary to morph-node/bin/morph-reth."; exit 1; }; \ + echo "morph-reth binary copied to morph-node/bin/morph-reth"; \ + fi + build: build-geth build-morphnode @echo "All binaries built successfully." +## Build the reth-based binary set (reth + morphnode). +build-reth-all: build-reth build-morphnode + @echo "All reth binaries built successfully." + ## binary mode run-node-binary: generate-jwt @echo "You are about to run a Mainnet node (binary mode)." @@ -116,8 +168,20 @@ run-hoodi-validator-binary: generate-jwt-hoodi @{ [ -t 0 ] || { echo "Error: interactive confirmation required (run in a TTY)."; exit 1; }; printf "Continue? [Y/n] "; read confirm; if [ "$$confirm" = "n" ] || [ "$$confirm" = "N" ]; then exit 1; fi; } MORPH_NODE_DERIVATION_VERIFY_MODE=layer1 sh ./run-binary.sh .env_hoodi +## reth binary mode +run-reth-node-binary: generate-jwt + @echo "You are about to run a Mainnet node with reth (binary mode)." + @{ [ -t 0 ] || { echo "Error: interactive confirmation required (run in a TTY)."; exit 1; }; printf "Continue? [Y/n] "; read confirm; if [ "$$confirm" = "n" ] || [ "$$confirm" = "N" ]; then exit 1; fi; } + sh ./run-binary-reth.sh .env + +run-hoodi-reth-node-binary: generate-jwt-hoodi + @echo "You are about to run a Hoodi node with reth (binary mode)." + @{ [ -t 0 ] || { echo "Error: interactive confirmation required (run in a TTY)."; exit 1; }; printf "Continue? [Y/n] "; read confirm; if [ "$$confirm" = "n" ] || [ "$$confirm" = "N" ]; then exit 1; fi; } + sh ./run-binary-reth.sh .env_hoodi + stop-binary: @if [ -f .geth.pid ]; then kill $$(cat .geth.pid) 2>/dev/null; rm -f .geth.pid; echo "geth stopped."; fi + @if [ -f .reth.pid ]; then kill $$(cat .reth.pid) 2>/dev/null; rm -f .reth.pid; echo "reth stopped."; fi @if [ -f .node.pid ]; then kill $$(cat .node.pid) 2>/dev/null; rm -f .node.pid; echo "morphnode stopped."; fi clean-mainnet-binary: @@ -134,6 +198,20 @@ clean-hoodi-binary: @printf '{\n "height": "0",\n "round": 0,\n "step": 0\n}\n' > ../hoodi/node-data/data/priv_validator_state.json @echo "Hoodi snapshot data cleaned." +clean-mainnet-reth: + @echo "Cleaning mainnet reth snapshot data (keeping config)..." + rm -rf ../mainnet/reth-data/db ../mainnet/reth-data/static_files ../mainnet/reth-data/rocksdb ../mainnet/reth-data/morph + rm -rf ../mainnet/node-data/data/* + @printf '{\n "height": "0",\n "round": 0,\n "step": 0\n}\n' > ../mainnet/node-data/data/priv_validator_state.json + @echo "Mainnet reth snapshot data cleaned." + +clean-hoodi-reth: + @echo "Cleaning hoodi reth snapshot data (keeping config)..." + rm -rf ../hoodi/reth-data/db ../hoodi/reth-data/static_files ../hoodi/reth-data/rocksdb ../hoodi/reth-data/morph + rm -rf ../hoodi/node-data/data/* + @printf '{\n "height": "0",\n "round": 0,\n "step": 0\n}\n' > ../hoodi/node-data/data/priv_validator_state.json + @echo "Hoodi reth snapshot data cleaned." + # Helper: download, extract, and move snapshot data into MORPH_HOME # $(1) = snapshot name, $(2) = download URL base, $(3) = MORPH_HOME define setup-snapshot-data @@ -165,6 +243,41 @@ define setup-snapshot-data fi endef +# Helper: download, extract, and move a RETH snapshot into RETH_HOME. Reth snapshots +# pack a different layout than geth: /reth-data/{db,static_files,rocksdb,morph} +# plus /data/* for the node. static-nodes.json is NOT in the snapshot, so we +# copy it from geth-data/ (already tracked in the repo) into reth-data/ for --trusted-peers. +# $(1) = snapshot name, $(2) = download URL base, $(3) = RETH_HOME +define setup-snapshot-data-reth + @if [ -d "$(3)/reth-data/db" ]; then \ + echo "Reth snapshot data already in place at $(3), skipping setup."; \ + else \ + echo "Setting up reth snapshot data for $(3)..."; \ + if [ ! -d "$(3)/$(1)" ]; then \ + if [ ! -f "$(3)/$(1).tar.gz" ]; then \ + echo "Downloading $(1).tar.gz from $(2)..."; \ + wget -O $(3)/$(1).tar.gz $(2)/$(1).tar.gz || { echo "Error: download failed."; exit 1; }; \ + else \ + echo "Found existing $(3)/$(1).tar.gz, skipping download."; \ + fi; \ + echo "Extracting $(1).tar.gz..."; \ + tar -xzf $(3)/$(1).tar.gz -C $(3) || { echo "Error: extraction failed."; exit 1; }; \ + rm -f $(3)/$(1).tar.gz; \ + else \ + echo "Found existing $(3)/$(1)/ directory, skipping download."; \ + fi; \ + mkdir -p $(3)/reth-data $(3)/node-data/data; \ + echo "Moving reth data to $(3)/reth-data/..."; \ + mv $(3)/$(1)/reth-data/* $(3)/reth-data/ || { echo "Error: failed to move reth data (may already exist)."; exit 1; }; \ + echo "Moving node data to $(3)/node-data/data/..."; \ + mv $(3)/$(1)/data/* $(3)/node-data/data/ || { echo "Error: failed to move node data."; exit 1; }; \ + rm -rf $(3)/$(1); \ + [ -f "$(3)/reth-data/static-nodes.json" ] || cp $(3)/geth-data/static-nodes.json $(3)/reth-data/static-nodes.json 2>/dev/null || echo "Warning: no static-nodes.json found in $(3)/geth-data/; --trusted-peers will be empty."; \ + [ -f "$(3)/node-data/data/priv_validator_state.json" ] || printf '{\n "height": "0",\n "round": 0,\n "step": 0\n}\n' > $(3)/node-data/data/priv_validator_state.json; \ + echo "Reth snapshot data ready at $(3)."; \ + fi +endef + ## quickstart targets (one-command setup and run) quickstart-hoodi-node: generate-jwt-hoodi @@ -187,6 +300,27 @@ quickstart-mainnet-node-binary: generate-jwt build $(call setup-snapshot-data,$(MAINNET_SNAPSHOT_NAME),https://snapshot.morphl2.io/mainnet,../mainnet) sh ./run-binary.sh .env +## reth quickstart targets (use the reth snapshot + reth execution client) +quickstart-hoodi-reth-node: generate-jwt-hoodi + @echo "=== Quickstart: Hoodi Node with reth (Docker) ===" + $(call setup-snapshot-data-reth,$(HOODI_RETH_SNAPSHOT_NAME),https://snapshot.morphl2.io/hoodi,../hoodi) + docker-compose -f docker-compose.reth.yml --env-file .env_hoodi up node + +quickstart-hoodi-reth-node-binary: generate-jwt-hoodi build-reth-all + @echo "=== Quickstart: Hoodi Node with reth (Binary) ===" + $(call setup-snapshot-data-reth,$(HOODI_RETH_SNAPSHOT_NAME),https://snapshot.morphl2.io/hoodi,../hoodi) + sh ./run-binary-reth.sh .env_hoodi + +quickstart-mainnet-reth-node: generate-jwt + @echo "=== Quickstart: Mainnet Node with reth (Docker) ===" + $(call setup-snapshot-data-reth,$(MAINNET_RETH_SNAPSHOT_NAME),https://snapshot.morphl2.io/mainnet,../mainnet) + docker-compose -f docker-compose.reth.yml --env-file .env up node + +quickstart-mainnet-reth-node-binary: generate-jwt build-reth-all + @echo "=== Quickstart: Mainnet Node with reth (Binary) ===" + $(call setup-snapshot-data-reth,$(MAINNET_RETH_SNAPSHOT_NAME),https://snapshot.morphl2.io/mainnet,../mainnet) + sh ./run-binary-reth.sh .env + # Common function for download and decompress # $(1) = snapshot name, $(2) = download URL base, $(3) = target directory @@ -227,6 +361,13 @@ download-and-decompress-mainnet-snapshot: download-and-decompress-hoodi-snapshot: $(call download-and-decompress,$(HOODI_SNAPSHOT_NAME),https://snapshot.morphl2.io/hoodi,../hoodi) +## reth snapshots (separate from the geth snapshots above) +download-and-decompress-mainnet-reth-snapshot: + $(call download-and-decompress,$(MAINNET_RETH_SNAPSHOT_NAME),https://snapshot.morphl2.io/mainnet,../mainnet) + +download-and-decompress-hoodi-reth-snapshot: + $(call download-and-decompress,$(HOODI_RETH_SNAPSHOT_NAME),https://snapshot.morphl2.io/hoodi,../hoodi) + # Backward-compatible aliases used in older PR/test plans download-and-decompress-mainnet-mpt-snapshot: download-and-decompress-mainnet-snapshot diff --git a/morph-node/docker-compose.reth.yml b/morph-node/docker-compose.reth.yml new file mode 100644 index 0000000..a414883 --- /dev/null +++ b/morph-node/docker-compose.reth.yml @@ -0,0 +1,68 @@ +services: + reth: + container_name: morph-reth + image: ghcr.io/morph-l2/morph-reth:v1.3.0 + restart: unless-stopped + ports: + - "8545:8545" + - "8546:8546" + - "8551" + - "6060" + - "30303" + volumes: + - "${RETH_HOME}/reth-data:/db" + - "${JWT_SECRET_FILE}:/jwt-secret.txt" + - "${RETH_ENTRYPOINT_FILE}:/entrypoint.sh" + environment: + # Chain spec: "mainnet" or "hoodi". + - RETH_CHAIN=${RETH_CHAIN} + - RETH_DATADIR=/db + - LOG_DIR=/db + # static-nodes.json lives inside the reth-data volume (mounted at /db) and is + # parsed into --trusted-peers by the entrypoint. If absent, --trusted-peers is + # omitted rather than failing. + - STATIC_NODES_FILE=/db/static-nodes.json + entrypoint: + - "/bin/sh" + - "/entrypoint.sh" + + # Single self-verifying node, identical to docker-compose.yml except it talks to + # the reth execution client instead of geth. There is no separate validator + # service: every node verifies batches via --derivation.verify-mode. Set + # DERIVATION_VERIFY_MODE=layer1 (in .env/.env_hoodi) for the former validator + # behavior. L1_BEACON_CHAIN_RPC is required in both modes. + node: + container_name: morph-node + depends_on: + reth: + condition: service_started + image: ghcr.io/morph-l2/node:0.6.3 + restart: unless-stopped + ports: + - "26656" + - "26657" + - "26658" + - "26660" + environment: + - MORPH_NODE_L2_ETH_RPC=http://morph-reth:8545 + - MORPH_NODE_L2_ENGINE_RPC=http://morph-reth:8551 + - MORPH_NODE_L2_ENGINE_AUTH=/jwt-secret.txt + - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} + - MORPH_NODE_L1_ETH_BEACON_RPC=${L1_BEACON_CHAIN_RPC} + - MORPH_NODE_L1_CHAIN_ID=${L1_CHAIN_ID} + - MORPH_NODE_ROLLUP_ADDRESS=${ROLLUP_CONTRACT} + - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${L1MESSAGEQUEUE_CONTRACT} + # Reth uses a separate snapshot from geth, so these come from the RETH_* heights + # (which match *_RETH_SNAPSHOT_NAME), not the shared geth heights. + - MORPH_NODE_SYNC_START_HEIGHT=${RETH_L1_MSG_START_HEIGHT} + - MORPH_NODE_DERIVATION_START_HEIGHT=${RETH_DERIVATION_START_HEIGHT} + - MORPH_NODE_DERIVATION_BASE_HEIGHT=${RETH_L2_BASE_HEIGHT} + - MORPH_NODE_DERIVATION_VERIFY_MODE=${DERIVATION_VERIFY_MODE:-} + - NODE_EXTRA_FLAGS=${NODE_EXTRA_FLAGS:-} + volumes: + - "${RETH_HOME}/node-data:/db" + - "${JWT_SECRET_FILE}:/jwt-secret.txt" + - "./entrypoint-node.sh:/entrypoint-node.sh" + entrypoint: + - "/bin/sh" + - "/entrypoint-node.sh" diff --git a/morph-node/entrypoint-reth.sh b/morph-node/entrypoint-reth.sh new file mode 100755 index 0000000..3ce13cb --- /dev/null +++ b/morph-node/entrypoint-reth.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +# Reth execution-client entrypoint. Mirrors entrypoint-geth.sh but launches +# morph-reth instead of geth. The two clients use DIFFERENT snapshots (see the +# README snapshot table): reth snapshots pack a reth-data/ tree (db, static_files, +# rocksdb, morph), whereas geth snapshots pack a geth/ chaindata tree. + +RETH_BIN=${RETH_BINARY:-morph-reth} +RETH_DATADIR=${RETH_DATADIR:-./db} +JWT_PATH=${JWT_SECRET_PATH:-/jwt-secret.txt} +# mainnet | hoodi (selects the built-in chain spec). +RETH_CHAIN=${RETH_CHAIN:-mainnet} +# Directory reth writes its rotating log file into. +LOG_DIR=${LOG_DIR:-${RETH_DATADIR}} +# static-nodes.json (JSON array of enode URLs). Its entries are joined with commas +# and passed to --trusted-peers so reth dials the Morph boot nodes directly +# (discovery is disabled below). +STATIC_NODES_FILE=${STATIC_NODES_FILE:-} + +if [ ! -f "${JWT_PATH}" ]; then + echo "Error: jwt-secret.txt not found at ${JWT_PATH}." + echo "Please create it before starting the service." + exit 1 +fi + +mkdir -p "${LOG_DIR}" + +# Parse static-nodes.json (a JSON array of "enode://..." strings) into a single +# comma-separated list for --trusted-peers. No jq dependency: strip everything +# that isn't part of an enode URL, then squeeze the newlines into commas. +TRUSTED_PEERS="" +if [ -n "${STATIC_NODES_FILE}" ] && [ -f "${STATIC_NODES_FILE}" ]; then + TRUSTED_PEERS=$(grep -o 'enode://[^"]*' "${STATIC_NODES_FILE}" | paste -sd, -) +fi + +set -- "${RETH_BIN}" node \ + --chain "${RETH_CHAIN}" \ + --datadir "${RETH_DATADIR}" \ + --http \ + --http.addr 0.0.0.0 \ + --http.port 8545 \ + --http.corsdomain '*' \ + --http.api web3,eth,txpool,net,trace \ + --ws \ + --ws.addr 0.0.0.0 \ + --ws.port 8546 \ + --ws.origins '*' \ + --ws.api web3,eth,txpool,net,trace \ + --authrpc.addr 0.0.0.0 \ + --authrpc.port 8551 \ + --authrpc.jwtsecret "${JWT_PATH}" \ + --disable-discovery \ + --nat none \ + --metrics 0.0.0.0:6060 \ + --log.file.directory "${LOG_DIR}" \ + --log.file.filter info + +# Only append --trusted-peers when we actually resolved some, so an empty/missing +# static-nodes.json doesn't hand reth a bare flag with no value. +if [ -n "${TRUSTED_PEERS}" ]; then + set -- "$@" --trusted-peers "${TRUSTED_PEERS}" +fi + +exec "$@" diff --git a/morph-node/run-binary-reth.sh b/morph-node/run-binary-reth.sh new file mode 100755 index 0000000..acfe191 --- /dev/null +++ b/morph-node/run-binary-reth.sh @@ -0,0 +1,121 @@ +#!/bin/sh + +# Usage: run-binary-reth.sh [override-env-file] +# env-file: .env | .env_hoodi +# override-env-file: optional extra env file layered on top +# +# Binary-mode launcher for the reth execution client + morphnode. Mirrors +# run-binary.sh but starts morph-reth instead of geth. Reth uses a separate +# snapshot and data dir (${RETH_HOME}/reth-data); node-data is shared. + +ENV_FILE=${1:-.env} +OVERRIDE_ENV_FILE=${2:-} + +# Source environment +set -a +. ./${ENV_FILE} +if [ -n "${OVERRIDE_ENV_FILE}" ]; then + . ./${OVERRIDE_ENV_FILE} +fi +set +a + +RETH_BINARY=${RETH_BINARY:-./bin/morph-reth} +NODE_BINARY=${NODE_BINARY:-./bin/morphnode} + +# Check binaries +if [ ! -f "${RETH_BINARY}" ]; then + echo "Error: morph-reth binary not found at ${RETH_BINARY}" + echo "Please build it (make build-reth) or place it in the bin/ directory." + exit 1 +fi + +if [ ! -f "${NODE_BINARY}" ]; then + echo "Error: morphnode binary not found at ${NODE_BINARY}" + echo "Please download and place it in the bin/ directory." + exit 1 +fi + +# Ensure data directories exist +mkdir -p "${RETH_HOME}/reth-data" +mkdir -p "${RETH_HOME}/node-data" + +# Cleanup on exit +cleanup() { + echo "" + echo "Stopping..." + [ -n "${RETH_PID:-}" ] && kill ${RETH_PID} 2>/dev/null + [ -n "${NODE_PID:-}" ] && kill ${NODE_PID} 2>/dev/null + rm -f .reth.pid .node.pid + exit +} +trap cleanup INT TERM + +# Validate entrypoint file +case "${RETH_ENTRYPOINT_FILE}" in + ./entrypoint-reth.sh) ;; + *) + echo "Error: invalid RETH_ENTRYPOINT_FILE: ${RETH_ENTRYPOINT_FILE}" + exit 1 + ;; +esac + +# Start reth +echo "Starting morph-reth..." +RETH_BINARY=${RETH_BINARY} \ +RETH_DATADIR=${RETH_HOME}/reth-data \ +LOG_DIR=${RETH_HOME}/reth-data \ +JWT_SECRET_PATH=${JWT_SECRET_FILE} \ +RETH_CHAIN=${RETH_CHAIN} \ +STATIC_NODES_FILE=${RETH_HOME}/reth-data/static-nodes.json \ +sh ${RETH_ENTRYPOINT_FILE} & +RETH_PID=$! +echo "${RETH_PID}" > .reth.pid + +# Wait for reth engine RPC (port 8551) +echo "Waiting for reth to be ready..." +for i in $(seq 1 30); do + if nc -z localhost 8551 2>/dev/null; then + echo "Reth is ready." + break + fi + if ! kill -0 ${RETH_PID} 2>/dev/null; then + echo "Error: reth process exited unexpectedly." + rm -f .reth.pid + exit 1 + fi + sleep 1 +done + +if ! nc -z localhost 8551 2>/dev/null; then + echo "Error: reth did not become ready within 30 seconds." + cleanup + exit 1 +fi + +# Start morphnode. +# Reth uses a separate snapshot from geth with its own heights (RETH_*). entrypoint-node.sh +# prefers MORPH_NODE_* over the shared DERIVATION_*/L1_MSG_*/L2_BASE_* vars, so pass the +# reth heights explicitly as MORPH_NODE_* to override the (geth) shared values. +echo "Starting morphnode..." +NODE_BINARY=${NODE_BINARY} \ +NODE_HOME=${RETH_HOME}/node-data \ +JWT_SECRET_PATH=${JWT_SECRET_FILE} \ +MORPH_NODE_DERIVATION_START_HEIGHT=${RETH_DERIVATION_START_HEIGHT} \ +MORPH_NODE_DERIVATION_BASE_HEIGHT=${RETH_L2_BASE_HEIGHT} \ +MORPH_NODE_SYNC_START_HEIGHT=${RETH_L1_MSG_START_HEIGHT} \ +NODE_EXTRA_FLAGS="${NODE_EXTRA_FLAGS:-}" \ +sh ./entrypoint-node.sh & +NODE_PID=$! +echo "${NODE_PID}" > .node.pid + +echo "" +echo "=========================================" +echo " reth PID: ${RETH_PID}" +echo " morphnode PID: ${NODE_PID}" +echo " reth log: ${RETH_HOME}/reth-data/reth.log" +echo " node log: ${RETH_HOME}/node-data/node.log" +echo "=========================================" +echo "Press Ctrl+C to stop both processes." +echo "" + +wait