From 38765bcc120b185a2eadca8d24e3ea1ac5a39e37 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:41:09 +0700 Subject: [PATCH 1/3] fix(testnet): mount ENSv2 manifest outside read-only code --- .../tests/test_validate_testnet_bens.py | 12 ++++++++++ .github/workflows/dependency-build.yml | 24 +++++++++++++++++++ docker-compose/docker-compose-testnet.yml | 4 +++- .../plans/2026-08-12-ensv2-bens-testnet.md | 2 ++ scripts/validate-testnet-bens.py | 3 ++- 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.github/scripts/tests/test_validate_testnet_bens.py b/.github/scripts/tests/test_validate_testnet_bens.py index db08781fab..df46b3570a 100644 --- a/.github/scripts/tests/test_validate_testnet_bens.py +++ b/.github/scripts/tests/test_validate_testnet_bens.py @@ -37,6 +37,18 @@ def setUp(self): def test_repository_configuration_is_valid(self): self.assertEqual([], self.module.validate()) + def test_adapter_manifest_mount_is_outside_read_only_code_directory(self): + compose = (ROOT / "docker-compose" / "docker-compose-testnet.yml").read_text( + encoding="utf-8" + ) + adapter = compose.split(" bens-ensv2-adapter:", 1)[1].split( + " bens-deployer:", 1 + )[0] + self.assertIn("./bens/subgraph/adapter:/app:ro", adapter) + self.assertIn("./bens/deployment.json:/deployment.json:ro", adapter) + self.assertIn("DOS_NAMES_DEPLOYMENT: /deployment.json", adapter) + self.assertNotIn(":/app/deployment.json:ro", adapter) + def test_validator_rejects_public_http_routing_for_internal_bens_history(self): template = json.loads(self.module.BENS_TEMPLATE.read_text(encoding="utf-8")) template["subgraphs_reader"]["networks"]["3939"]["blockscout"]["url"] = ( diff --git a/.github/workflows/dependency-build.yml b/.github/workflows/dependency-build.yml index 0473e87f53..654295ec53 100644 --- a/.github/workflows/dependency-build.yml +++ b/.github/workflows/dependency-build.yml @@ -108,6 +108,30 @@ jobs: node --test "${checkout}/subgraph/dos-names/adapter/server.test.mjs" cp "${checkout}/contracts/deployments/dos-testnet-3939.json" \ docker-compose/bens/deployment.json + # Exercise real read-only mounts and the production entrypoint, not just module tests. + docker run --rm --init --network none --user 1000:1000 --read-only \ + --cap-drop ALL --security-opt no-new-privileges:true --memory 128m --cpus 0.5 \ + --platform linux/amd64 \ + --mount "type=bind,src=${checkout}/subgraph/dos-names/adapter,dst=/app,readonly" \ + --mount "type=bind,src=${PWD}/docker-compose/bens/deployment.json,dst=/deployment.json,readonly" \ + --env DOS_NAMES_DEPLOYMENT=/deployment.json \ + node:22.18.0-alpine3.22@sha256:1b2479dd35a99687d6638f5976fd235e26c5b37e8122f786fcd5fe231d63de5b \ + node -e ' + const { spawn } = require("node:child_process"); + const child = spawn(process.execPath, ["/app/server.mjs"], { stdio: ["ignore", "pipe", "inherit"] }); + let output = ""; + let started = false; + const deadline = setTimeout(() => child.kill("SIGKILL"), 5000); + child.stdout.on("data", (chunk) => { + output += chunk; + if (output.includes("DOS Names Testnet metadata adapter listening")) { + started = true; + child.kill("SIGTERM"); + } + }); + child.on("error", () => { process.exitCode = 1; }); + child.on("close", () => { clearTimeout(deadline); process.exitCode = started ? 0 : 1; }); + ' node "${checkout}/subgraph/dos-names/scripts/render-manifest.mjs" \ docker-compose/bens/deployment.json \ "${checkout}/subgraph/dos-names/subgraph.template.yaml" \ diff --git a/docker-compose/docker-compose-testnet.yml b/docker-compose/docker-compose-testnet.yml index 039354ae65..11867d1507 100644 --- a/docker-compose/docker-compose-testnet.yml +++ b/docker-compose/docker-compose-testnet.yml @@ -129,6 +129,8 @@ services: - no-new-privileges:true mem_limit: 128m cpus: 0.5 + environment: + DOS_NAMES_DEPLOYMENT: /deployment.json depends_on: bens: condition: service_started @@ -136,7 +138,7 @@ services: condition: service_started volumes: - ./bens/subgraph/adapter:/app:ro - - ./bens/deployment.json:/app/deployment.json:ro + - ./bens/deployment.json:/deployment.json:ro command: ["node", "/app/server.mjs"] healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8050/health').then(r=>r.json()).then(r=>process.exit(r.status==='SERVING'?0:1)).catch(()=>process.exit(1))"] diff --git a/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md b/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md index a7a00419d4..dc8286f1df 100644 --- a/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md +++ b/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md @@ -33,6 +33,8 @@ - Source adapter accepted: DOS Names PR #11 merged as `2d47fb77bb09dcbdc9d7bfeee77d12ddbebc2e4d`, with final-head independent review and successful required CI. Adapter tests cover canonical/retired tokens, subdomains, regeneration, invalid state and client-disconnect cancellation. - Testnet fixture renewal accepted: `0xce800d5b23a7eb1a0cf7318585af76b02e0e93e0b7ce468038ff941732c1094c` succeeded at block 299. `policy-smoke.dos` expiry changed from 1824131273 to 1855667273 (+365 days); token ID and owner were unchanged. Positive payment transfer, paidExpiry and post-renewal nonce replay rejection were verified. BENS history includes the claim and renewal receipts, and its expiry is `2028-10-20T15:07:53Z`. - Negative policy simulations passed for WrongWallet, InvalidVoucher, VoucherExpired, PriceExceedsVoucher, NonceAlreadyUsed and UnauthorizedRegistrationController. Only existing Testnet fixture keys were used in memory; no keys/signatures were logged or persisted. These checks do not prove DOS.Me authentication/score admission for a real user. +- First runtime attempt: DOScan PR #273 merged as `b1686490b0a454c2b66d2d5cb9ffb9a9cee4a99d`. Deploy Config run `40197157966` failed because Docker could not create `/app/deployment.json` inside the read-only `/app` code bind mount. The rollback stopped at its database-writer container-state gate before any database drop/restore. Prior public configuration and services were recovered using the existing persistent volumes; Backend became healthy, BENS returned SERVING, and the public explorer read block 300. No database restore or Mainnet action was performed. +- Runtime correction: mount the manifest separately at `/deployment.json` and select it with `DOS_NAMES_DEPLOYMENT`. Keep the non-root/read-only/capability restrictions. A new regression reproduces the old layout failure, and CI now starts the production entrypoint with actual read-only Docker mounts. The separate container-removal diagnostic remains a follow-up; its safety gate is not weakened by this fix. --- diff --git a/scripts/validate-testnet-bens.py b/scripts/validate-testnet-bens.py index 60d493098e..e2c3b5f451 100644 --- a/scripts/validate-testnet-bens.py +++ b/scripts/validate-testnet-bens.py @@ -221,7 +221,8 @@ def validate() -> list[str]: if ( " bens-ensv2-adapter:" not in compose or "./bens/subgraph/adapter:/app:ro" not in compose - or "./bens/deployment.json:/app/deployment.json:ro" not in compose + or "./bens/deployment.json:/deployment.json:ro" not in compose + or "DOS_NAMES_DEPLOYMENT: /deployment.json" not in compose ): errors.append("Testnet must run the immutable DOS Names metadata adapter") From b3d58c9866565903b1070269e085ef01421eac94 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:52:07 +0700 Subject: [PATCH 2/3] fix(testnet): normalize public adapter artifact permissions --- .github/scripts/tests/test_validate_testnet_bens.py | 8 ++++++++ .github/workflows/dependency-build.yml | 3 +++ .github/workflows/deploy-config.yml | 3 +++ 3 files changed, 14 insertions(+) diff --git a/.github/scripts/tests/test_validate_testnet_bens.py b/.github/scripts/tests/test_validate_testnet_bens.py index df46b3570a..87526a722b 100644 --- a/.github/scripts/tests/test_validate_testnet_bens.py +++ b/.github/scripts/tests/test_validate_testnet_bens.py @@ -49,6 +49,14 @@ def test_adapter_manifest_mount_is_outside_read_only_code_directory(self): self.assertIn("DOS_NAMES_DEPLOYMENT: /deployment.json", adapter) self.assertNotIn(":/app/deployment.json:ro", adapter) + def test_non_root_adapter_artifacts_have_explicit_readable_modes(self): + workflow = (ROOT / ".github" / "workflows" / "deploy-config.yml").read_text( + encoding="utf-8" + ) + self.assertIn('sudo chmod 0755 "${DEPLOY_PATH}/bens/subgraph/adapter"', workflow) + self.assertIn('sudo chmod 0644 "${DEPLOY_PATH}/bens/subgraph/adapter/server.mjs"', workflow) + self.assertIn('"${DEPLOY_PATH}/bens/deployment.json"', workflow) + def test_validator_rejects_public_http_routing_for_internal_bens_history(self): template = json.loads(self.module.BENS_TEMPLATE.read_text(encoding="utf-8")) template["subgraphs_reader"]["networks"]["3939"]["blockscout"]["url"] = ( diff --git a/.github/workflows/dependency-build.yml b/.github/workflows/dependency-build.yml index 654295ec53..68cf310078 100644 --- a/.github/workflows/dependency-build.yml +++ b/.github/workflows/dependency-build.yml @@ -109,6 +109,9 @@ jobs: cp "${checkout}/contracts/deployments/dos-testnet-3939.json" \ docker-compose/bens/deployment.json # Exercise real read-only mounts and the production entrypoint, not just module tests. + chmod 0755 "${checkout}/subgraph/dos-names/adapter" + chmod 0644 "${checkout}/subgraph/dos-names/adapter/server.mjs" \ + docker-compose/bens/deployment.json docker run --rm --init --network none --user 1000:1000 --read-only \ --cap-drop ALL --security-opt no-new-privileges:true --memory 128m --cpus 0.5 \ --platform linux/amd64 \ diff --git a/.github/workflows/deploy-config.yml b/.github/workflows/deploy-config.yml index 6f21321a68..ae9ee5a60a 100644 --- a/.github/workflows/deploy-config.yml +++ b/.github/workflows/deploy-config.yml @@ -1532,6 +1532,9 @@ jobs: sudo cp -a "${SRC}/docker-compose/bens" "${DEPLOY_PATH}/bens" sudo chmod 0755 "${DEPLOY_PATH}/bens" sudo chmod 0644 "${DEPLOY_PATH}/bens/config.json" + sudo chmod 0755 "${DEPLOY_PATH}/bens/subgraph/adapter" + sudo chmod 0644 "${DEPLOY_PATH}/bens/subgraph/adapter/server.mjs" \ + "${DEPLOY_PATH}/bens/deployment.json" cd "${DEPLOY_PATH}" sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" docker compose config -q From bd9f5f8ca1be7bc9889215f44e9bf6de3d864afa Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:54:31 +0700 Subject: [PATCH 3/3] test(testnet): clarify adapter layout and startup coverage --- .github/scripts/tests/test_validate_testnet_bens.py | 10 +++++++--- .../superpowers/plans/2026-08-12-ensv2-bens-testnet.md | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/scripts/tests/test_validate_testnet_bens.py b/.github/scripts/tests/test_validate_testnet_bens.py index 87526a722b..c147ba7395 100644 --- a/.github/scripts/tests/test_validate_testnet_bens.py +++ b/.github/scripts/tests/test_validate_testnet_bens.py @@ -41,9 +41,13 @@ def test_adapter_manifest_mount_is_outside_read_only_code_directory(self): compose = (ROOT / "docker-compose" / "docker-compose-testnet.yml").read_text( encoding="utf-8" ) - adapter = compose.split(" bens-ensv2-adapter:", 1)[1].split( - " bens-deployer:", 1 - )[0] + match = re.search( + r"^ bens-ensv2-adapter:.*?(?=^ [a-zA-Z0-9_-]+:|\Z)", + compose, + re.DOTALL | re.MULTILINE, + ) + self.assertIsNotNone(match, "Missing Testnet adapter service") + adapter = match.group(0) self.assertIn("./bens/subgraph/adapter:/app:ro", adapter) self.assertIn("./bens/deployment.json:/deployment.json:ro", adapter) self.assertIn("DOS_NAMES_DEPLOYMENT: /deployment.json", adapter) diff --git a/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md b/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md index dc8286f1df..38b2434f1d 100644 --- a/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md +++ b/docs/superpowers/plans/2026-08-12-ensv2-bens-testnet.md @@ -34,7 +34,7 @@ - Testnet fixture renewal accepted: `0xce800d5b23a7eb1a0cf7318585af76b02e0e93e0b7ce468038ff941732c1094c` succeeded at block 299. `policy-smoke.dos` expiry changed from 1824131273 to 1855667273 (+365 days); token ID and owner were unchanged. Positive payment transfer, paidExpiry and post-renewal nonce replay rejection were verified. BENS history includes the claim and renewal receipts, and its expiry is `2028-10-20T15:07:53Z`. - Negative policy simulations passed for WrongWallet, InvalidVoucher, VoucherExpired, PriceExceedsVoucher, NonceAlreadyUsed and UnauthorizedRegistrationController. Only existing Testnet fixture keys were used in memory; no keys/signatures were logged or persisted. These checks do not prove DOS.Me authentication/score admission for a real user. - First runtime attempt: DOScan PR #273 merged as `b1686490b0a454c2b66d2d5cb9ffb9a9cee4a99d`. Deploy Config run `40197157966` failed because Docker could not create `/app/deployment.json` inside the read-only `/app` code bind mount. The rollback stopped at its database-writer container-state gate before any database drop/restore. Prior public configuration and services were recovered using the existing persistent volumes; Backend became healthy, BENS returned SERVING, and the public explorer read block 300. No database restore or Mainnet action was performed. -- Runtime correction: mount the manifest separately at `/deployment.json` and select it with `DOS_NAMES_DEPLOYMENT`. Keep the non-root/read-only/capability restrictions. A new regression reproduces the old layout failure, and CI now starts the production entrypoint with actual read-only Docker mounts. The separate container-removal diagnostic remains a follow-up; its safety gate is not weakened by this fix. +- Runtime correction: mount the manifest at `/deployment.json`, outside the read-only code directory, and select it with `DOS_NAMES_DEPLOYMENT`. The text regression enforces that corrected layout; the separate Docker CI probe verifies actual entrypoint startup. VM preflight also found public artifact modes of 0700/0600. Explicitly setting only the adapter directory to 0755 and its entrypoint/manifest to 0644 allowed the production entrypoint to start on the VM with UID 1000, read-only mounts/rootfs, dropped capabilities, resource limits and no network. Secret modes are unchanged. The separate container-removal diagnostic remains a follow-up; its safety gate is unchanged. ---