From 791e6e5ed993aa53dd7d2fe618940294be137770 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 9 Aug 2026 05:01:20 -0400 Subject: [PATCH] test(lifecycle): probe electrumx with the protocol, not a bare socket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both electrumx tests did socket.create_connection(("127.0.0.1", 50001)) and called it proof the service was up. It is not: that is the HOST-published port, and podman's port forwarder completes the TCP handshake even when nothing inside the container is listening. The cost was real. Both tests were green while mempool-api sat in a hard ECONNREFUSED loop against electrumx:50001 from inside archy-net — the operator's "mempool doesn't connect to bitcoin". Measured side by side: 127.0.0.1:50001 from the host OPEN, electrumx:50001 from a sibling container REFUSED. The cause is not a misconfiguration — mempool-api's CORE_RPC_HOST, ELECTRUM_HOST and ports are all correct. electrumx has not finished its initial sync (DB height 959,774 vs chain tip 961,706, ~1,932 blocks, 8d 14h elapsed) and ElectrumX does not serve sessions until it has caught up. Now both tests do a server.version round-trip and require a JSON result, so a forwarder that accepts-and-drops fails. Verified against the live node: the new probe FAILS today where the old one passed. Co-Authored-By: Claude Opus 5 (1M context) --- tests/lifecycle/bats/electrumx.bats | 24 +++++++++++++++++--- tests/lifecycle/bats/required-stack.bats | 29 ++++++++++++++++++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/tests/lifecycle/bats/electrumx.bats b/tests/lifecycle/bats/electrumx.bats index b3591cce..98ff38e6 100644 --- a/tests/lifecycle/bats/electrumx.bats +++ b/tests/lifecycle/bats/electrumx.bats @@ -53,11 +53,29 @@ teardown_file() { fi # Same probe required-stack.bats uses — divergence flags a real regression. + # It is a real Electrum round-trip, not a bare connect(): podman's port + # forwarder accepts the TCP handshake on the host-published port even when + # nothing inside the container is serving, which kept this test green for + # days while mempool-api could not reach electrumx at all. See the longer + # note in required-stack.bats. run python3 - <<'PY' -import socket -s = socket.create_connection(("127.0.0.1", 50001), 3) +import json, socket +s = socket.create_connection(("127.0.0.1", 50001), 5) +s.settimeout(10) +s.sendall((json.dumps({"id": 0, "method": "server.version", + "params": ["archy-gate", "1.4"]}) + "\n").encode()) +buf = b"" +while b"\n" not in buf: + chunk = s.recv(4096) + if not chunk: + raise SystemExit("electrumx closed the connection without replying " + "— listening but not serving (still syncing?)") + buf += chunk s.close() -print("ok") +resp = json.loads(buf.split(b"\n")[0]) +if "result" not in resp: + raise SystemExit(f"electrumx returned no result: {resp}") +print("ok", resp["result"]) PY [ "$status" -eq 0 ] } diff --git a/tests/lifecycle/bats/required-stack.bats b/tests/lifecycle/bats/required-stack.bats index 9fa04a86..a52662f4 100644 --- a/tests/lifecycle/bats/required-stack.bats +++ b/tests/lifecycle/bats/required-stack.bats @@ -133,13 +133,34 @@ bitcoin_json() { fi } -@test "electrumx TCP port accepts connections" { +@test "electrumx answers the Electrum protocol (not just an open socket)" { skip_if_not_installed electrumx + # A bare connect() to the HOST-published port proves nothing: podman's port + # forwarder accepts the TCP handshake even when nothing inside the container + # is listening. On 2026-08-09 this test was green while mempool-api was in a + # hard ECONNREFUSED loop against electrumx:50001 from inside archy-net — + # electrumx was still doing its initial sync (DB height 959,774 vs chain tip + # 961,706) and ElectrumX does not serve sessions until it has caught up. + # So do a real protocol round-trip: the forwarder cannot fake a reply. run python3 - <<'PY' -import socket -s = socket.create_connection(("127.0.0.1", 50001), 3) +import json, socket +s = socket.create_connection(("127.0.0.1", 50001), 5) +s.settimeout(10) +req = json.dumps({"id": 0, "method": "server.version", + "params": ["archy-gate", "1.4"]}) + "\n" +s.sendall(req.encode()) +buf = b"" +while b"\n" not in buf: + chunk = s.recv(4096) + if not chunk: + raise SystemExit("electrumx closed the connection without replying " + "— it is listening but not serving (still syncing?)") + buf += chunk s.close() -print("ok") +resp = json.loads(buf.split(b"\n")[0]) +if "result" not in resp: + raise SystemExit(f"electrumx returned no result: {resp}") +print("ok", resp["result"]) PY [ "$status" -eq 0 ] }