From f059ff8d753448acf8a29819f037747eb94ccfb1 Mon Sep 17 00:00:00 2001 From: Kewe63 <86300262+Kewe63@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:06:47 +0300 Subject: [PATCH] fix: enable the published op-geth WebSocket endpoint --- README.md | 5 ++ scripts/start-op-geth.sh | 3 + tests/test_op_geth_websocket.py | 98 +++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 tests/test_op_geth_websocket.py diff --git a/README.md b/README.md index 9352333..ffcd581 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,11 @@ Execution RPC for the current `op-geth` service: curl -fsS -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' http://127.0.0.1:9993 ``` +The execution WebSocket RPC is available at `ws://127.0.0.1:9994` on the +Docker host. `PORT__OP_GETH_WS` changes that published host port; the container +listens on `8546`. This endpoint uses plain WebSocket (`ws://`), not TLS +(`wss://`). The client's default WebSocket API and origin policies are retained. + Rollup node RPC: ```sh diff --git a/scripts/start-op-geth.sh b/scripts/start-op-geth.sh index e18145b..f6ed1c7 100755 --- a/scripts/start-op-geth.sh +++ b/scripts/start-op-geth.sh @@ -22,6 +22,9 @@ exec geth \ --http.addr=0.0.0.0 \ --http.port=8545 \ --http.api=eth,engine,web3,debug,net \ + --ws \ + --ws.addr=0.0.0.0 \ + --ws.port=8546 \ --metrics \ --metrics.influxdb \ --metrics.influxdb.endpoint=http://influxdb:8086 \ diff --git a/tests/test_op_geth_websocket.py b/tests/test_op_geth_websocket.py new file mode 100644 index 0000000..04a2dc7 --- /dev/null +++ b/tests/test_op_geth_websocket.py @@ -0,0 +1,98 @@ +"""Offline wrapper regression tests (Linux user/network namespaces required). + +Run: python3 -m unittest discover -s tests -v +The real entrypoint runs unchanged under /bin/sh in a disposable chroot. +Only geth is stubbed: these tests verify argv, not binary flag support. +No Docker, RPC access, existing datadir, or host /shared directory is used. +""" +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + + +REPO = Path(__file__).resolve().parents[1] + + +class OpGethWebSocketTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + def require_tool(name): + tool = shutil.which(name) + if tool is None: + raise unittest.SkipTest("Required Linux tool unavailable: " + name) + return tool + + cls.unshare = require_tool("unshare") + cls.chroot = require_tool("chroot") + cls.ldd = require_tool("ldd") + probe = subprocess.run( + [cls.unshare, "--user", "--map-root-user", "--net", "/bin/true"], + capture_output=True, text=True, timeout=10, + ) + if probe.returncode: + raise unittest.SkipTest("User/network namespaces unavailable: " + probe.stderr) + + def wrapper_args(self, node_type="full", extra_args=()): + with tempfile.TemporaryDirectory(prefix="op-geth-wrapper-test-") as tmp: + root = Path(tmp) + for directory in ("bin", "scripts", "shared", "geth"): + (root / directory).mkdir() + shutil.copyfile("/bin/sh", root / "bin/sh") + (root / "bin/sh").chmod(0o755) + # Copy the host shell's loader and libraries, not host configuration. + deps = subprocess.run( + [self.ldd, "/bin/sh"], check=True, capture_output=True, text=True, + ) + for dependency in set(re.findall(r"/[^\s()]+", deps.stdout)): + dest = root / dependency.lstrip("/") + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(dependency, dest) + dest.chmod(0o755) + shutil.copyfile(REPO / "scripts/start-op-geth.sh", root / "scripts/start-op-geth.sh") + (root / "shared/initialized.txt").touch() + stub = root / "bin/geth" + stub.write_text("#!/bin/sh\nprintf '%s\\n' \"$@\"\n") + stub.chmod(0o755) + env = { + "PATH": "/bin", "LC_ALL": "C", "NETWORK_NAME": "ink-sepolia", + "NODE_TYPE": node_type, "BEDROCK_DATADIR": "/geth", + "BEDROCK_SEQUENCER_HTTP": "http://sequencer.invalid", + "PORT__OP_GETH_P2P": "49393", "EXTENDED_ARG": "", + "OVERRIDE_HOLOCENE": "", + } + result = subprocess.run( + [self.unshare, "--user", "--map-root-user", "--net", self.chroot, + str(root), "/bin/sh", "/scripts/start-op-geth.sh", *extra_args], + env=env, cwd="/", capture_output=True, text=True, timeout=10, + ) + self.assertEqual(result.returncode, 0, result.stderr) + return result.stdout.splitlines()[1:] # Skip the init-wait message. + + def assert_websocket_listener(self, args): + self.assertIn("--ws", args) + self.assertIn("--ws.addr=0.0.0.0", args) + self.assertIn("--ws.port=8546", args) + + def test_full_node_enables_published_websocket_listener(self): + args = self.wrapper_args() + self.assert_websocket_listener(args) + self.assertIn("--http.port=8545", args) + self.assertIn("--gcmode=full", args) + + def test_archive_node_enables_same_websocket_listener(self): + args = self.wrapper_args(node_type="archive") + self.assert_websocket_listener(args) + self.assertIn("--gcmode=archive", args) + self.assertIn("--port=49393", args) + + def test_explicit_arguments_remain_after_wrapper_defaults(self): + args = self.wrapper_args(extra_args=("--ws=false",)) + self.assert_websocket_listener(args) + self.assertEqual(args[-1], "--ws=false") + + +if __name__ == "__main__": + unittest.main()