# erldistpy roadmap Phases below are sized to land as discrete commits. Each phase ends with `make all` green and a real-VM interop test where applicable. ## Phase 0 — Repo bones ✅ - LICENSE, README, .gitignore, Makefile, pyproject.toml - Package skeleton, ruff config matches unfeed - `make bootstrap test lint` works on a fresh checkout ## Phase 1 — ETF codec ✅ - Subset of External Term Format we need for gen_call to Elixir: small_int, int, big_int, atom_utf8 (legacy atom_ext on decode), binary, nil, list, tuple (small + large), pid (new_pid), ref (newer_reference) - Booleans and `None` ride as atoms `true` / `false` / `nil` - Golden vectors decoded from real `term_to_binary/1` output - Round-trip tests for the encoder - Generator script committed at `docs/etf_vectors.erl` ## Phase 2 — EPMD client ✅ EPMD (Erlang Port Mapper Daemon) maps node names to TCP ports. - Synchronous TCP client - `PORT_PLEASE2_REQ` (122) → `EpmdInfo(name, port, node_type, protocol, hi_ver, lo_ver, extra)` - Returns `None` cleanly on unregistered node, raises `EpmdError` on socket failures - Tests: recorded byte streams from real EPMD + live integration that spawns its own `erl -sname` and tears it down ## Phase 3 — Distribution handshake - TCP connect to the resolved port - `send_name` / `recv_status` / `recv_challenge` / `send_challenge_reply` / `recv_challenge_ack` - Cookie digest via `erlang:phash2`-equivalent (md5-based per spec) - Version 6 ("v6") handshake, the modern one Elixir 1.15+ uses - Tests: handshake against a live Erlang node started in conftest ## Phase 4 — Distribution channel - After handshake, the socket carries control + payload messages framed by a 4-byte length prefix - Send `SEND_TT` / `REG_SEND` for outgoing messages - Receive replies, route by ref - Tick loop for keepalive (60s default per OTP) ## Phase 5 — gen_call convenience layer - `Node.call(name_or_pid, request, timeout=5.0)` → reply term - Wraps the `$gen_call` protocol used by `:gen_server` - Idempotency-key handling lives in the caller; we just pass the term ## Phase 6 — TLS dist - Wrap the post-EPMD socket in TLS - Match `inet_tls_dist` config on the Erlang side (cert + key + ca paths) - Same handshake, just runs inside the TLS tunnel ## Phase 7 — unfeed integration - `ErlangDistTransport` implementing unfeed's `WalletTransport` Protocol - Maps `/v1/health`, `/v1/address`, ... to gen_call requests - `unfeed` deploys with a config switch: `wallet_transport = http | erldist` - Cutover after a soak period on staging