blaze-host: A/B the RPC routes a real FIFA session actually used
The gate 5-6 live run exercised 14 RPCs the recorded fixtures never covered -- Stats, Clubs, OSDKSettings, SponsoredEvents, Messaging::fetchMessages, Util::getTelemetryServer, Util::userSettingsLoadAll, UserSessions cmd 0x0008, and the transport PING -- every one taking the empty-reply fallback. That Python does the same was an inference from reading its dispatch table. This sends those exact routes to both backends and diffs the replies: 14/14 byte-identical. Worth keeping: the fixtures were built from what the responder implements, so they could never have covered what the client asks for and the responder does not. Only a live session reveals that surface, and this makes it checkable afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Send the routes a real FIFA session used to BOTH backends and diff the replies.
|
||||||
|
|
||||||
|
./route-ab.py [python-host:port] [rust-host:port]
|
||||||
|
|
||||||
|
WHY
|
||||||
|
|
||||||
|
The gate 5-6 live session exercised 14 RPCs the recorded fixtures never covered
|
||||||
|
-- Stats, Clubs, OSDKSettings, SponsoredEvents, Messaging::fetchMessages,
|
||||||
|
Util::getTelemetryServer, the transport PING -- and every one took the
|
||||||
|
empty-reply fallback. That Python does the same was an INFERENCE from reading
|
||||||
|
its dispatch table. This turns it into evidence, using the routes FIFA actually
|
||||||
|
sent.
|
||||||
|
|
||||||
|
None of these RPCs read their request body (they are unhandled on both sides),
|
||||||
|
so sending them with an empty payload exercises the same path the client did.
|
||||||
|
|
||||||
|
Read-only: opens a client connection to each backend and sends requests. All
|
||||||
|
routes here are unimplemented on both sides, so nothing mutates.
|
||||||
|
"""
|
||||||
|
import socket, struct, sys
|
||||||
|
|
||||||
|
# (component, command, msg_type) observed in the live FIFA session.
|
||||||
|
MESSAGE, PING = 0, 4
|
||||||
|
ROUTES = [
|
||||||
|
(0x0009, 0x0005, MESSAGE, "Util::getTelemetryServer"),
|
||||||
|
(0x0009, 0x000C, MESSAGE, "Util::userSettingsLoadAll"),
|
||||||
|
(0x0009, 0x001C, MESSAGE, "Util::setClientState"),
|
||||||
|
(0x7802, 0x0008, MESSAGE, "UserSessions::cmd:0x0008"),
|
||||||
|
(0x000F, 0x0002, MESSAGE, "Messaging::fetchMessages"),
|
||||||
|
(0x000B, 0x0640, MESSAGE, "Clubs::cmd:0x0640"),
|
||||||
|
(0x000B, 0x0A28, MESSAGE, "Clubs::cmd:0x0a28"),
|
||||||
|
(0x08C9, 0x0001, MESSAGE, "OSDKSettings::cmd:0x0001"),
|
||||||
|
(0x08C9, 0x0002, MESSAGE, "OSDKSettings::cmd:0x0002"),
|
||||||
|
(0x0007, 0x0003, MESSAGE, "Stats::cmd:0x0003"),
|
||||||
|
(0x0007, 0x000F, MESSAGE, "Stats::cmd:0x000f"),
|
||||||
|
(0x0007, 0x0014, MESSAGE, "Stats::cmd:0x0014"),
|
||||||
|
(0x081C, 0x0003, MESSAGE, "SponsoredEvents::cmd:0x0003"),
|
||||||
|
(0x0000, 0x0000, PING, "transport PING"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def frame(comp, cmd, num, mtype, payload=b""):
|
||||||
|
h = bytearray(16)
|
||||||
|
struct.pack_into(">I", h, 0, len(payload))
|
||||||
|
struct.pack_into(">H", h, 4, 0)
|
||||||
|
struct.pack_into(">H", h, 6, comp)
|
||||||
|
struct.pack_into(">H", h, 8, cmd)
|
||||||
|
h[10], h[11], h[12] = (num >> 16) & 0xFF, (num >> 8) & 0xFF, num & 0xFF
|
||||||
|
h[13] = (mtype & 7) << 5
|
||||||
|
return bytes(h) + payload
|
||||||
|
|
||||||
|
def ask(host, port, routes):
|
||||||
|
s = socket.create_connection((host, port), timeout=10)
|
||||||
|
s.settimeout(10)
|
||||||
|
out, buf = [], b""
|
||||||
|
for i, (comp, cmd, mtype, name) in enumerate(routes):
|
||||||
|
s.sendall(frame(comp, cmd, i + 1, mtype))
|
||||||
|
while len(buf) < 16:
|
||||||
|
d = s.recv(65536)
|
||||||
|
if not d:
|
||||||
|
out.append((name, "NO REPLY")); s.close(); return out
|
||||||
|
buf += d
|
||||||
|
total = 16 + struct.unpack_from(">H", buf, 4)[0] + struct.unpack_from(">I", buf, 0)[0]
|
||||||
|
while len(buf) < total:
|
||||||
|
buf += s.recv(65536)
|
||||||
|
out.append((name, buf[:total].hex()))
|
||||||
|
buf = buf[total:]
|
||||||
|
s.close()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def endpoint(arg, default_port):
|
||||||
|
if ":" in arg:
|
||||||
|
h, p = arg.rsplit(":", 1)
|
||||||
|
return h, int(p)
|
||||||
|
return arg, default_port
|
||||||
|
|
||||||
|
args = sys.argv[1:]
|
||||||
|
PY_HOST, PY_PORT = endpoint(args[0], 42130) if len(args) > 0 else ("127.0.0.1", 42130)
|
||||||
|
RS_HOST, RS_PORT = endpoint(args[1], 42230) if len(args) > 1 else ("127.0.0.1", 42230)
|
||||||
|
|
||||||
|
print("python %s:%d rust %s:%d" % (PY_HOST, PY_PORT, RS_HOST, RS_PORT))
|
||||||
|
py = ask(PY_HOST, PY_PORT, ROUTES)
|
||||||
|
rs = ask(RS_HOST, RS_PORT, ROUTES)
|
||||||
|
|
||||||
|
bad = 0
|
||||||
|
for (n1, a), (n2, b) in zip(py, rs):
|
||||||
|
same = (a == b)
|
||||||
|
if not same:
|
||||||
|
bad += 1
|
||||||
|
print(" %-32s %s" % (n1, "identical" if same else "DIFFERS\n py=%s\n rs=%s" % (a, b)))
|
||||||
|
print("\n%d/%d identical" % (len(ROUTES) - bad, len(ROUTES)))
|
||||||
|
sys.exit(1 if bad else 0)
|
||||||
Reference in New Issue
Block a user