test(lifecycle): probe electrumx with the protocol, not a bare socket

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) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-09 05:01:20 -04:00
co-authored by Claude Opus 5
parent 6c41852c56
commit 791e6e5ed9
2 changed files with 46 additions and 7 deletions
+21 -3
View File
@@ -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 ]
}
+25 -4
View File
@@ -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 ]
}