erldistpy/docs/ROADMAP.md
russell@unturf.com b9fd28ca3b
phase 0 + 1: repo bones and ETF codec
Repo scaffolding (LICENSE, Makefile, pyproject.toml, README) matching
unfeed conventions. Flat package layout, ruff config, Unlicense.

ETF codec covers the subset needed for gen_call against an Elixir node:
small/int/big_int, atom_utf8 (legacy atom_ext on decode), binary, nil,
list, small/large tuple, new_pid, newer_reference. Booleans round-trip
as atoms true/false; Python None as atom nil; str encodes to utf-8
binary to match Elixir convention.

Golden vectors were generated from real Erlang term_to_binary/1 output
(generator script at docs/etf_vectors.erl). Decode tests verify wire
compatibility; round-trip tests verify encoder consistency.

make all green: 58 passed, lint clean.

Next phases tracked in docs/ROADMAP.md (EPMD, handshake, channel,
gen_call, TLS, unfeed integration).
2026-06-16 10:39:01 -04:00

2.5 KiB

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. Tiny synchronous TCP client.

  • Connect epmd_host:4369 (configurable)
  • PORT_PLEASE2_REQ (113) → port + dist version range
  • Return EpmdNodeInfo(port, lo_ver, hi_ver, node_type, proto)
  • Tests: live EPMD on localhost (skip if not running), unit tests against a recorded byte stream

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