Find a file
russell@unturf.com d86ef8530f
0.1.7: declare DFLAG_MANDATORY_25_DIGEST + decode dist-header replies
Against real-world OTP 26 peers the v6 handshake "succeeded" but the
first REG_SEND silently dropped on the peer side — peer accepted the
connection then closed the link with no bytes when we tried to call a
registered process. Hit during MPS↔portal wallet RPC smoke test.

Root cause: OTP 25+ requires DFLAG_MANDATORY_25_DIGEST to be present
in our advertised flag set. The digest is the hash of the OTP-25
mandatory flag set; without it the peer's dist driver loses confidence
in the negotiation and drops messages from us without surfacing an
error.

Adds DFLAG_MANDATORY_25_DIGEST to DEFAULT_FLAGS. Also extends
_decode_message to accept both legacy pass-through (0x70 ...) and
dist-header framing (0x83 0x44 0x00 ...) on receive — modern OTP may
send dist-headed messages even when we didn't negotiate
DFLAG_DIST_HDR_ATOM_CACHE. Fragments (0x83 0x45 / 0x83 0x46) still
TODO; we surface a clear ChannelError instead of silent corruption.

Send side still uses pass-through framing — we don't yet implement
the atom-cache encode/decode that DFLAG_DIST_HDR_ATOM_CACHE would
require. Peer routes our pass-through sends without issue.

122/122 tests pass including live integration against a local Erlang
node and the TLS dist suite.
2026-06-17 08:51:09 -04:00
docs ci: classic auth via group-scoped TWINE_USERNAME/TWINE_PASSWORD, v0.1.4 2026-06-16 15:01:23 -04:00
erldistpy 0.1.7: declare DFLAG_MANDATORY_25_DIGEST + decode dist-header replies 2026-06-17 08:51:09 -04:00
tests 0.1.7: declare DFLAG_MANDATORY_25_DIGEST + decode dist-header replies 2026-06-17 08:51:09 -04:00
.gitignore phase 6: TLS dist via inet_tls_dist 2026-06-16 12:06:07 -04:00
.gitlab-ci.yml ci: modern twine, classic auth via group vars, v0.1.6 2026-06-16 15:18:54 -04:00
LICENSE phase 0 + 1: repo bones and ETF codec 2026-06-16 10:39:01 -04:00
Makefile packaging: ship to PyPI via tag + GitLab CI (mirrors ago's pattern) 2026-06-16 13:45:08 -04:00
pyproject.toml 0.1.7: declare DFLAG_MANDATORY_25_DIGEST + decode dist-header replies 2026-06-17 08:51:09 -04:00
README.md packaging: ship to PyPI via tag + GitLab CI (mirrors ago's pattern) 2026-06-16 13:45:08 -04:00

erldistpy

Native Python client for our Erlang distribution protocol. Talk Erlang/Elixir nodes from CPython without HTTP shim layers.

Built to swap into Python web apps as a drop-in for HTTP wallet-bridge clients (see unfeed's WalletTransport and make_post_sell's crypto watcher) so they can call Elixir gen_server processes over native Erlang dist instead of JSON-RPC or HTTPS. Same call semantics, lower latency, fewer moving parts.

Install

pip install erldistpy

Quick start

from erldistpy import Node, Atom

with Node(
    our_name="myapp@host",
    peer_name="wallet",                # short EPMD name
    peer_host="wallet.example.com",
    cookie="SHARED_COOKIE",            # read from a file path, never inline
) as node:
    reply = node.call(
        "Elixir.Wallet.Service",
        (Atom("monero"), Atom("get_height"), []),
        timeout=5.0,
    )
    # reply is whatever the gen_server returned — atoms / binaries /
    # tuples / maps / lists / pids / refs decode to Python natives.

For TLS dist (Erlang inet_tls_dist):

from erldistpy import Node, make_dist_tls_context

ctx = make_dist_tls_context(
    cert="/etc/myapp/client.pem",
    key="/etc/myapp/client.key",
    ca="/etc/myapp/ca.pem",
)
node = Node(our_name=..., peer_name=..., cookie=..., tls_context=ctx)

Scope

  • ETF (External Term Format) codec — encode/decode Erlang terms
  • EPMD client — node name → port lookup
  • v6 distribution handshake — MD5 cookie auth, version negotiation
  • gen_call to registered processes on a remote node
  • TLS dist support (Erlang inet_tls_dist)

Out of scope: full Erlang node impersonation, link/monitor lifecycles, distributed Mnesia. We are a client, not a peer node.

Why not Pyrlang?

Pyrlang implements a full asyncio Erlang node. Heavy, asyncio-first, complex. erldistpy is a small synchronous client that fits behind the same Protocol surface as httpx. Different shape, different audience.

Development

make bootstrap   # create venv, install editable + dev deps
make test        # run pytest (122 tests)
make lint        # ruff check
make build       # build sdist + wheel into dist/
make dist-check  # twine check dist/*

See docs/ROADMAP.md for the phase-by-phase build log.

License

Unlicense (public domain).