From 479c7029a50a51959d0b1421d04a304e6d8dd83f Mon Sep 17 00:00:00 2001 From: nol4lej Date: Tue, 16 Jun 2026 20:00:05 -0400 Subject: [PATCH 01/13] feat: clean docker --- docker/testnet/Makefile | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docker/testnet/Makefile b/docker/testnet/Makefile index 5e20abc1..3721d5de 100644 --- a/docker/testnet/Makefile +++ b/docker/testnet/Makefile @@ -94,6 +94,19 @@ reset-chain: ## Wipe chain DB + network cache for a re-genesis. KEEPS keystore. rm -rf /data/chains/orbinum_testnet/db /data/chains/orbinum_testnet/network @echo "✅ Chain data wiped. Run 'make up' to start from the new genesis." +# ── Full cleanup (DESTRUCTIVE — wipes EVERYTHING) ───────────────────────────── +.PHONY: clean +clean: ## Wipe ALL: containers, volumes (chain DB + keystore), networks, images. Asks first. + @echo "⚠️ This permanently DESTROYS everything for $(ROLE) ($(CONTAINER)):" + @echo " - containers (stopped + removed)" + @echo " - volumes → chain database AND keystore (session keys LOST)" + @echo " - networks (project networks removed)" + @echo " - images (compose images removed)" + @echo " This is NOT recoverable. Back up the keystore first if you need it." + @printf " Type 'yes' to continue: " && read ans && [ "$$ans" = "yes" ] || (echo "aborted" && exit 1) + $(DC) down --volumes --remove-orphans --rmi all + @echo "✅ Full cleanup done. Run 'make up' to start fresh." + # ── Help ────────────────────────────────────────────────────────────────────── .PHONY: help help: ## Show this help From 04d9524e361020c744ba6cdf92b8030a1002f488 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Tue, 16 Jun 2026 20:45:01 -0400 Subject: [PATCH 02/13] feat: add allowed origins --- docker/testnet/.env.rpc.example | 16 ++++++++++++++++ docker/testnet/Caddyfile | 12 +++++++++++- docker/testnet/docker-compose.rpc.yml | 4 +++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docker/testnet/.env.rpc.example b/docker/testnet/.env.rpc.example index 804122f9..5cd455ee 100644 --- a/docker/testnet/.env.rpc.example +++ b/docker/testnet/.env.rpc.example @@ -20,3 +20,19 @@ RPC_NODE_KEY= # proxies HTTPS/WSS to localhost:9944. Must already have a DNS A record # pointing to this VPS's public IP. Use rpc-1 / rpc-2 / ... per node. RPC_DOMAIN=rpc-1.testnet.orbinum.io + +# ── Control de acceso al RPC (dos capas) ────────────────────────────────────── + +# Capa 1 — CORS de substrate (--rpc-cors). Client-side (lo respeta el browser). +# Formato: lista separada por comas, o "all". +# un origin : https://explorer.testnet.orbinum.network +# varios : https://explorer.testnet.orbinum.network,https://app.orbinum.io +# todos : all +ALLOWED_ORIGINS=https://explorer.testnet.orbinum.network,https://explorer-dev.testnet.orbinum.network + +# Capa 2 — Bloqueo en Caddy. Server-side (real: bloquea curl/scripts y Origins no +# permitidos). REGEX sobre el header Origin. Anclar con ^...$ y escapar puntos \. +# un origin : ^https://explorer\.testnet\.orbinum\.network$ +# varios : ^https://(explorer\.testnet\.orbinum\.network|app\.orbinum\.io)$ +# todos : .* (desactiva el bloqueo) +CADDY_ALLOWED_ORIGINS=^https://(explorer\.testnet\.orbinum\.network|explorer-dev\.testnet\.orbinum\.network)$ \ No newline at end of file diff --git a/docker/testnet/Caddyfile b/docker/testnet/Caddyfile index 85c96cf8..5107f1cb 100644 --- a/docker/testnet/Caddyfile +++ b/docker/testnet/Caddyfile @@ -1,3 +1,13 @@ {$RPC_DOMAIN:rpc-1.testnet.orbinum.io} { + # Server-side Origin blocking (actual enforcement — also blocks curl/scripts + # that send a non-allowed Origin, and requests without an Origin header). + # CADDY_ALLOWED_ORIGINS is a REGEX that matches the Origin header. Anchor it with ^...$ + # and escape dots with \. + # single origin : ^https://explorer\.testnet\.orbinum\.network$ + # multiple : ^https://(explorer\.testnet\.orbinum\.network|app\.orbinum\.io)$ + # all origins : .* (disables the restriction) + @blocked not header_regexp Origin {$CADDY_ALLOWED_ORIGINS:^https://explorer\.testnet\.orbinum\.network$} + respond @blocked "Forbidden" 403 + reverse_proxy localhost:9944 -} +} \ No newline at end of file diff --git a/docker/testnet/docker-compose.rpc.yml b/docker/testnet/docker-compose.rpc.yml index a5878276..0b253fa5 100644 --- a/docker/testnet/docker-compose.rpc.yml +++ b/docker/testnet/docker-compose.rpc.yml @@ -14,9 +14,10 @@ services: --listen-addr /ip4/0.0.0.0/tcp/30333 --node-key ${RPC_NODE_KEY} --rpc-port 9944 - --rpc-cors all + --rpc-cors ${ALLOWED_ORIGINS:-https://explorer.testnet.orbinum.network} --rpc-external --rpc-methods Safe + --rpc-max-connections 200 --no-mdns --no-private-ipv4 --prometheus-external @@ -41,6 +42,7 @@ services: restart: unless-stopped environment: RPC_DOMAIN: ${RPC_DOMAIN:-rpc-1.testnet.orbinum.io} + CADDY_ALLOWED_ORIGINS: ${CADDY_ALLOWED_ORIGINS:-^https://explorer\.testnet\.orbinum\.network$} volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data From 91a17e65398406cea97a974d4b398a3438480149 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Tue, 16 Jun 2026 23:50:09 -0400 Subject: [PATCH 03/13] feat(testnet): lock RPC CORS to allowed origins, tune RPC node --- docker/testnet/docker-compose.rpc.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/testnet/docker-compose.rpc.yml b/docker/testnet/docker-compose.rpc.yml index 0b253fa5..811d5ab7 100644 --- a/docker/testnet/docker-compose.rpc.yml +++ b/docker/testnet/docker-compose.rpc.yml @@ -17,7 +17,9 @@ services: --rpc-cors ${ALLOWED_ORIGINS:-https://explorer.testnet.orbinum.network} --rpc-external --rpc-methods Safe - --rpc-max-connections 200 + --rpc-max-connections 1000 + --state-pruning archive + --blocks-pruning archive --no-mdns --no-private-ipv4 --prometheus-external From fecd62d58734830f65d1637c419e6a425bf327f7 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Tue, 16 Jun 2026 23:50:42 -0400 Subject: [PATCH 04/13] fix(testnet): make Makefile role-selectable via ROLE override --- docker/testnet/Makefile | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docker/testnet/Makefile b/docker/testnet/Makefile index 3721d5de..5c7de6c4 100644 --- a/docker/testnet/Makefile +++ b/docker/testnet/Makefile @@ -1,25 +1,28 @@ # Orbinum Testnet — node operations # -# Auto-detects the node role from which compose file exists on this server: -# - docker-compose.rpc.yml present → RPC / bootnode -# - docker-compose.yml present → validator +# Picks the node role. Override with ROLE=rpc or ROLE=validator on any command: +# make down # auto-detect (rpc if its compose exists, else validator) +# make down ROLE=validator # force validator (needed when BOTH compose files exist) +# make up ROLE=rpc # Same Makefile on every server; each one operates on its own node. # # Usage: make (run from this directory) # make help lists all targets -# ── Role auto-detection ─────────────────────────────────────────────────────── -# Prefer the RPC compose if it exists, else fall back to the validator compose. -COMPOSE := $(firstword $(wildcard docker-compose.rpc.yml docker-compose.yml)) -# The volume name is "_"; project defaults to this dir's name. +# ── Role selection ──────────────────────────────────────────────────────────── +# ROLE may be set on the command line. If unset, auto-detect: prefer the RPC +# compose when present, else the validator compose. When BOTH files exist on the +# same checkout (e.g. the dev repo), auto-detect always picks rpc — pass +# ROLE=validator explicitly to operate on the validator stack. +ROLE ?= $(if $(wildcard docker-compose.rpc.yml),rpc,validator) PROJECT := $(notdir $(CURDIR)) -ifeq ($(COMPOSE),docker-compose.rpc.yml) +ifeq ($(ROLE),rpc) + COMPOSE := docker-compose.rpc.yml CONTAINER := orbinum-rpc-node - ROLE := RPC VOLUME := $(PROJECT)_rpc-node-data else + COMPOSE := docker-compose.yml CONTAINER := orbinum-validator - ROLE := validator VOLUME := $(PROJECT)_validator-data endif From dfad6f5ef003276407a5cd4d1640512e71520056 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Tue, 16 Jun 2026 23:50:59 -0400 Subject: [PATCH 05/13] docs(testnet): fix container names and in-container curl calls --- docker/testnet/README.md | 24 ++++++++++++------------ docker/testnet/rpc-node.md | 24 ++++++++++++++++++------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/docker/testnet/README.md b/docker/testnet/README.md index 5e8871ba..f013fdec 100644 --- a/docker/testnet/README.md +++ b/docker/testnet/README.md @@ -173,7 +173,7 @@ With your `.env` file in place (see A.3), start the node from `node/docker/testn ```bash docker compose pull # download pre-built image (~1-2 min) docker compose up -d # start validator + watchtower -docker compose logs -f validator +docker compose logs -f orbinum-validator ``` Wait until you see `Idle` in the logs before proceeding: @@ -185,7 +185,7 @@ Wait until you see `Idle` in the logs before proceeding: Get your **Peer ID** — you will need it when requesting approval: ```bash -docker compose logs validator | grep "Local node identity" +docker compose logs orbinum-validator | grep "Local node identity" # -> Local node identity is: 12D3KooWxxxxx... ``` @@ -264,8 +264,8 @@ This writes the keys into the **node's local keystore** via RPC. These are local The validator's RPC port (`9944`) is **not** published to the host or the network — it only listens on `127.0.0.1` *inside* the container. Run the calls -with `docker compose exec`, which executes them from inside the container where -`localhost:9944` is reachable (the image ships `curl` for this purpose). +with `docker exec orbinum-validator`, which executes them from inside the container +where `localhost:9944` is reachable (the image ships `curl` for this purpose). The node must be running before you proceed. @@ -273,12 +273,12 @@ The node must be running before you proceed. # Replace , , with your values # Aura (Sr25519) -docker compose exec validator curl -s -H "Content-Type: application/json" \ +docker exec orbinum-validator curl -s -H "Content-Type: application/json" \ -d '{"id":1,"jsonrpc":"2.0","method":"author_insertKey","params":["aura","",""]}' \ http://localhost:9944 # GRANDPA (Ed25519) -docker compose exec validator curl -s -H "Content-Type: application/json" \ +docker exec orbinum-validator curl -s -H "Content-Type: application/json" \ -d '{"id":1,"jsonrpc":"2.0","method":"author_insertKey","params":["gran","",""]}' \ http://localhost:9944 ``` @@ -292,7 +292,7 @@ Each call returns `{"result":null}` on success. After inserting the Aura key, restart the node: ```bash -docker compose restart validator +docker compose restart orbinum-validator ``` The node reads the Aura key from the keystore, derives the EVM relay address, and prints it in the logs: @@ -322,7 +322,7 @@ First, get the combined session key from the node (same `docker compose exec` pattern as Step 4 — the RPC is only reachable inside the container): ```bash -docker compose exec validator curl -s -H "Content-Type: application/json" \ +docker exec orbinum-validator curl -s -H "Content-Type: application/json" \ -d '{"id":1,"jsonrpc":"2.0","method":"author_rotateKeys","params":[]}' \ http://localhost:9944 # Returns: {"result":"0x"} @@ -440,7 +440,7 @@ If you are still in the **pending** queue (not yet approved), you can also call ```bash # View validator logs -docker compose logs -f validator +docker compose logs -f orbinum-validator # View Watchtower logs (check when updates were applied) docker compose logs -f watchtower @@ -449,7 +449,7 @@ docker compose logs -f watchtower docker compose down # Restart validator only -docker compose restart validator +docker compose restart orbinum-validator # Force update now (without waiting for Watchtower) docker compose pull && docker compose up -d @@ -472,8 +472,8 @@ docker stats orbinum-validator **`author_insertKey` returns an error** - Make sure the node is running before calling the RPC - Port `9944` is not exposed to the host or network — it only listens on - `127.0.0.1` inside the container. Always call it via `docker compose exec - validator curl ... http://localhost:9944` (see Step 4), not from the host directly. + `127.0.0.1` inside the container. Always call it via `docker exec + orbinum-validator curl ... http://localhost:9944` (see Step 4), not from the host directly. **Node is not producing blocks after approval** - Verify all keys are inserted: restart and check logs for `Loaded session key` diff --git a/docker/testnet/rpc-node.md b/docker/testnet/rpc-node.md index 1657a3c4..28b7c130 100644 --- a/docker/testnet/rpc-node.md +++ b/docker/testnet/rpc-node.md @@ -50,7 +50,7 @@ a Let's Encrypt certificate for `RPC_DOMAIN` automatically — no Nginx or Certb ```bash docker compose -f docker-compose.rpc.yml up -d -docker compose -f docker-compose.rpc.yml logs -f rpc-node +docker compose -f docker-compose.rpc.yml logs -f orbinum-rpc-node ``` Wait until you see `Idle` or `Syncing` in the logs. On first start, check that Caddy @@ -62,16 +62,28 @@ docker compose -f docker-compose.rpc.yml logs caddy | grep -i "certificate obtai ## Verify -Use the domain you set in `RPC_DOMAIN` (replace below): +The public endpoint is locked down by Caddy: requests must send an `Origin` +header that matches `CADDY_ALLOWED_ORIGINS`, else 403. To test from anywhere, +pass an allowed origin: ```bash -curl -H "Content-Type: application/json" \ +curl -H "Origin: https://explorer.testnet.orbinum.network" \ + -H "Content-Type: application/json" \ -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ https://rpc-1.testnet.orbinum.io # Expected: {"jsonrpc":"2.0","result":{"isSyncing":false,"peers":3,...},"id":1} ``` +To bypass Caddy and hit the node's RPC directly (no Origin needed), run the +curl **inside the container** against `localhost:9944`: + +```bash +docker exec orbinum-rpc-node curl -s -H "Content-Type: application/json" \ + -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ + http://localhost:9944 +``` + The endpoint is ready to use as: - `https://rpc-1.testnet.orbinum.io` — HTTP RPC - `wss://rpc-1.testnet.orbinum.io` — WebSocket (Polkadot.js, Talisman) @@ -85,8 +97,8 @@ docker compose -f docker-compose.rpc.yml down # Full reset docker compose -f docker-compose.rpc.yml down -v -# Check sync status -curl -s -H "Content-Type: application/json" \ +# Check sync status (inside the container — no Origin needed) +docker exec orbinum-rpc-node curl -s -H "Content-Type: application/json" \ -d '{"id":1,"jsonrpc":"2.0","method":"system_syncState","params":[]}' \ - https://rpc-1.testnet.orbinum.io | jq + http://localhost:9944 ``` From d9fc12c666a0d3e84da12c9d71c785ee5b36062a Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 17 Jun 2026 00:06:48 -0400 Subject: [PATCH 06/13] fix: Caddy rewrites Host to a local host --- docker/testnet/Caddyfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docker/testnet/Caddyfile b/docker/testnet/Caddyfile index 5107f1cb..3193ae6f 100644 --- a/docker/testnet/Caddyfile +++ b/docker/testnet/Caddyfile @@ -9,5 +9,9 @@ @blocked not header_regexp Origin {$CADDY_ALLOWED_ORIGINS:^https://explorer\.testnet\.orbinum\.network$} respond @blocked "Forbidden" 403 - reverse_proxy localhost:9944 + # Substrate whitelists the Host header (only localhost by default). Rewrite it + # so the proxied request passes the node's host filter. + reverse_proxy localhost:9944 { + header_up Host localhost:9944 + } } \ No newline at end of file From ee4a0012f3b327907c43c1393567b1346e314022 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 17 Jun 2026 00:11:40 -0400 Subject: [PATCH 07/13] feat: Caddy now manage CORS --- docker/testnet/Caddyfile | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/docker/testnet/Caddyfile b/docker/testnet/Caddyfile index 3193ae6f..9825cb79 100644 --- a/docker/testnet/Caddyfile +++ b/docker/testnet/Caddyfile @@ -1,17 +1,28 @@ {$RPC_DOMAIN:rpc-1.testnet.orbinum.io} { - # Server-side Origin blocking (actual enforcement — also blocks curl/scripts - # that send a non-allowed Origin, and requests without an Origin header). - # CADDY_ALLOWED_ORIGINS is a REGEX that matches the Origin header. Anchor it with ^...$ - # and escape dots with \. - # single origin : ^https://explorer\.testnet\.orbinum\.network$ - # multiple : ^https://(explorer\.testnet\.orbinum\.network|app\.orbinum\.io)$ - # all origins : .* (disables the restriction) + # ── Origin allowlist (server-side, blocks curl/scripts and bad origins) ────── + # CADDY_ALLOWED_ORIGINS is a REGEX matched against the Origin header. Anchor it + # with ^...$ and escape dots with \. + # single : ^https://explorer\.testnet\.orbinum\.network$ + # multiple : ^https://(explorer\.testnet\.orbinum\.network|app\.orbinum\.io)$ + # all : .* (disables the restriction) @blocked not header_regexp Origin {$CADDY_ALLOWED_ORIGINS:^https://explorer\.testnet\.orbinum\.network$} respond @blocked "Forbidden" 403 + # ── CORS (Caddy answers it, not substrate) ─────────────────────────────────── + # Reflect the (already-allowlisted) Origin and allow the JSON-RPC content-type. + header { + Access-Control-Allow-Origin "{header.Origin}" + Access-Control-Allow-Methods "POST, OPTIONS" + Access-Control-Allow-Headers "Content-Type" + Vary Origin + } + # Answer the preflight directly — never forward OPTIONS to the node. + @preflight method OPTIONS + respond @preflight 204 + # Substrate whitelists the Host header (only localhost by default). Rewrite it # so the proxied request passes the node's host filter. reverse_proxy localhost:9944 { header_up Host localhost:9944 } -} \ No newline at end of file +} From 0984681181639c696fdf66f8425c2c5e236b8a40 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 17 Jun 2026 00:17:03 -0400 Subject: [PATCH 08/13] feat: header_down in reverse proxy --- docker/testnet/Caddyfile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docker/testnet/Caddyfile b/docker/testnet/Caddyfile index 9825cb79..e28e0eaf 100644 --- a/docker/testnet/Caddyfile +++ b/docker/testnet/Caddyfile @@ -3,12 +3,12 @@ # CADDY_ALLOWED_ORIGINS is a REGEX matched against the Origin header. Anchor it # with ^...$ and escape dots with \. # single : ^https://explorer\.testnet\.orbinum\.network$ - # multiple : ^https://(explorer\.testnet\.orbinum\.network|app\.orbinum\.io)$ + # multiple : ^https://(explorer\.testnet\.orbinum\.network|explorer-dev\.testnet\.orbinum\.network)$ # all : .* (disables the restriction) - @blocked not header_regexp Origin {$CADDY_ALLOWED_ORIGINS:^https://explorer\.testnet\.orbinum\.network$} + @blocked not header_regexp Origin {$CADDY_ALLOWED_ORIGINS:^https://(explorer\.testnet\.orbinum\.network|explorer-dev\.testnet\.orbinum\.network)$} respond @blocked "Forbidden" 403 - # ── CORS (Caddy answers it, not substrate) ─────────────────────────────────── + # ── CORS (Caddy owns it) ───────────────────────────────────────────────────── # Reflect the (already-allowlisted) Origin and allow the JSON-RPC content-type. header { Access-Control-Allow-Origin "{header.Origin}" @@ -20,9 +20,11 @@ @preflight method OPTIONS respond @preflight 204 - # Substrate whitelists the Host header (only localhost by default). Rewrite it - # so the proxied request passes the node's host filter. + # Substrate whitelists Host (localhost only) and also emits its own CORS header. + # Rewrite Host so the host filter passes, and strip the upstream CORS header so + # it doesn't duplicate the one Caddy sets above. reverse_proxy localhost:9944 { header_up Host localhost:9944 + header_down -Access-Control-Allow-Origin } } From 3f3e9569569fc52e7d7f293be4da3003bc948859 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 17 Jun 2026 02:31:16 -0400 Subject: [PATCH 09/13] refactor: remove CORS tests --- docker/testnet/.env.rpc.example | 16 ---------------- docker/testnet/Caddyfile | 19 +++++-------------- docker/testnet/docker-compose.rpc.yml | 3 +-- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/docker/testnet/.env.rpc.example b/docker/testnet/.env.rpc.example index 5cd455ee..804122f9 100644 --- a/docker/testnet/.env.rpc.example +++ b/docker/testnet/.env.rpc.example @@ -20,19 +20,3 @@ RPC_NODE_KEY= # proxies HTTPS/WSS to localhost:9944. Must already have a DNS A record # pointing to this VPS's public IP. Use rpc-1 / rpc-2 / ... per node. RPC_DOMAIN=rpc-1.testnet.orbinum.io - -# ── Control de acceso al RPC (dos capas) ────────────────────────────────────── - -# Capa 1 — CORS de substrate (--rpc-cors). Client-side (lo respeta el browser). -# Formato: lista separada por comas, o "all". -# un origin : https://explorer.testnet.orbinum.network -# varios : https://explorer.testnet.orbinum.network,https://app.orbinum.io -# todos : all -ALLOWED_ORIGINS=https://explorer.testnet.orbinum.network,https://explorer-dev.testnet.orbinum.network - -# Capa 2 — Bloqueo en Caddy. Server-side (real: bloquea curl/scripts y Origins no -# permitidos). REGEX sobre el header Origin. Anclar con ^...$ y escapar puntos \. -# un origin : ^https://explorer\.testnet\.orbinum\.network$ -# varios : ^https://(explorer\.testnet\.orbinum\.network|app\.orbinum\.io)$ -# todos : .* (desactiva el bloqueo) -CADDY_ALLOWED_ORIGINS=^https://(explorer\.testnet\.orbinum\.network|explorer-dev\.testnet\.orbinum\.network)$ \ No newline at end of file diff --git a/docker/testnet/Caddyfile b/docker/testnet/Caddyfile index e28e0eaf..a2b1bd34 100644 --- a/docker/testnet/Caddyfile +++ b/docker/testnet/Caddyfile @@ -1,26 +1,17 @@ {$RPC_DOMAIN:rpc-1.testnet.orbinum.io} { - # ── Origin allowlist (server-side, blocks curl/scripts and bad origins) ────── - # CADDY_ALLOWED_ORIGINS is a REGEX matched against the Origin header. Anchor it - # with ^...$ and escape dots with \. - # single : ^https://explorer\.testnet\.orbinum\.network$ - # multiple : ^https://(explorer\.testnet\.orbinum\.network|explorer-dev\.testnet\.orbinum\.network)$ - # all : .* (disables the restriction) - @blocked not header_regexp Origin {$CADDY_ALLOWED_ORIGINS:^https://(explorer\.testnet\.orbinum\.network|explorer-dev\.testnet\.orbinum\.network)$} - respond @blocked "Forbidden" 403 - - # ── CORS (Caddy owns it) ───────────────────────────────────────────────────── - # Reflect the (already-allowlisted) Origin and allow the JSON-RPC content-type. + # Public RPC: open CORS so MetaMask, wallets, dApps, indexers and 3rd-party + # explorers can all reach it. Access is restricted by --rpc-methods Safe on the + # node (no unsafe calls), not by Origin. header { - Access-Control-Allow-Origin "{header.Origin}" + Access-Control-Allow-Origin "*" Access-Control-Allow-Methods "POST, OPTIONS" Access-Control-Allow-Headers "Content-Type" - Vary Origin } # Answer the preflight directly — never forward OPTIONS to the node. @preflight method OPTIONS respond @preflight 204 - # Substrate whitelists Host (localhost only) and also emits its own CORS header. + # Substrate whitelists Host (localhost only) and emits its own CORS header. # Rewrite Host so the host filter passes, and strip the upstream CORS header so # it doesn't duplicate the one Caddy sets above. reverse_proxy localhost:9944 { diff --git a/docker/testnet/docker-compose.rpc.yml b/docker/testnet/docker-compose.rpc.yml index 811d5ab7..8c6ca206 100644 --- a/docker/testnet/docker-compose.rpc.yml +++ b/docker/testnet/docker-compose.rpc.yml @@ -14,7 +14,7 @@ services: --listen-addr /ip4/0.0.0.0/tcp/30333 --node-key ${RPC_NODE_KEY} --rpc-port 9944 - --rpc-cors ${ALLOWED_ORIGINS:-https://explorer.testnet.orbinum.network} + --rpc-cors all --rpc-external --rpc-methods Safe --rpc-max-connections 1000 @@ -44,7 +44,6 @@ services: restart: unless-stopped environment: RPC_DOMAIN: ${RPC_DOMAIN:-rpc-1.testnet.orbinum.io} - CADDY_ALLOWED_ORIGINS: ${CADDY_ALLOWED_ORIGINS:-^https://explorer\.testnet\.orbinum\.network$} volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data From 809877a400124a3263a48d0e8952ffc1fdb220e4 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 17 Jun 2026 02:43:55 -0400 Subject: [PATCH 10/13] refactor: feat(testnet): open RPC CORS, fix WS upgrade behind Caddy --- docker/testnet/Caddyfile | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/docker/testnet/Caddyfile b/docker/testnet/Caddyfile index a2b1bd34..78dc994d 100644 --- a/docker/testnet/Caddyfile +++ b/docker/testnet/Caddyfile @@ -2,20 +2,24 @@ # Public RPC: open CORS so MetaMask, wallets, dApps, indexers and 3rd-party # explorers can all reach it. Access is restricted by --rpc-methods Safe on the # node (no unsafe calls), not by Origin. - header { - Access-Control-Allow-Origin "*" - Access-Control-Allow-Methods "POST, OPTIONS" - Access-Control-Allow-Headers "Content-Type" - } - # Answer the preflight directly — never forward OPTIONS to the node. + + # Preflight: answer OPTIONS directly with CORS headers, never hit the node. @preflight method OPTIONS - respond @preflight 204 + handle @preflight { + header Access-Control-Allow-Origin "*" + header Access-Control-Allow-Methods "POST, OPTIONS" + header Access-Control-Allow-Headers "Content-Type" + respond 204 + } - # Substrate whitelists Host (localhost only) and emits its own CORS header. - # Rewrite Host so the host filter passes, and strip the upstream CORS header so - # it doesn't duplicate the one Caddy sets above. - reverse_proxy localhost:9944 { - header_up Host localhost:9944 - header_down -Access-Control-Allow-Origin + # Everything else (HTTP POST + WebSocket upgrade) goes to the node. + # CORS header added here applies to POST responses; the WS upgrade (101) passes + # through untouched so the handshake isn't broken. + handle { + header Access-Control-Allow-Origin "*" + reverse_proxy localhost:9944 { + header_up Host localhost:9944 + header_down -Access-Control-Allow-Origin + } } } From d49a8d3188a7a2b59d7fd264a912446e9201c482 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 17 Jun 2026 03:03:43 -0400 Subject: [PATCH 11/13] fix(testnet): preserve WS upgrade in Caddy, open RPC CORS --- docker/testnet/Caddyfile | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/docker/testnet/Caddyfile b/docker/testnet/Caddyfile index 78dc994d..3bd2ddd4 100644 --- a/docker/testnet/Caddyfile +++ b/docker/testnet/Caddyfile @@ -1,25 +1,21 @@ {$RPC_DOMAIN:rpc-1.testnet.orbinum.io} { # Public RPC: open CORS so MetaMask, wallets, dApps, indexers and 3rd-party # explorers can all reach it. Access is restricted by --rpc-methods Safe on the - # node (no unsafe calls), not by Origin. + # node, not by Origin. - # Preflight: answer OPTIONS directly with CORS headers, never hit the node. - @preflight method OPTIONS - handle @preflight { - header Access-Control-Allow-Origin "*" - header Access-Control-Allow-Methods "POST, OPTIONS" - header Access-Control-Allow-Headers "Content-Type" - respond 204 - } + # Answer the CORS preflight directly; never forward OPTIONS to the node. + @options method OPTIONS + respond @options 204 + + # CORS for the actual POST responses. WS upgrade (101) ignores these. + header Access-Control-Allow-Origin "*" + header Access-Control-Allow-Methods "POST, OPTIONS" + header Access-Control-Allow-Headers "Content-Type" - # Everything else (HTTP POST + WebSocket upgrade) goes to the node. - # CORS header added here applies to POST responses; the WS upgrade (101) passes - # through untouched so the handshake isn't broken. - handle { - header Access-Control-Allow-Origin "*" - reverse_proxy localhost:9944 { - header_up Host localhost:9944 - header_down -Access-Control-Allow-Origin - } + # Proxy HTTP POST and the WebSocket upgrade to the node. header_up Host fixes the + # node's Host whitelist; strip its CORS header so it doesn't duplicate ours. + reverse_proxy localhost:9944 { + header_up Host localhost:9944 + header_down -Access-Control-Allow-Origin } } From 7fa2590bf00fb00e2157510d2d1bd113560e8ad3 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 17 Jun 2026 03:08:38 -0400 Subject: [PATCH 12/13] chore: restore to only reverse proxy --- docker/testnet/Caddyfile | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/docker/testnet/Caddyfile b/docker/testnet/Caddyfile index 3bd2ddd4..85c96cf8 100644 --- a/docker/testnet/Caddyfile +++ b/docker/testnet/Caddyfile @@ -1,21 +1,3 @@ {$RPC_DOMAIN:rpc-1.testnet.orbinum.io} { - # Public RPC: open CORS so MetaMask, wallets, dApps, indexers and 3rd-party - # explorers can all reach it. Access is restricted by --rpc-methods Safe on the - # node, not by Origin. - - # Answer the CORS preflight directly; never forward OPTIONS to the node. - @options method OPTIONS - respond @options 204 - - # CORS for the actual POST responses. WS upgrade (101) ignores these. - header Access-Control-Allow-Origin "*" - header Access-Control-Allow-Methods "POST, OPTIONS" - header Access-Control-Allow-Headers "Content-Type" - - # Proxy HTTP POST and the WebSocket upgrade to the node. header_up Host fixes the - # node's Host whitelist; strip its CORS header so it doesn't duplicate ours. - reverse_proxy localhost:9944 { - header_up Host localhost:9944 - header_down -Access-Control-Allow-Origin - } + reverse_proxy localhost:9944 } From 924a5e54d7203fa56dd3df85d2935841c9486189 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 17 Jun 2026 15:28:15 -0400 Subject: [PATCH 13/13] chore: verify session keys --- docker/testnet/README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docker/testnet/README.md b/docker/testnet/README.md index f013fdec..ab0662d5 100644 --- a/docker/testnet/README.md +++ b/docker/testnet/README.md @@ -285,6 +285,33 @@ docker exec orbinum-validator curl -s -H "Content-Type: application/json" \ Each call returns `{"result":null}` on success. +**Verify the keys were inserted.** Two files must exist in the keystore — one for +aura, one for grandpa: + +```bash +docker exec orbinum-validator ls /data/chains/orbinum_testnet/keystore/ +# expect 2 files (the 8-hex prefix is the key type: 61757261=aura, 6772616e=gran) +``` + +You can also ask the node directly whether it holds each public key: + +```bash +# Aura — replace +docker exec orbinum-validator curl -s -H "Content-Type: application/json" \ + -d '{"id":1,"jsonrpc":"2.0","method":"author_hasKey","params":["","aura"]}' \ + http://localhost:9944 + +# GRANDPA — replace +docker exec orbinum-validator curl -s -H "Content-Type: application/json" \ + -d '{"id":1,"jsonrpc":"2.0","method":"author_hasKey","params":["","gran"]}' \ + http://localhost:9944 +``` + +Both must return `{"result":true}`. If either is `false`, the seed and the public +key don't match — re-check them before continuing (GRANDPA finalization needs ≥2 of +3 validators holding their `gran` key, or the chain produces blocks but never +finalizes). + --- ### 5. Get Your EVM Relay Address