Macula HTTP/3 Mesh: Comprehensive Technical Roadmap

View Source

Executive Summary

Vision: Macula enables distributed BEAM applications to form encrypted, self-healing mesh networks over HTTP/3, with zero infrastructure dependencies. Works behind NATs, scales to thousands of nodes, native distributed Erlang semantics - all over standard HTTPS.

The "Wow" Factor:

  • Distributed Erlang over HTTP/3 (nobody else does this!)
  • Works through corporate firewalls (it's just HTTPS!)
  • Self-healing mesh topology (no central coordinator)
  • Built-in NAT traversal (QUIC magic)
  • Native BEAM implementation (minimal NIFs)

Timeline: 20 weeks (5 months)

Outcome: Production-ready mesh networking infrastructure for edge BEAM applications.


Table of Contents

  1. Understanding QUIC and HTTP/3
  2. QUIC/HTTP/3 Libraries for BEAM
  3. Architecture Overview
  4. Detailed Roadmap
  5. Architecture Diagrams
  6. Technical Deep Dives
  7. Success Metrics

Understanding QUIC and HTTP/3

What is QUIC?

QUIC (Quick UDP Internet Connections) is a modern transport protocol developed by Google and standardized by IETF as RFC 9000. It's designed to replace TCP for web traffic.

Key Characteristics

1. UDP-Based Transport

Traditional:  HTTP/2 → TLS → TCP → IP
Modern:       HTTP/3 → QUIC (includes TLS 1.3) → UDP → IP

QUIC runs over UDP instead of TCP, which provides several advantages:

  • Faster connection establishment (0-RTT and 1-RTT)
  • No head-of-line blocking across streams
  • Better performance on lossy networks
  • Easier NAT traversal (UDP is simpler than TCP for NAT)

2. Built-in Encryption

  • TLS 1.3 is integrated into QUIC (not layered on top)
  • All packets are encrypted (except initial handshake metadata)
  • Forward secrecy by default
  • Connection migration (can change IP addresses mid-connection)

3. Multiplexed Streams

┌─────────────────────────────────────┐
│ QUIC Connection                     │
│  ┌───────────┐ ┌───────────┐       │
│  │ Stream 0  │ │ Stream 1  │  ...  │
│  │ (Ordered) │ │ (Ordered) │       │
│  └───────────┘ └───────────┘       │
│                                     │
│  Streams are independent!           │
│  Loss in Stream 0 doesn't block     │
│  Stream 1 (unlike TCP)              │
└─────────────────────────────────────┘

4. Connection Establishment

Traditional TCP + TLS:

Client → Server:  SYN                        (1 RTT)
Server → Client:  SYN-ACK
Client → Server:  ACK
Client → Server:  ClientHello (TLS)          (2 RTT)
Server → Client:  ServerHello + Certificate
Client → Server:  Finished
Server → Client:  Finished
Client → Server:  HTTP Request               (3 RTT)
                  Total: 3 Round Trips

QUIC (first connection):

Client → Server:  Initial (ClientHello)      (1 RTT)
Server → Client:  Handshake (ServerHello)
Client → Server:  HTTP Request               (1 RTT)
                  Total: 1 Round Trip

QUIC (resumed connection):

Client → Server:  0-RTT Data + HTTP Request  (0 RTT!)
                  Total: 0 Round Trips

5. Loss Recovery

  • Per-stream reliability (not per-connection like TCP)
  • More sophisticated than TCP (monotonically increasing packet numbers)
  • Better handling of spurious retransmissions
  • Pluggable congestion control

6. Connection Migration

Mobile device scenario:
WiFi (IP: 192.168.1.100) → Cellular (IP: 10.20.30.40)

TCP:  Connection breaks, must reconnect (new handshake)
QUIC: Connection continues seamlessly (connection ID stays same)

What is HTTP/3?

HTTP/3 is the third major version of HTTP, using QUIC as its transport instead of TCP.

HTTP Evolution

HTTP/1.1 (1997)
  ↓
  - Text-based protocol
  - One request per connection (or pipelining)
  - Head-of-line blocking

HTTP/2 (2015)
  ↓
  - Binary framing
  - Multiplexing over single TCP connection
  - Header compression (HPACK)
  - Still suffers from TCP head-of-line blocking

HTTP/3 (2022)
  ↓
  - Same semantics as HTTP/2
  - QUIC transport (UDP-based)
  - No head-of-line blocking
  - 0-RTT connection resumption
  - Better mobile performance

HTTP/3 Frame Types

HTTP/3 uses similar frames to HTTP/2 but adapted for QUIC:

Frame Types:
- DATA:        Application data (response body)
- HEADERS:     HTTP headers (compressed with QPACK)
- PRIORITY:    Stream priority hints
- CANCEL_PUSH: Cancel server push
- SETTINGS:    Connection parameters
- PUSH_PROMISE: Server push announcement
- GOAWAY:      Graceful shutdown
- MAX_PUSH_ID: Limit server push

QPACK Header Compression

HTTP/3 uses QPACK (QUIC-aware header compression) instead of HPACK:

  • Dynamic table updates on dedicated stream
  • Prevents head-of-line blocking from header compression
  • Better performance on lossy networks

Why QUIC/HTTP/3 for Macula Mesh?

1. NAT Traversal

UDP is much easier to punch through NATs than TCP:

  • Simpler state machines in NAT devices
  • Easier simultaneous open
  • Better compatibility with STUN/TURN

2. Multiplexing Without Head-of-Line Blocking

Perfect for distributed Erlang:

Process A → Stream 0:  send(...) → [packet lost!] → retransmit
Process B → Stream 1:  send(...) → delivered immediately!

With TCP: Process B would be blocked waiting for Process A's retransmit
With QUIC: Process B's stream is independent

3. Connection Migration

Edge devices often change networks:

IoT device switches from WiFi to cellular:
  - TCP: Connection lost, full reconnect
  - QUIC: Seamless migration, no interruption

4. Firewall Friendly

  • Uses port 443 (standard HTTPS)
  • Looks like HTTPS to middleboxes
  • No special firewall rules needed

5. 0-RTT Resumption

Reconnecting nodes don't waste time:

Node rejoins mesh after brief disconnect:
  - TCP + TLS: 3 RTT to re-establish
  - QUIC 0-RTT: Immediate data transmission

6. Built-in Encryption

  • TLS 1.3 integrated (not optional)
  • Perfect forward secrecy
  • No configuration needed

QUIC Protocol Details

Connection ID

QUIC uses Connection IDs instead of 4-tuple (src IP, src port, dst IP, dst port):

┌─────────────────────────────────────┐
│ QUIC Packet                         │
├─────────────────────────────────────┤
│ Header:                             │
│  - Connection ID: 0x1a2b3c4d...     │ ← Identifies connection
│  - Packet Number: 42                │
│  - Flags: ...                       │
├─────────────────────────────────────┤
│ Encrypted Payload                   │
└─────────────────────────────────────┘

Benefits:
- Connection survives IP address changes
- Load balancers can route without decryption
- NAT rebinding doesn't break connection

Stream Management

QUIC streams are lightweight:

% Each Erlang process can have its own stream
spawn(fun() ->
    {ok, StreamId} = quic:open_stream(Conn),
    quic:send(StreamId, Data),
    receive
        {stream_data, StreamId, Response} -> handle(Response)
    end,
    quic:close_stream(StreamId)
end).

% Streams are cheap! Can create thousands
% No overhead like TCP sockets

Congestion Control

QUIC implements multiple congestion control algorithms:

  • Reno: Traditional TCP-like
  • Cubic: Linux default (aggressive)
  • BBR: Google's Bottleneck Bandwidth and RTT
  • Custom: Can implement your own!

For Macula Mesh:

% Could implement edge-optimized congestion control
-module(macula_congestion).

estimate_bandwidth(Samples) ->
    % Use local measurements instead of RTT
    % Better for edge networks with variable latency
    ...

Flow Control

QUIC has flow control at two levels:

  1. Stream-level: Each stream has credit
  2. Connection-level: Overall connection credit
┌─────────────────────────────────────┐
│ Connection Credit: 1 MB             │
├─────────────────────────────────────┤
│ Stream 0:  256 KB credit            │
│ Stream 1:  512 KB credit            │
│ Stream 2:  256 KB credit            │
└─────────────────────────────────────┘

Prevents any single stream from starving the connection

QUIC/HTTP/3 Libraries for BEAM

Repository: https://github.com/emqx/quic License: Apache 2.0 Maintainer: EMQX Team (Erlang Solutions) Language: Erlang + C (NIF wrapper) Backend: Microsoft MsQuic

Overview

quicer is an Erlang NIF binding for Microsoft's MsQuic library. MsQuic is a production-grade QUIC implementation used by Windows, Azure, and various Microsoft services.

Architecture

┌─────────────────────────────────────┐
│ Erlang Application                  │
├─────────────────────────────────────┤
│ quicer (Erlang API)                 │
│  - quicer:listen/2                  │
│  - quicer:connect/3                 │
│  - quicer:send/2                    │
│  - quicer:recv/2                    │
├─────────────────────────────────────┤
│ quicer NIF (C)                      │
│  - Erlang ↔ MsQuic bridge          │
│  - Resource management              │
│  - Callback handling                │
├─────────────────────────────────────┤
│ MsQuic (C)                          │
│  - RFC 9000 QUIC implementation     │
│  - TLS 1.3 integration              │
│  - Platform-specific optimizations  │
├─────────────────────────────────────┤
│ OS Network Stack (UDP)              │
└─────────────────────────────────────┘

Features

✅ Supported:

  • QUIC v1 (RFC 9000)
  • TLS 1.3
  • 0-RTT connection resumption
  • Connection migration
  • Multiple streams per connection
  • Flow control and congestion control
  • Both client and server modes

❌ Limitations:

  • NIF dependency (requires C compilation)
  • Tied to MsQuic release cycle
  • Platform-specific quirks

API Examples

%% Server
start_server() ->
    %% Load certificate
    {ok, Cert} = file:read_file("server.crt"),
    {ok, Key} = file:read_file("server.key"),

    %% Listen options
    ListenOpts = #{
        cert => Cert,
        key => Key,
        alpn => ["macula/1.0"],
        peer_unidi_stream_count => 10,
        peer_bidi_stream_count => 10
    },

    %% Start listener
    {ok, Listener} = quicer:listen("0.0.0.0", 4433, ListenOpts),

    %% Accept loop
    accept_loop(Listener).

accept_loop(Listener) ->
    {ok, Conn} = quicer:accept(Listener, [], 5000),
    {ok, Conn} = quicer:handshake(Conn),

    %% Spawn handler
    spawn(fun() -> handle_connection(Conn) end),

    %% Continue accepting
    accept_loop(Listener).

handle_connection(Conn) ->
    %% Accept stream
    {ok, Stream} = quicer:accept_stream(Conn, []),

    %% Receive data
    {ok, Data} = quicer:recv(Stream, 0),

    %% Process and respond
    Response = process(Data),
    ok = quicer:send(Stream, Response),

    %% Close stream
    quicer:close_stream(Stream).

%% Client
start_client() ->
    %% Connect options
    ConnOpts = #{
        alpn => ["macula/1.0"],
        verify => verify_peer,
        cacertfile => "ca.crt"
    },

    %% Connect
    {ok, Conn} = quicer:connect("server.example.com", 4433, ConnOpts, 5000),

    %% Open stream
    StreamOpts = #{active => false},
    {ok, Stream} = quicer:start_stream(Conn, StreamOpts),

    %% Send data
    ok = quicer:send(Stream, <<"Hello, QUIC!">>),

    %% Receive response
    {ok, Response} = quicer:recv(Stream, 0),
    io:format("Received: ~p~n", [Response]),

    %% Close
    quicer:close_stream(Stream),
    quicer:close_connection(Conn).

Production Usage

EMQX: Used in EMQX 5.0+ for MQTT over QUIC

  • Handles millions of concurrent connections
  • Production-tested at scale
  • Good performance characteristics

RabbitMQ: Experimental QUIC support via quicer

  • AMQP 1.0 over QUIC transport
  • Still in development

Pros & Cons

Pros:

  • ✅ Battle-tested (MsQuic used in Windows, Azure)
  • ✅ Actively maintained
  • ✅ Good documentation
  • ✅ Performance optimized
  • ✅ Cross-platform (Linux, macOS, Windows)
  • ✅ Production-ready (used in EMQX)

Cons:

  • ❌ NIF dependency (requires compilation)
  • ❌ Tied to MsQuic (external C library)
  • ❌ Breaking changes between MsQuic versions
  • ❌ Limited control over low-level behavior

Verdict: ⭐⭐⭐⭐⭐ Best choice for Macula Mesh

2. xquic (Alternative)

Repository: https://github.com/alibaba/xquic License: Apache 2.0 Maintainer: Alibaba Cloud Language: C + Erlang bindings Backend: xquic (Alibaba's QUIC)

Overview

xquic is Alibaba's in-house QUIC implementation, used in their edge CDN and cloud services.

Features

  • QUIC v1 + IETF draft-29
  • HTTP/3 support
  • QUIC multipath extension
  • BBR congestion control
  • High performance (optimized for Alibaba scale)

Erlang Bindings

Erlang bindings exist but are less mature than quicer:

Pros & Cons

Pros:

  • ✅ Very high performance
  • ✅ Multipath QUIC support
  • ✅ Used at Alibaba scale

Cons:

  • ❌ Less mature Erlang bindings
  • ❌ Documentation mostly in Chinese
  • ❌ Smaller community
  • ❌ Not as widely tested outside Alibaba

Verdict: ⭐⭐⭐ Good but less accessible

3. quinn (Rust, via Rustler)

Repository: https://github.com/quinn-rs/quinn License: Apache 2.0 / MIT Language: Rust Bindings: Could use Rustler for Erlang

Overview

quinn is a pure Rust QUIC implementation, considered one of the best non-C QUIC libraries.

Hypothetical Erlang Integration

┌─────────────────────────────────────┐
│ Erlang Application                  │
├─────────────────────────────────────┤
│ quinn_nif (Rustler)                 │
│  - Erlang ↔ Rust bridge            │
├─────────────────────────────────────┤
│ quinn (Rust)                        │
│  - Pure Rust QUIC implementation    │
├─────────────────────────────────────┤
│ tokio (Rust async runtime)          │
└─────────────────────────────────────┘

Pros & Cons

Pros:

  • ✅ Pure Rust (memory safe)
  • ✅ Excellent performance
  • ✅ Active development
  • ✅ Clean API

Cons:

  • ❌ No official Erlang bindings
  • ❌ Would need to build Rustler wrapper
  • ❌ Rust async runtime complexity
  • ❌ Additional development effort

Verdict: ⭐⭐ Interesting but requires work

4. Pure Erlang QUIC (Hypothetical)

Status: Doesn't exist Effort: 6-12 months of development

Why Pure Erlang?

Pros:

  • ✅ No NIFs (easier deployment)
  • ✅ Full control over implementation
  • ✅ Could optimize for BEAM semantics
  • ✅ Easier debugging

Cons:

  • ❌ Huge development effort
  • ❌ Likely slower than C/Rust
  • ❌ Hard to match performance of tuned C libs
  • ❌ Cryptography still needs NIFs

Feasibility Analysis

Components needed:
1. UDP socket handling         ✅ (gen_udp)
2. TLS 1.3 implementation     ⚠️  (ssl app, but need low-level access)
3. Packet parsing             ✅ (binary pattern matching)
4. Connection state machine   ✅ (gen_statem)
5. Stream multiplexing        ✅ (processes)
6. Flow control               ✅ (credits/backpressure)
7. Congestion control         ✅ (algorithms in Erlang)
8. Loss detection             ✅ (timers + state)
9. 0-RTT resumption          ⚠️  (needs crypto primitives)

Estimated effort: 3-6 person-months for basic implementation
                 12+ person-months for production-ready

Verdict: ⭐ Not practical for Phase 1

Library Comparison Matrix

FeaturequicerxquicquinnPure Erlang
Maturity⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐❌
Erlang Integration⭐⭐⭐⭐⭐⭐⭐❌⭐⭐⭐⭐⭐
Performance⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Documentation⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐N/A
Production Ready✅✅✅❌
Cross-platform✅✅✅✅
NIF RequiredYesYesYesNo
Development EffortLowMediumHighVery High
Community SupportStrongModerateStrongN/A

Recommendation: quicer

For Macula Mesh Phase 1, quicer is the clear choice:

  1. Production-ready: Used in EMQX with millions of connections
  2. Well-documented: Good examples and API docs
  3. Actively maintained: Regular updates, responsive maintainers
  4. Erlang-native: Designed for BEAM from the ground up
  5. MsQuic backend: Battle-tested in Microsoft services

Migration Path:

  • Phase 1: Use quicer (proven, fast path to PoC)
  • Phase 2: Optimize (profile, tune, maybe contribute improvements)
  • Phase 3: Evaluate alternatives (if needed, could switch to pure Erlang or Rust)

Architecture Overview

High-Level Architecture

┌─────────────────────────────────────────────────────────┐
│                    Macula Mesh Network                  │
│                                                         │
│  ┌────────┐      ┌────────┐      ┌────────┐           │
│  │ Node A │◄────►│ Node B │◄────►│ Node C │           │
│  │        │      │        │      │        │           │
│  └────┬───┘      └────┬───┘      └────┬───┘           │
│       │               │               │                │
│       └───────────────┼───────────────┘                │
│                       │                                │
│                  HTTP/3 (QUIC)                         │
│              (Encrypted, Multiplexed)                  │
└─────────────────────────────────────────────────────────┘

Layered Architecture

┌─────────────────────────────────────────────────────────┐
│ Layer 5: Application                                    │
│  • Elixir/Erlang Applications                          │
│  • Standard distributed Erlang API                     │
│  • spawn/2, send/2, monitor/2, etc.                   │
└─────────────────────────────────────────────────────────┘
              ↓ (transparent to application)
┌─────────────────────────────────────────────────────────┐
│ Layer 4: WAMP Protocol (Optional Compatibility)        │
│  • publish(Topic, Data)                                │
│  • subscribe(Topic, Handler)                           │
│  • call(Procedure, Args)                               │
│  • register(Procedure, Handler)                        │
└─────────────────────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────────────────────┐
│ Layer 3: Mesh Routing                                  │
│  • Node discovery (bootstrap, mDNS, DHT)               │
│  • Membership management (SWIM gossip)                 │
│  • Topology management (k-regular graph)               │
│  • Message routing (DHT-based, O(log n) hops)         │
└─────────────────────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Macula Distribution Protocol                  │
│  • Message framing (wire protocol)                     │
│  • Handshake and authentication                        │
│  • Process messaging (SEND, LINK, MONITOR, etc.)      │
│  • Stream multiplexing (process ↔ stream mapping)     │
└─────────────────────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────────────────────┐
│ Layer 1: QUIC Transport (HTTP/3)                       │
│  • quicer (Erlang NIF)                                 │
│  • MsQuic (C library)                                  │
│  • UDP sockets                                         │
│  • TLS 1.3 encryption                                  │
│  • NAT traversal (STUN/ICE)                           │
└─────────────────────────────────────────────────────────┘

Component Architecture

┌─────────────────────────────────────────────────────────┐
│ Macula Node (Erlang/OTP Application)                   │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │ Supervision Tree                                 │  │
│  │                                                  │  │
│  │  macula_sup (top-level supervisor)              │  │
│  │    ├─ macula_connection_sup (connections)       │  │
│  │    ├─ macula_membership (SWIM gossip)           │  │
│  │    ├─ macula_topology (neighbor management)     │  │
│  │    ├─ macula_routing (DHT routing)              │  │
│  │    ├─ macula_discovery (node discovery)         │  │
│  │    ├─ macula_pubsub (pub/sub registry)          │  │
│  │    └─ macula_dist (distribution driver)         │  │
│  └──────────────────────────────────────────────────┘  │
│                                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │ Connection Pool (one per remote node)            │  │
│  │                                                  │  │
│  │  connection_1 (QUIC, 5 streams active)          │  │
│  │  connection_2 (QUIC, 3 streams active)          │  │
│  │  connection_3 (QUIC, 8 streams active)          │  │
│  └──────────────────────────────────────────────────┘  │
│                                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │ ETS Tables (shared state)                        │  │
│  │                                                  │  │
│  │  - membership_table (alive nodes)                │  │
│  │  - routing_table (DHT entries)                   │  │
│  │  - stream_registry (stream_id → pid)            │  │
│  │  - subscription_registry (topic → [pids])       │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

Detailed Roadmap

Phase 1: Foundation (Weeks 1-4)

Goal: Prove HTTP/3 Can Carry Erlang Distribution Traffic

Week 1-2: QUIC Transport Layer

Objectives:

  1. Set up quicer dependency
  2. Create basic QUIC server/client
  3. Implement bidirectional streaming
  4. Build connection management

Deliverables:

%% File: macula_quic_echo.erl
%% Simple QUIC echo server/client to validate transport

-module(macula_quic_echo).
-export([start_server/1, start_client/2]).

start_server(Port) ->
    %% Generate self-signed cert for testing
    {Cert, Key} = macula_cert:generate_self_signed("localhost"),

    ListenOpts = #{
        cert => Cert,
        key => Key,
        alpn => ["macula/1.0"],
        idle_timeout_ms => 10000,
        peer_unidi_stream_count => 10,
        peer_bidi_stream_count => 100
    },

    {ok, Listener} = quicer:listen(Port, ListenOpts),
    io:format("QUIC server listening on port ~p~n", [Port]),

    accept_loop(Listener).

accept_loop(Listener) ->
    case quicer:accept(Listener, [], infinity) of
        {ok, Conn} ->
            %% Complete handshake
            {ok, Conn} = quicer:handshake(Conn),

            %% Get peer info
            {ok, PeerAddr} = quicer:getopt(Conn, peer_addr),
            io:format("Accepted connection from ~p~n", [PeerAddr]),

            %% Spawn connection handler
            spawn_link(fun() -> handle_connection(Conn) end),

            %% Continue accepting
            accept_loop(Listener);
        {error, Reason} ->
            io:format("Accept error: ~p~n", [Reason]),
            timer:sleep(1000),
            accept_loop(Listener)
    end.

handle_connection(Conn) ->
    %% Accept streams
    stream_accept_loop(Conn).

stream_accept_loop(Conn) ->
    case quicer:accept_stream(Conn, []) of
        {ok, Stream} ->
            %% Spawn stream handler
            spawn_link(fun() -> echo_stream(Stream) end),
            stream_accept_loop(Conn);
        {error, closed} ->
            io:format("Connection closed~n");
        {error, Reason} ->
            io:format("Stream accept error: ~p~n", [Reason])
    end.

echo_stream(Stream) ->
    case quicer:recv(Stream, 0) of
        {ok, Data} ->
            io:format("Received: ~p~n", [Data]),
            quicer:send(Stream, Data),
            echo_stream(Stream);
        {error, closed} ->
            quicer:close_stream(Stream);
        {error, Reason} ->
            io:format("Recv error: ~p~n", [Reason]),
            quicer:close_stream(Stream)
    end.

start_client(Host, Port) ->
    ConnOpts = #{
        alpn => ["macula/1.0"],
        verify => verify_none  %% For testing only!
    },

    io:format("Connecting to ~s:~p~n", [Host, Port]),
    {ok, Conn} = quicer:connect(Host, Port, ConnOpts, 5000),

    %% Open stream
    {ok, Stream} = quicer:start_stream(Conn, #{active => false}),

    %% Send message
    Message = <<"Hello, QUIC!">>,
    ok = quicer:send(Stream, Message),
    io:format("Sent: ~p~n", [Message]),

    %% Receive echo
    {ok, Echo} = quicer:recv(Stream, 0),
    io:format("Received echo: ~p~n", [Echo]),

    %% Close
    quicer:close_stream(Stream),
    quicer:close_connection(Conn).

Testing:

# Terminal 1: Start server
$ erl -pa _build/default/lib/*/ebin
1> macula_quic_echo:start_server(4433).

# Terminal 2: Connect client
$ erl -pa _build/default/lib/*/ebin
1> macula_quic_echo:start_client("localhost", 4433).

Success Criteria:

  • ✅ Server accepts QUIC connections
  • ✅ Client can connect and exchange data
  • ✅ Multiple streams work concurrently
  • ✅ Connection survives stream closure

Week 3: Message Framing Protocol

Objectives:

  1. Design Macula wire protocol v1
  2. Implement message encoding/decoding
  3. Define message types for distribution

Wire Protocol Specification:

Macula Wire Protocol v1
=======================

All integers are big-endian.

Packet Format:
┌──────┬──────┬───────┬────────┬─────────┐
│ Ver  │ Type │ Flags │ Length │ Payload │
│ (1B) │ (1B) │ (2B)  │ (4B)   │ (N B)   │
└──────┴──────┴───────┴────────┴─────────┘

Version (1 byte):
  - 0x01: Version 1

Type (1 byte):
  - 0x01: HANDSHAKE
  - 0x02: HEARTBEAT
  - 0x03: SEND           (send message to process)
  - 0x04: REG_SEND       (send to registered name)
  - 0x05: EXIT           (process exit signal)
  - 0x06: LINK           (link processes)
  - 0x07: UNLINK         (unlink processes)
  - 0x08: MONITOR        (monitor process)
  - 0x09: DEMONITOR      (demonitor)
  - 0x0A: GROUP_LEADER   (group leader operations)
  - 0x0B: RPC            (remote procedure call)
  - 0x0C: SPAWN_REQUEST  (spawn on remote node)
  - 0x0D: SPAWN_REPLY    (spawn result)

Flags (2 bytes):
  Bit 0: COMPRESSED (payload is compressed)
  Bit 1: FRAGMENTED (part of fragmented message)
  Bit 2-15: Reserved

Length (4 bytes):
  - Payload length in bytes (max 16 MB)

Payload (N bytes):
  - Message-type specific data
  - Encoded using Erlang External Term Format (EETF)

Implementation:

%% File: macula_protocol.erl
-module(macula_protocol).
-export([encode/2, decode/1]).

%% Protocol version
-define(VERSION, 1).

%% Message types
-define(MSG_HANDSHAKE, 16#01).
-define(MSG_HEARTBEAT, 16#02).
-define(MSG_SEND, 16#03).
-define(MSG_REG_SEND, 16#04).
-define(MSG_EXIT, 16#05).
-define(MSG_LINK, 16#06).
-define(MSG_UNLINK, 16#07).
-define(MSG_MONITOR, 16#08).
-define(MSG_DEMONITOR, 16#09).
-define(MSG_GROUP_LEADER, 16#0A).
-define(MSG_RPC, 16#0B).
-define(MSG_SPAWN_REQUEST, 16#0C).
-define(MSG_SPAWN_REPLY, 16#0D).

%% Flags
-define(FLAG_COMPRESSED, 16#0001).
-define(FLAG_FRAGMENTED, 16#0002).

%% Encode message
encode(Type, Payload) when is_integer(Type), Type >= 0, Type =< 255 ->
    encode(Type, 0, Payload).

encode(Type, Flags, Payload) ->
    %% Serialize payload
    PayloadBin = term_to_binary(Payload, [compressed]),
    Length = byte_size(PayloadBin),

    %% Check size limit (16 MB)
    if
        Length > 16#1000000 ->
            {error, payload_too_large};
        true ->
            %% Build packet
            <<?VERSION:8, Type:8, Flags:16, Length:32, PayloadBin/binary>>
    end.

%% Decode message
decode(<<?VERSION:8, Type:8, Flags:16, Length:32, PayloadBin:Length/binary, Rest/binary>>) ->
    %% Deserialize payload
    Payload = binary_to_term(PayloadBin),

    {ok, #{
        type => Type,
        flags => Flags,
        payload => Payload
    }, Rest};
decode(Bin) when byte_size(Bin) < 8 ->
    {error, insufficient_data};
decode(<<Version:8, _/binary>>) when Version =/= ?VERSION ->
    {error, {unsupported_version, Version}};
decode(_) ->
    {error, invalid_packet}.

%% Helper: Encode SEND message
encode_send(FromPid, ToPid, Message) ->
    encode(?MSG_SEND, {FromPid, ToPid, Message}).

%% Helper: Encode LINK message
encode_link(Pid1, Pid2) ->
    encode(?MSG_LINK, {Pid1, Pid2}).

%% Helper: Encode MONITOR message
encode_monitor(Pid, Ref, MonitoredPid) ->
    encode(?MSG_MONITOR, {Pid, Ref, MonitoredPid}).

%% Helper: Encode SPAWN_REQUEST message
encode_spawn_request(ReqId, Module, Function, Args) ->
    encode(?MSG_SPAWN_REQUEST, {ReqId, Module, Function, Args}).

Handshake Protocol:

%% File: macula_handshake.erl
-module(macula_handshake).
-export([perform/1, accept/1]).

-record(handshake, {
    version = ?MACULA_VERSION,
    node_name :: atom(),
    node_id :: binary(),      %% SHA256(certificate)
    capabilities = [] :: [atom()],
    creation :: integer(),    %% Node start time
    challenge :: binary()     %% Random bytes for auth
}).

%% Initiate handshake (client side)
perform(Conn) ->
    %% Generate our handshake
    Handshake = #handshake{
        node_name = node(),
        node_id = macula_identity:node_id(),
        capabilities = [compression, rpc, monitoring, streams],
        creation = erlang:system_time(millisecond),
        challenge = crypto:strong_rand_bytes(32)
    },

    %% Send handshake
    Packet = macula_protocol:encode(?MSG_HANDSHAKE, Handshake),
    {ok, Stream} = quicer:start_stream(Conn, #{active => false}),
    ok = quicer:send(Stream, Packet),

    %% Receive remote handshake
    {ok, ResponsePacket} = quicer:recv(Stream, 0),
    {ok, #{payload := RemoteHandshake}, _} = macula_protocol:decode(ResponsePacket),

    %% Verify compatibility
    case check_compatibility(Handshake, RemoteHandshake) of
        ok ->
            %% Send acknowledgment
            Ack = macula_protocol:encode(?MSG_HANDSHAKE, {ack, Handshake#handshake.challenge}),
            ok = quicer:send(Stream, Ack),
            {ok, RemoteHandshake};
        {error, Reason} ->
            {error, Reason}
    end.

%% Accept handshake (server side)
accept(Conn) ->
    %% Accept stream
    {ok, Stream} = quicer:accept_stream(Conn, []),

    %% Receive handshake
    {ok, Packet} = quicer:recv(Stream, 0),
    {ok, #{payload := RemoteHandshake}, _} = macula_protocol:decode(Packet),

    %% Generate our handshake
    Handshake = #handshake{
        node_name = node(),
        node_id = macula_identity:node_id(),
        capabilities = [compression, rpc, monitoring, streams],
        creation = erlang:system_time(millisecond),
        challenge = crypto:strong_rand_bytes(32)
    },

    %% Verify compatibility
    case check_compatibility(Handshake, RemoteHandshake) of
        ok ->
            %% Send our handshake
            Response = macula_protocol:encode(?MSG_HANDSHAKE, Handshake),
            ok = quicer:send(Stream, Response),

            %% Wait for acknowledgment
            {ok, AckPacket} = quicer:recv(Stream, 0),
            {ok, #{payload := {ack, Challenge}}, _} = macula_protocol:decode(AckPacket),

            %% Verify challenge
            if
                Challenge =:= Handshake#handshake.challenge ->
                    {ok, RemoteHandshake};
                true ->
                    {error, invalid_challenge}
            end;
        {error, Reason} ->
            {error, Reason}
    end.

check_compatibility(Local, Remote) ->
    %% Check version
    if
        Local#handshake.version =/= Remote#handshake.version ->
            {error, version_mismatch};
        true ->
            %% Check capabilities
            CommonCaps = sets:intersection(
                sets:from_list(Local#handshake.capabilities),
                sets:from_list(Remote#handshake.capabilities)
            ),
            if
                sets:size(CommonCaps) > 0 -> ok;
                true -> {error, no_common_capabilities}
            end
    end.

Week 4: Basic Distribution Protocol

Objectives:

  1. Implement net_kernel distribution driver
  2. Support basic message sending
  3. Enable process spawning

Distribution Driver:

%% File: macula_dist.erl
%% Erlang distribution protocol driver for Macula
%%
%% This module implements the callbacks required by Erlang's net_kernel
%% to use Macula as a custom distribution protocol.

-module(macula_dist).

%% Distribution driver callbacks
-export([
    listen/1,
    accept/1,
    accept_connection/5,
    setup/5,
    close/1,
    select/1,
    is_node_name/1,
    address/0
]).

%% Internal API
-export([
    send/2,
    send/3,
    recv/2
]).

-record(macula_dist_state, {
    listener,      %% QUIC listener
    connection,    %% QUIC connection
    node,          %% Remote node name
    streams = #{}  %% Map: purpose => stream
}).

%% Listen for incoming connections
listen(Name) ->
    %% Extract port from name (e.g., node@host:4433)
    Port = extract_port(Name, 4433),

    %% Generate certificate
    {Cert, Key} = macula_cert:generate_node_cert(Name),

    ListenOpts = #{
        cert => Cert,
        key => Key,
        alpn => ["macula-dist/1.0"],
        peer_bidi_stream_count => 100
    },

    case quicer:listen(Port, ListenOpts) of
        {ok, Listener} ->
            {ok, {Listener, #macula_dist_state{listener = Listener}}};
        {error, Reason} ->
            {error, Reason}
    end.

%% Accept incoming connection
accept(Listen) ->
    Listener = element(1, Listen),
    case quicer:accept(Listener, [], infinity) of
        {ok, Conn} ->
            %% Perform handshake
            case macula_handshake:accept(Conn) of
                {ok, RemoteHandshake} ->
                    RemoteNode = RemoteHandshake#handshake.node_name,
                    State = #macula_dist_state{
                        connection = Conn,
                        node = RemoteNode
                    },
                    {ok, Conn, State};
                {error, Reason} ->
                    quicer:close_connection(Conn),
                    {error, Reason}
            end;
        {error, Reason} ->
            {error, Reason}
    end.

%% Accept connection (post-handshake setup)
accept_connection(AcceptPid, DistCtrl, MyNode, Allowed, SetupTime) ->
    %% This is called by net_kernel after accept/1 succeeds
    gen_server:call(DistCtrl, {accept_connection, AcceptPid, MyNode, Allowed, SetupTime}).

%% Setup outgoing connection
setup(Node, Type, MyNode, LongOrShortNames, SetupTime) ->
    %% Extract host and port
    {Host, Port} = parse_node_name(Node),

    %% Connect
    ConnOpts = #{
        alpn => ["macula-dist/1.0"],
        verify => verify_peer
    },

    case quicer:connect(Host, Port, ConnOpts, 5000) of
        {ok, Conn} ->
            %% Perform handshake
            case macula_handshake:perform(Conn) of
                {ok, RemoteHandshake} ->
                    State = #macula_dist_state{
                        connection = Conn,
                        node = Node
                    },
                    {ok, Conn, State};
                {error, Reason} ->
                    quicer:close_connection(Conn),
                    {error, Reason}
            end;
        {error, Reason} ->
            {error, Reason}
    end.

%% Close connection
close(Conn) ->
    quicer:close_connection(Conn),
    ok.

%% Select (for epmd compatibility)
select(Node) ->
    %% Macula doesn't use epmd
    {ok, Node}.

%% Check if valid node name
is_node_name(Node) when is_atom(Node) ->
    case atom_to_list(Node) of
        [$@ | _] -> false;
        Name ->
            case string:chr(Name, $@) of
                0 -> false;
                _ -> true
            end
    end;
is_node_name(_) ->
    false.

%% Get address (for net_kernel)
address() ->
    %% Return local node address
    {ok, {0, 0, 0, 0}}.

%% Send message to process on remote node
send(Conn, Pid, Message) ->
    %% Encode SEND message
    Packet = macula_protocol:encode_send(self(), Pid, Message),

    %% Get or create stream for this message
    {ok, Stream} = get_or_create_stream(Conn, control),

    %% Send
    quicer:send(Stream, Packet).

send(Conn, Name, Message) when is_atom(Name) ->
    %% Send to registered name
    Packet = macula_protocol:encode(?MSG_REG_SEND, {self(), Name, Message}),
    {ok, Stream} = get_or_create_stream(Conn, control),
    quicer:send(Stream, Packet).

%% Receive message
recv(Conn, Timeout) ->
    %% Accept next stream with data
    case quicer:accept_stream(Conn, [], Timeout) of
        {ok, Stream} ->
            case quicer:recv(Stream, 0) of
                {ok, Packet} ->
                    {ok, Msg, _} = macula_protocol:decode(Packet),
                    {ok, Msg};
                {error, Reason} ->
                    {error, Reason}
            end;
        {error, Reason} ->
            {error, Reason}
    end.

%%% Internal functions

get_or_create_stream(Conn, Purpose) ->
    %% Look up existing stream for this purpose
    %% If not found, create new stream
    case ets:lookup(macula_streams, {Conn, Purpose}) of
        [{_, Stream}] ->
            {ok, Stream};
        [] ->
            {ok, Stream} = quicer:start_stream(Conn, #{active => false}),
            ets:insert(macula_streams, {{Conn, Purpose}, Stream}),
            {ok, Stream}
    end.

parse_node_name(Node) when is_atom(Node) ->
    case string:split(atom_to_list(Node), "@") of
        [_Name, HostPort] ->
            case string:split(HostPort, ":") of
                [Host, Port] -> {Host, list_to_integer(Port)};
                [Host] -> {Host, 4433}  %% Default port
            end;
        _ ->
            {error, invalid_node_name}
    end.

extract_port(Name, Default) ->
    case string:split(atom_to_list(Name), ":") of
        [_, Port] -> list_to_integer(Port);
        _ -> Default
    end.

Testing:

# Terminal 1: Start first node
$ erl -name node1@localhost:4433 -proto_dist macula -pa _build/default/lib/*/ebin

# Terminal 2: Start second node
$ erl -name node2@localhost:4434 -proto_dist macula -pa _build/default/lib/*/ebin

# In node2:
(node2@localhost:4434)1> net_kernel:connect_node('node1@localhost:4433').
true

(node2@localhost:4434)2> nodes().
['node1@localhost:4433']

(node2@localhost:4434)3> {node1, 'node1@localhost:4433'} ! {hello, from, node2}.
{hello, from, node2}

# In node1, check process mailbox:
(node1@localhost:4433)1> receive Msg -> Msg end.
{hello, from, node2}

# Spawn remote process:
(node2@localhost:4434)4> spawn('node1@localhost:4433', fun() ->
    io:format("Running on ~p!~n", [node()])
end).
<12345.67.0>

# On node1, see output:
Running on 'node1@localhost:4433'!

Success Criteria:

  • ✅ Two nodes can connect via Macula
  • ✅ nodes() shows connected nodes
  • ✅ Messages can be sent between nodes
  • ✅ Remote spawning works
  • ✅ Basic distributed Erlang operations function

Phase 2: Mesh Topology (Weeks 5-8)

Goal: Move Beyond Point-to-Point to Self-Organizing Mesh

Week 5-6: Node Discovery and Membership

Objectives:

  1. Implement multi-strategy node discovery
  2. Build SWIM-based membership protocol
  3. Handle node joins, leaves, failures

Node Discovery:

%% File: macula_discovery.erl
-module(macula_discovery).
-behaviour(gen_server).

-export([start_link/0, discover/0, get_known_nodes/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).

-record(state, {
    bootstrap_nodes = [],
    known_nodes = sets:new(),
    discovery_interval = 30000  %% 30 seconds
}).

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

discover() ->
    gen_server:call(?MODULE, discover).

get_known_nodes() ->
    gen_server:call(?MODULE, get_known_nodes).

init([]) ->
    %% Load bootstrap nodes from config
    Bootstrap = application:get_env(macula, bootstrap_nodes, []),

    %% Start discovery timer
    erlang:send_after(1000, self(), discover),

    {ok, #state{bootstrap_nodes = Bootstrap}}.

handle_call(discover, _From, State) ->
    %% Run all discovery strategies in parallel
    Self = self(),
    spawn(fun() -> Self ! {discovered, discover_via_bootstrap(State#state.bootstrap_nodes)} end),
    spawn(fun() -> Self ! {discovered, discover_via_mdns()} end),
    spawn(fun() -> Self ! {discovered, discover_via_dns_srv()} end),

    {reply, ok, State};

handle_call(get_known_nodes, _From, State) ->
    Nodes = sets:to_list(State#state.known_nodes),
    {reply, Nodes, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(discover, State) ->
    %% Trigger discovery
    discover(),

    %% Schedule next discovery
    erlang:send_after(State#state.discovery_interval, self(), discover),
    {noreply, State};

handle_info({discovered, Nodes}, State) ->
    %% Merge discovered nodes
    KnownNodes = lists:foldl(
        fun(Node, Acc) -> sets:add_element(Node, Acc) end,
        State#state.known_nodes,
        Nodes
    ),

    %% Notify membership protocol
    macula_membership:discovered_nodes(Nodes),

    {noreply, State#state{known_nodes = KnownNodes}}.

%%% Discovery Strategies

%% Strategy 1: Bootstrap Nodes
discover_via_bootstrap(Bootstrap) ->
    %% Try to connect to bootstrap nodes
    lists:filtermap(fun(Node) ->
        case macula_connection:ping(Node, 1000) of
            pong -> {true, Node};
            timeout -> false
        end
    end, Bootstrap).

%% Strategy 2: mDNS (local network)
discover_via_mdns() ->
    %% Send mDNS query for _macula._udp.local
    case macula_mdns:discover("_macula._udp.local", 2000) of
        {ok, Nodes} -> Nodes;
        {error, _} -> []
    end.

%% Strategy 3: DNS SRV Records
discover_via_dns_srv() ->
    %% Query DNS SRV for _macula._udp.example.com
    Domain = application:get_env(macula, dns_domain, "macula.local"),
    SRVName = "_macula._udp." ++ Domain,

    case inet_res:lookup(SRVName, in, srv) of
        [] -> [];
        Records ->
            %% Extract host:port from SRV records
            lists:map(fun({_Priority, _Weight, Port, Host}) ->
                list_to_atom(atom_to_list(node()) ++ "@" ++ Host ++ ":" ++ integer_to_list(Port))
            end, Records)
    end.

SWIM Membership Protocol:

%% File: macula_membership.erl
%% SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol
-module(macula_membership).
-behaviour(gen_server).

-export([start_link/0, join/1, leave/0, get_members/0, discovered_nodes/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).

-record(member, {
    node_id,
    address,
    state = alive,      %% alive | suspect | dead
    incarnation = 0,    %% For conflict resolution
    last_seen,          %% Timestamp
    metadata = #{}      %% Arbitrary key-value data
}).

-record(state, {
    local_member,
    members = #{},       %% node_id => member
    protocol_period = 1000,  %% 1 second
    suspect_timeout = 5000,  %% 5 seconds
    ping_targets = []
}).

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

join(BootstrapNode) ->
    gen_server:call(?MODULE, {join, BootstrapNode}).

leave() ->
    gen_server:call(?MODULE, leave).

get_members() ->
    gen_server:call(?MODULE, get_members).

discovered_nodes(Nodes) ->
    gen_server:cast(?MODULE, {discovered_nodes, Nodes}).

init([]) ->
    %% Create local member
    Local = #member{
        node_id = macula_identity:node_id(),
        address = macula_identity:address(),
        state = alive,
        incarnation = 0,
        last_seen = erlang:system_time(millisecond),
        metadata = #{
            node_name => node(),
            started_at => erlang:system_time(millisecond)
        }
    },

    %% Start protocol tick
    erlang:send_after(1000, self(), protocol_tick),

    {ok, #state{local_member = Local}}.

handle_call({join, BootstrapNode}, _From, State) ->
    %% Contact bootstrap node and exchange membership
    case macula_connection:connect(BootstrapNode) of
        {ok, Conn} ->
            %% Send join request
            Request = {join, State#state.local_member},
            case macula_rpc:call(Conn, ?MODULE, handle_join, [Request]) of
                {ok, RemoteMembers} ->
                    %% Merge members
                    Members = maps:merge(State#state.members, RemoteMembers),
                    {reply, ok, State#state{members = Members}};
                {error, Reason} ->
                    {reply, {error, Reason}, State}
            end;
        {error, Reason} ->
            {reply, {error, Reason}, State}
    end;

handle_call(get_members, _From, State) ->
    Members = maps:values(State#state.members),
    AliveMembers = lists:filter(fun(M) -> M#member.state =:= alive end, Members),
    {reply, AliveMembers, State};

handle_call(leave, _From, State) ->
    %% Broadcast leave message
    Members = maps:values(State#state.members),
    Msg = {leave, State#state.local_member},
    lists:foreach(fun(Member) ->
        macula_connection:send(Member#member.node_id, Msg)
    end, Members),

    {stop, normal, ok, State}.

handle_cast({discovered_nodes, Nodes}, State) ->
    %% Add discovered nodes to members (if not already known)
    NewMembers = lists:foldl(fun(Node, Acc) ->
        NodeId = macula_identity:node_id(Node),
        case maps:is_key(NodeId, Acc) of
            true -> Acc;
            false ->
                Member = #member{
                    node_id = NodeId,
                    address = Node,
                    state = alive,
                    last_seen = erlang:system_time(millisecond)
                },
                maps:put(NodeId, Member, Acc)
        end
    end, State#state.members, Nodes),

    {noreply, State#state{members = NewMembers}};

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(protocol_tick, State) ->
    %% SWIM protocol tick
    NewState = swim_tick(State),

    %% Schedule next tick
    erlang:send_after(State#state.protocol_period, self(), protocol_tick),

    {noreply, NewState};

handle_info({ping, From, Incarnation}, State) ->
    %% Respond to ping
    macula_connection:send(From, {ack, State#state.local_member#member.incarnation}),
    {noreply, State};

handle_info({ack, _Incarnation}, State) ->
    %% Received ack from ping
    {noreply, State};

handle_info({ping_req, Target, From}, State) ->
    %% Indirect ping request
    case macula_connection:ping(Target, 500) of
        pong ->
            macula_connection:send(From, {ping_req_ack, Target});
        timeout ->
            macula_connection:send(From, {ping_req_timeout, Target})
    end,
    {noreply, State}.

%%% SWIM Protocol Implementation

swim_tick(State) ->
    %% 1. Select random member to ping
    case select_random_member(State#state.members) of
        {ok, Target} ->
            case direct_ping(Target) of
                pong ->
                    %% Update last_seen
                    update_member_state(Target#member.node_id, alive, State);
                timeout ->
                    %% Try indirect ping via other members
                    case indirect_ping(Target, State) of
                        ok ->
                            update_member_state(Target#member.node_id, alive, State);
                        failed ->
                            %% Mark as suspect
                            State1 = update_member_state(Target#member.node_id, suspect, State),
                            %% Schedule suspicion timeout
                            erlang:send_after(State#state.suspect_timeout, self(),
                                {suspect_timeout, Target#member.node_id}),
                            State1
                    end
            end;
        error ->
            State
    end,

    %% 2. Gossip membership changes
    gossip_changes(State),

    State.

direct_ping(Target) ->
    case macula_connection:ping(Target#member.node_id, 1000) of
        pong -> pong;
        _ -> timeout
    end.

indirect_ping(Target, State) ->
    %% Select K random members for indirect ping
    K = 3,
    Members = maps:values(State#state.members),
    Proxies = select_random_n(Members, K),

    %% Send ping_req to proxies
    Ref = make_ref(),
    lists:foreach(fun(Proxy) ->
        macula_connection:send(Proxy#member.node_id, {ping_req, Target, self(), Ref})
    end, Proxies),

    %% Wait for responses
    wait_for_ping_req_responses(Ref, K, 2000).

wait_for_ping_req_responses(_Ref, 0, _Timeout) ->
    failed;
wait_for_ping_req_responses(Ref, Remaining, Timeout) ->
    receive
        {ping_req_ack, _Target, Ref} ->
            ok;
        {ping_req_timeout, _Target, Ref} ->
            wait_for_ping_req_responses(Ref, Remaining - 1, Timeout)
    after Timeout ->
        failed
    end.

gossip_changes(State) ->
    %% Select random peers for gossip
    Members = maps:values(State#state.members),
    GossipTargets = select_random_n(Members, 3),

    %% Get recent changes
    Changes = get_recent_changes(State),

    %% Send to targets
    lists:foreach(fun(Target) ->
        macula_connection:send(Target#member.node_id, {gossip, Changes})
    end, GossipTargets).

get_recent_changes(State) ->
    %% Get members that changed state recently (last 10 seconds)
    Now = erlang:system_time(millisecond),
    RecentWindow = 10000,

    maps:filter(fun(_NodeId, Member) ->
        (Now - Member#member.last_seen) < RecentWindow
    end, State#state.members).

update_member_state(NodeId, NewState, State) ->
    case maps:find(NodeId, State#state.members) of
        {ok, Member} ->
            UpdatedMember = Member#member{
                state = NewState,
                last_seen = erlang:system_time(millisecond)
            },
            Members = maps:put(NodeId, UpdatedMember, State#state.members),
            State#state{members = Members};
        error ->
            State
    end.

select_random_member(Members) when map_size(Members) > 0 ->
    List = maps:values(Members),
    {ok, lists:nth(rand:uniform(length(List)), List)};
select_random_member(_) ->
    error.

select_random_n(List, N) when length(List) =< N ->
    List;
select_random_n(List, N) ->
    %% Shuffle and take N
    Shuffled = [X || {_, X} <- lists:sort([{rand:uniform(), E} || E <- List])],
    lists:sublist(Shuffled, N).

Week 7: Topology Management and Routing

Objectives:

  1. Implement k-regular graph topology
  2. Build DHT-based routing
  3. Optimize for low diameter

Topology Manager:

%% File: macula_topology.erl
%% Manages connection topology using k-regular graph
%% Each node maintains K connections to neighbors on consistent hash ring

-module(macula_topology).
-behaviour(gen_server).

-export([start_link/0, maintain/0, get_neighbors/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).

-define(K_NEIGHBORS, 6).  %% Number of neighbors to maintain

-record(state, {
    neighbors = [],         %% Current neighbor connections
    desired_neighbors = [], %% Neighbors we should connect to
    ring_position          %% Our position on hash ring
}).

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

maintain() ->
    gen_server:call(?MODULE, maintain).

get_neighbors() ->
    gen_server:call(?MODULE, get_neighbors).

init([]) ->
    %% Calculate our position on hash ring
    NodeId = macula_identity:node_id(),
    Position = crypto:hash(sha256, NodeId),

    %% Start maintenance timer
    erlang:send_after(5000, self(), maintain),

    {ok, #state{ring_position = Position}}.

handle_call(get_neighbors, _From, State) ->
    {reply, State#state.neighbors, State};

handle_call(maintain, _From, State) ->
    NewState = maintain_topology(State),
    {reply, ok, NewState}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(maintain, State) ->
    NewState = maintain_topology(State),
    erlang:send_after(5000, self(), maintain),
    {noreply, NewState}.

maintain_topology(State) ->
    %% Get all known members
    Members = macula_membership:get_members(),

    %% Select desired neighbors using consistent hashing
    DesiredNeighbors = select_neighbors(Members, ?K_NEIGHBORS, State#state.ring_position),

    %% Current connections
    CurrentNeighbors = State#state.neighbors,

    %% Find missing connections
    Missing = DesiredNeighbors -- CurrentNeighbors,

    %% Find extra connections (if we're over K neighbors)
    Extra = CurrentNeighbors -- DesiredNeighbors,

    %% Connect to missing
    lists:foreach(fun(NodeId) ->
        case macula_connection:connect(NodeId) of
            {ok, _Conn} ->
                io:format("Connected to neighbor: ~p~n", [NodeId]);
            {error, Reason} ->
                io:format("Failed to connect to ~p: ~p~n", [NodeId, Reason])
        end
    end, Missing),

    %% Disconnect extra (if too many connections)
    if
        length(CurrentNeighbors) > ?K_NEIGHBORS * 2 ->
            lists:foreach(fun(NodeId) ->
                macula_connection:disconnect(NodeId)
            end, Extra);
        true ->
            ok
    end,

    %% Update state
    NewNeighbors = (CurrentNeighbors ++ Missing) -- Extra,
    State#state{
        neighbors = NewNeighbors,
        desired_neighbors = DesiredNeighbors
    }.

select_neighbors(Members, K, MyPosition) ->
    %% Place all members on hash ring
    Ring = lists:map(fun(Member) ->
        NodeId = Member#member.node_id,
        Position = crypto:hash(sha256, NodeId),
        {Position, NodeId}
    end, Members),

    %% Sort by position
    SortedRing = lists:sort(Ring),

    %% Find our position
    MyIndex = find_position(MyPosition, SortedRing),

    %% Select K clockwise neighbors (for redundancy, select K/2 clockwise + K/2 counter-clockwise)
    ClockwiseCount = K div 2,
    CounterClockwiseCount = K - ClockwiseCount,

    Clockwise = select_clockwise(MyIndex, ClockwiseCount, SortedRing),
    CounterClockwise = select_counter_clockwise(MyIndex, CounterClockwiseCount, SortedRing),

    Clockwise ++ CounterClockwise.

find_position(MyPosition, Ring) ->
    find_position(MyPosition, Ring, 0).

find_position(_MyPosition, [], _Index) ->
    0;
find_position(MyPosition, [{Position, _NodeId} | _Rest], Index) when Position >= MyPosition ->
    Index;
find_position(MyPosition, [_H | Rest], Index) ->
    find_position(MyPosition, Rest, Index + 1).

select_clockwise(MyIndex, Count, Ring) ->
    RingSize = length(Ring),
    Indices = [(MyIndex + I) rem RingSize || I <- lists:seq(1, Count)],
    [NodeId || {Idx, {_Pos, NodeId}} <- lists:zip(Indices, Ring), Idx =:= element(1, lists:nth(Idx + 1, lists:zip(lists:seq(0, RingSize - 1), Ring)))].

select_counter_clockwise(MyIndex, Count, Ring) ->
    RingSize = length(Ring),
    Indices = [(MyIndex - I + RingSize) rem RingSize || I <- lists:seq(1, Count)],
    [NodeId || {Idx, {_Pos, NodeId}} <- lists:zip(Indices, Ring), Idx =:= element(1, lists:nth(Idx + 1, lists:zip(lists:seq(0, RingSize - 1), Ring)))].

DHT Routing:

%% File: macula_routing.erl
%% Kademlia-inspired DHT routing for mesh

-module(macula_routing).
-export([route/2, find_node/1, find_closest_nodes/2]).

-define(K, 20).  %% Replication factor
-define(ALPHA, 3).  %% Concurrency parameter

%% Route message to destination node
route(DestNodeId, Message) ->
    case macula_connection:is_connected(DestNodeId) of
        true ->
            %% Direct connection, send immediately
            macula_connection:send(DestNodeId, Message);
        false ->
            %% Find next hop via DHT
            NextHop = find_next_hop(DestNodeId),
            forward(NextHop, DestNodeId, Message)
    end.

%% Find next hop closer to destination
find_next_hop(DestNodeId) ->
    MyNodeId = macula_identity:node_id(),

    %% Get connected neighbors
    Neighbors = macula_topology:get_neighbors(),

    %% Calculate XOR distance from each neighbor to destination
    Distances = lists:map(fun(NeighborId) ->
        Dist = xor_distance(NeighborId, DestNodeId),
        {Dist, NeighborId}
    end, Neighbors),

    %% Sort by distance (closest first)
    Sorted = lists:sort(Distances),

    %% Return closest neighbor
    case Sorted of
        [{_Dist, NextHop} | _] ->
            %% Check if NextHop is closer than us
            MyDist = xor_distance(MyNodeId, DestNodeId),
            if
                _Dist < MyDist -> NextHop;
                true -> DestNodeId  %% We're closest, destination must be dead
            end;
        [] ->
            %% No neighbors, can't route
            {error, no_route}
    end.

%% Forward message to next hop
forward(NextHop, FinalDest, Message) ->
    ForwardMsg = {forward, FinalDest, Message},
    macula_connection:send(NextHop, ForwardMsg).

%% XOR distance metric (like Kademlia)
xor_distance(A, B) when is_binary(A), is_binary(B) ->
    crypto:bytes_to_integer(crypto:exor(A, B));
xor_distance(A, B) ->
    xor_distance(term_to_binary(A), term_to_binary(B)).

%% Find node by ID (iterative lookup)
find_node(TargetId) ->
    find_node(TargetId, [], [macula_identity:node_id()]).

find_node(TargetId, Queried, Closest) ->
    %% Select ALPHA closest unqueried nodes
    ToQuery = select_unqueried(Closest, Queried, ?ALPHA),

    case ToQuery of
        [] ->
            %% No more nodes to query, return closest
            {ok, lists:sublist(Closest, ?K)};
        _ ->
            %% Query nodes in parallel
            Results = query_nodes(ToQuery, TargetId),

            %% Merge results
            NewClosest = merge_and_sort(Closest, Results, TargetId),
            NewQueried = Queried ++ ToQuery,

            %% Check if we found target
            case lists:member(TargetId, NewClosest) of
                true -> {ok, TargetId};
                false -> find_node(TargetId, NewQueried, NewClosest)
            end
    end.

query_nodes(Nodes, TargetId) ->
    %% Query each node for closer nodes
    lists:flatmap(fun(NodeId) ->
        case macula_rpc:call(NodeId, ?MODULE, find_closest_nodes, [TargetId, ?K], 1000) of
            {ok, Nodes} -> Nodes;
            {error, _} -> []
        end
    end, Nodes).

find_closest_nodes(TargetId, K) ->
    %% Return K closest known nodes to TargetId
    Members = macula_membership:get_members(),
    Distances = [{xor_distance(M#member.node_id, TargetId), M#member.node_id} || M <- Members],
    Sorted = lists:sort(Distances),
    {ok, [NodeId || {_Dist, NodeId} <- lists:sublist(Sorted, K)]}.

select_unqueried(Closest, Queried, Alpha) ->
    Unqueried = Closest -- Queried,
    lists:sublist(Unqueried, Alpha).

merge_and_sort(Closest, New, TargetId) ->
    All = lists:usort(Closest ++ New),
    Distances = [{xor_distance(NodeId, TargetId), NodeId} || NodeId <- All],
    Sorted = lists:sort(Distances),
    [NodeId || {_Dist, NodeId} <- Sorted].

(Continuing in next section due to length...)

Phase 3: NAT Traversal (Weeks 9-12)

Phase 4: WAMP Layer (Weeks 13-16)

Phase 5: Production Hardening (Weeks 17-20)


Architecture Diagrams

1. System Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                         Macula Mesh Network                         │
│                                                                     │
│  Internet / WAN                                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │                                                              │  │
│  │  ┌─────────┐          ┌─────────┐          ┌─────────┐     │  │
│  │  │ Node A  │◄────────►│ Node B  │◄────────►│ Node C  │     │  │
│  │  │ (USA)   │          │ (Europe)│          │ (Asia)  │     │  │
│  │  └────┬────┘          └────┬────┘          └────┬────┘     │  │
│  │       │                    │                    │          │  │
│  │       └────────────────────┼────────────────────┘          │  │
│  │                            │                                │  │
│  │                   HTTP/3 (QUIC/UDP)                         │  │
│  │                     Port 443/UDP                            │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                                                                     │
│  NAT/Firewall Traversal:                                           │
│  • STUN for public address discovery                               │
│  • ICE for connectivity checks                                     │
│  • UDP hole punching                                               │
│  • TURN relay as fallback                                          │
└─────────────────────────────────────────────────────────────────────┘

2. Node Internal Architecture

┌─────────────────────────────────────────────────────────────────────┐
│ Macula Node (BEAM VM)                                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │ Application Layer                                          │    │
│  │  • Elixir/Erlang Apps                                      │    │
│  │  • spawn/2, send/2, monitor/2                             │    │
│  │  • Transparent distribution                                │    │
│  └────────────────────────────────────────────────────────────┘    │
│                             ↓                                       │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │ WAMP Compatibility Layer (Optional)                        │    │
│  │  • publish/subscribe                                       │    │
│  │  • call/register (RPC)                                     │    │
│  └────────────────────────────────────────────────────────────┘    │
│                             ↓                                       │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │ Mesh Services                                              │    │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐    │    │
│  │  │ Discovery    │  │ Membership   │  │ Topology     │    │    │
│  │  │ (Bootstrap,  │  │ (SWIM        │  │ (k-regular   │    │    │
│  │  │  mDNS, DNS)  │  │  Gossip)     │  │  graph)      │    │    │
│  │  └──────────────┘  └──────────────┘  └──────────────┘    │    │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐    │    │
│  │  │ Routing      │  │ Pub/Sub      │  │ RPC          │    │    │
│  │  │ (DHT,        │  │ (Topic-based │  │ (Request/    │    │    │
│  │  │  Kademlia)   │  │  Registry)   │  │  Response)   │    │    │
│  │  └──────────────┘  └──────────────┘  └──────────────┘    │    │
│  └────────────────────────────────────────────────────────────┘    │
│                             ↓                                       │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │ Macula Distribution Protocol                               │    │
│  │  • Message framing (wire protocol)                         │    │
│  │  • Handshake & authentication                              │    │
│  │  • Stream multiplexing                                     │    │
│  │  • Process ↔ Stream mapping                               │    │
│  └────────────────────────────────────────────────────────────┘    │
│                             ↓                                       │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │ QUIC Transport (via quicer NIF)                            │    │
│  │  ┌──────────────────────────────────────────────────┐     │    │
│  │  │ MsQuic (C library)                                │     │    │
│  │  │  • RFC 9000 QUIC implementation                   │     │    │
│  │  │  • TLS 1.3 integrated                             │     │    │
│  │  │  • Streams, flow control, congestion control      │     │    │
│  │  └──────────────────────────────────────────────────┘     │    │
│  └────────────────────────────────────────────────────────────┘    │
│                             ↓                                       │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │ UDP Sockets (OS Network Stack)                             │    │
│  └────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘

3. Message Flow Diagram

Process A (Node 1)                                  Process B (Node 2)
      │                                                    │
      │ Pid ! Message                                     │
      ├──────────────────────┐                            │
      │                      │                            │
      ▼                      │                            │
 macula_dist                 │                            │
      │                      │                            │
      │ encode_send()        │                            │
      ├─────────────┐        │                            │
      │             │        │                            │
      ▼             │        │                            │
 macula_protocol    │        │                            │
      │             │        │                            │
      │ Frame:      │        │                            │
      │ [Ver|Type|  │        │                            │
      │  Flags|Len| │        │                            │
      │  Payload]   │        │                            │
      ├─────────────┘        │                            │
      │                      │                            │
      ▼                      │                            │
 macula_connection           │                            │
      │                      │                            │
      │ Get/Create Stream    │                            │
      ├─────────────┐        │                            │
      │             │        │                            │
      ▼             │        │                            │
 quicer (NIF)       │        │                            │
      │             │        │                            │
      │ quicer:send()        │                            │
      ├─────────────┘        │                            │
      │                      │                            │
      ▼                      │                            │
   MsQuic (C)                │                            │
      │                      │                            │
      │ QUIC Packet          │                            │
      │ (encrypted)          │                            │
      ├──────────────────────┘                            │
      │                                                   │
      ▼                                                   │
   UDP Socket                                             │
      │                                                   │
      │ ═════════════════════════════════════════════►   │
      │           Network (Internet)                     │
      │                                                  │
      │                                            UDP Socket
      │                                                  │
      │                                                  ▼
      │                                             MsQuic (C)
      │                                                  │
      │                                                  │ Decrypt
      │                                                  │ Reassemble
      │                                                  ├─────────┐
      │                                                  │         │
      │                                                  ▼         │
      │                                            quicer (NIF)    │
      │                                                  │         │
      │                                                  │ quicer:recv()
      │                                                  ├─────────┘
      │                                                  │
      │                                                  ▼
      │                                         macula_connection
      │                                                  │
      │                                                  │ Stream → Pid lookup
      │                                                  ├─────────┐
      │                                                  │         │
      │                                                  ▼         │
      │                                          macula_protocol   │
      │                                                  │         │
      │                                                  │ decode()│
      │                                                  ├─────────┘
      │                                                  │
      │                                                  ▼
      │                                            macula_dist
      │                                                  │
      │                                                  │ Deliver to process
      │                                                  ├────────┐
      │                                                  │        │
      │                                                  ▼        │
      │                                            Process B      │
      │                                                  │        │
      │                                                  │ receive │
      │                                                  │   Message
      │                                                  ├────────┘
      │                                                  │
      │                                                  ▼

4. Mesh Topology Diagram

┌──────────────────────────────────────────────────────────────────┐
│ Consistent Hash Ring (k-regular graph, k=6)                     │
│                                                                  │
│                          Node 3                                 │
│                            ●                                    │
│                       ╱         ╲                               │
│                  ╱                  ╲                            │
│             ╱                           ╲                        │
│        ╱                                    ╲                    │
│   Node 2 ●─────────────────────────────●  Node 4               │
│       │   ╲                         ╱   │                       │
│       │       ╲                 ╱       │                       │
│       │           ╲         ╱           │                       │
│       │               ● Node 5          │                       │
│       │           ╱         ╲           │                       │
│       │       ╱                 ╲       │                       │
│       │   ╱                         ╲   │                       │
│   Node 1 ●─────────────────────────────● Node 6                │
│        ╲                                    ╱                    │
│             ╲                           ╱                        │
│                  ╲                  ╱                            │
│                       ╲         ╱                               │
│                            ●                                    │
│                        Node 7                                   │
│                                                                  │
│  Each node connects to K neighbors (K=6 in this example)       │
│  • K/2 clockwise neighbors (3)                                 │
│  • K/2 counter-clockwise neighbors (3)                         │
│                                                                  │
│  Properties:                                                    │
│  • Low diameter (O(log n) hops between any two nodes)         │
│  • High fault tolerance (multiple paths)                       │
│  • Scalable (each node has fixed K connections)               │
└──────────────────────────────────────────────────────────────────┘

5. NAT Traversal Flow

┌──────────────────────────────────────────────────────────────────┐
│ NAT Traversal Using STUN + ICE                                   │
│                                                                  │
│  Node A (behind NAT)          STUN Server         Node B (public)│
│       │                            │                      │      │
│       │  1. STUN Binding Request   │                      │      │
│       ├───────────────────────────►│                      │      │
│       │                            │                      │      │
│       │  2. STUN Response          │                      │      │
│       │     (Public IP: 1.2.3.4)   │                      │      │
│       │◄───────────────────────────┤                      │      │
│       │                            │                      │      │
│       │  3. Register with Signaling Server               │      │
│       │     POST /register                                │      │
│       │     { candidates: [                               │      │
│       │       {type: "host", addr: "192.168.1.100"},      │      │
│       │       {type: "srflx", addr: "1.2.3.4"}            │      │
│       │     ]}                                             │      │
│       ├───────────────────────────────────────────────────►│      │
│       │                                                    │      │
│       │  4. Query for Node B candidates                   │      │
│       │     GET /lookup/node_b                            │      │
│       ├───────────────────────────────────────────────────►│      │
│       │                                                    │      │
│       │  5. Receive Node B candidates                     │      │
│       │     { candidates: [{type: "host", addr: "5.6.7.8"}]}    │
│       │◄───────────────────────────────────────────────────┤      │
│       │                                                    │      │
│       │  6. Connectivity Checks (ICE)                     │      │
│       │     Send STUN probes to all candidate pairs       │      │
│       ├───────────────────────────────────────────────────►│      │
│       │◄───────────────────────────────────────────────────┤      │
│       │                                                    │      │
│       │  7. Select best candidate pair                    │      │
│       │     (Direct: 1.2.3.4 ↔ 5.6.7.8)                  │      │
│       │                                                    │      │
│       │  8. Establish QUIC Connection                     │      │
│       │═══════════════════════════════════════════════════►│      │
│       │                                                    │      │
│       │  9. Communication over QUIC/UDP                   │      │
│       │◄══════════════════════════════════════════════════►│      │
│       │                                                    │      │
└──────────────────────────────────────────────────────────────────┘

6. Development Roadmap Gantt Chart

Week  Phase   Milestone
────────────────────────────────────────────────────────────────────
 1-2  │■■■■■■│ QUIC Transport Layer
      │      │ • quicer integration
      │      │ • Basic client/server
      │      │ • Bidirectional streams
────────────────────────────────────────────────────────────────────
 3    │  ■■■ │ Message Framing Protocol
      │      │ • Wire protocol spec
      │      │ • Encode/decode
      │      │ • Handshake
────────────────────────────────────────────────────────────────────
 4    │   ■■■│ Distribution Protocol
      │      │ • net_kernel driver
      │      │ • Basic messaging
      │      │ • Remote spawn
────────────────────────────────────────────────────────────────────
 5-6  │    ■■■■■│ Node Discovery
      │        │ • Bootstrap
      │        │ • mDNS
      │        │ • SWIM membership
────────────────────────────────────────────────────────────────────
 7    │       ■■■│ Topology & Routing
      │          │ • k-regular graph
      │          │ • DHT routing
────────────────────────────────────────────────────────────────────
 8    │        ■■│ Testing & Validation
      │          │ • Chaos testing
      │          │ • Benchmarks
────────────────────────────────────────────────────────────────────
 9-10 │         ■■■■│ NAT Traversal
      │             │ • STUN client
      │             │ • ICE implementation
────────────────────────────────────────────────────────────────────
11-12 │           ■■■■│ Hole Punching
      │               │ • UDP hole punch
      │               │ • TURN relay
────────────────────────────────────────────────────────────────────
13-14 │             ■■■■│ Distributed Pub/Sub
      │                 │ • Topic registry
      │                 │ • Content routing
────────────────────────────────────────────────────────────────────
15-16 │               ■■■■│ RPC Layer
      │                   │ • Sync RPC
      │                   │ • WAMP compat
────────────────────────────────────────────────────────────────────
17    │                 ■■│ Security
      │                   │ • TLS certs
      │                   │ • Rate limiting
────────────────────────────────────────────────────────────────────
18    │                  ■■│ Monitoring
      │                    │ • Metrics
      │                    │ • Visualization
────────────────────────────────────────────────────────────────────
19    │                   ■■│ Performance
      │                     │ • Optimization
      │                     │ • Benchmarks
────────────────────────────────────────────────────────────────────
20    │                    ■■│ Documentation
      │                      │ • Arch guide
      │                      │ • API docs
────────────────────────────────────────────────────────────────────

Legend:
■ = Active development

Technical Deep Dives

Deep Dive 1: QUIC vs TCP for Distributed Erlang

Why QUIC is Better for Distributed Erlang:

1. Head-of-Line Blocking

TCP Problem:

Process A sends: ──[Packet 1]──[Packet 2]──[Packet 3*LOST]──[Packet 4]──►
                                          ▲
                                          │
                        All packets 4+ blocked until 3 retransmitted!

Process B waiting to receive Packet 4:  ⏳ BLOCKED

QUIC Solution:

Stream 0 (Process A): ──[Pkt 1]──[Pkt 2]──[Pkt 3*LOST]──►
                                          ▲ Retransmit only this
                                          │
Stream 1 (Process B): ──[Pkt 1]──[Pkt 2]──[Pkt 3]──► ✓ NOT BLOCKED!

Independent streams = No cross-stream blocking

2. Connection Migration

TCP Problem:

Mobile device moves from WiFi to Cellular:

WiFi IP:     192.168.1.100:5000
             ↓ [Connection established]
             ↓ [Active transfers]
             ↓ [Network switches]
Cellular IP: 10.20.30.40:6000
             ↓ [TCP connection LOST]
             ↓ [Must re-establish: 3 RTT]
             ↓ [Resume transfers]

QUIC Solution:

WiFi IP:     192.168.1.100:5000
             ↓ [Connection ID: 0x1a2b3c4d]
             ↓ [Active transfers]
             ↓ [Network switches]
Cellular IP: 10.20.30.40:6000
             ↓ [Same Connection ID!]
             ↓ [Continue immediately: 0 RTT]
             ↓ [Transfers uninterrupted]

3. 0-RTT Resumption

TCP + TLS 1.2:

Client → Server: SYN                      RTT 1
Server → Client: SYN-ACK
Client → Server: ACK
Client → Server: ClientHello              RTT 2
Server → Client: ServerHello + Certificate
Client → Server: Finished
Server → Client: Finished
Client → Server: HTTP Request             RTT 3

Total: 3 RTT before application data

QUIC (with 0-RTT token):

Client → Server: Initial + 0-RTT Data + App Request   RTT 0

Total: 0 RTT! Data sent immediately

This is HUGE for edge devices that frequently reconnect!

4. Multiplexing Efficiency

HTTP/2 over TCP:

┌──────────────────────────────────────┐
│ TCP Connection                       │
│  ┌──────────────────────────────┐   │
│  │ HTTP/2 Multiplexing          │   │
│  │  Stream 1, Stream 2, ...     │   │
│  └──────────────────────────────┘   │
│                                      │
│ Problem: TCP sees bytes, not streams │
│ Single packet loss blocks ALL streams│
└──────────────────────────────────────┘

HTTP/3 over QUIC:

┌──────────────────────────────────────┐
│ QUIC Connection                      │
│  Stream 1 (independent)              │
│  Stream 2 (independent)              │
│  Stream 3 (independent)              │
│  ...                                 │
│                                      │
│ QUIC understands streams natively!   │
│ Packet loss only affects one stream  │
└──────────────────────────────────────┘

For Erlang distribution with millions of processes, this is critical!


Deep Dive 2: SWIM Gossip Protocol

SWIM: Scalable Weakly-consistent Infection-style Process Group Membership

Why SWIM?

Traditional heartbeat protocols don't scale:

N nodes sending heartbeats to all others:
  Network load: O(N²) messages per period

Example: 1000 nodes, 1 sec heartbeat
  = 1,000,000 messages/sec
  = NOT SCALABLE

SWIM uses gossip:

Each node:
  - Pings 1 random member per period
  - Gossips to K random members

Network load: O(N) messages per period

Example: 1000 nodes, K=3
  = 1000 pings + 3000 gossip = 4000 messages/sec
  = SCALABLE!

SWIM Algorithm

Every protocol period (1 second):

1. SELECT random member M
2. PING M (wait for ACK)
   ├─ If ACK received → M is alive
   └─ If timeout → INDIRECT PING

3. INDIRECT PING:
   ├─ Select K random members (e.g., K=3)
   ├─ Ask each to ping M on your behalf
   └─ If any ACK → M is alive
       If all timeout → M is SUSPECT

4. SUSPECT handling:
   ├─ Don't immediately mark M as dead
   ├─ Give time for refutation (5 seconds)
   ├─ M can increase its "incarnation number" to refute
   └─ If no refutation → M is DEAD

5. GOSSIP:
   ├─ Select K random members
   └─ Send recent membership changes
       (new members, state changes, etc.)

Suspicion Mechanism

Timeline:

T+0s:  Node fails to respond to ping
       │
       ▼
       Mark as SUSPECT (not dead!)
       │
       ├─ Broadcast "Node X is suspect"
       │  via gossip
       │
T+1s:  │  Other nodes hear rumor
       │  └─► Try to ping Node X
       │       └─► Some may succeed!
       │
T+2s:  │  Node X hears it's suspected
       │  └─► Refutes by increasing
       │       incarnation number
       │
T+5s:  ▼
       If no refutation → Mark as DEAD
       │
       └─ Broadcast "Node X is dead"

This prevents false positives from temporary network glitches!

Gossip Dissemination

Epidemic-style spread:

T+0: Node A detects change (Node X joined)
     ┌───────┐
     │   A   │ knows: X joined
     └───────┘

T+1: A gossips to B, C, D (K=3 random)
     ┌───────┐   ┌───────┐   ┌───────┐   ┌───────┐
     │   A   │──►│   B   │──►│   C   │──►│   D   │
     └───────┘   └───────┘   └───────┘   └───────┘
                  knows       knows       knows

T+2: B, C, D gossip to 3 others each (exponential spread)
     9 nodes know

T+3: 27 nodes know

T+log₃(N): All nodes know!

Convergence time: O(log N)


Deep Dive 3: Kademlia DHT for Routing

Why DHT (Distributed Hash Table)?

In large mesh networks, full mesh (N² connections) doesn't scale:

Nodes  Connections  Problem
  10       45       OK
 100     4,950      Getting expensive
1000   499,500      IMPOSSIBLE

DHT enables O(log N) routing:

Nodes  Hops (log₂ N)
  10        3
 100        6
1000       10

Kademlia Basics

XOR Distance Metric:

Node IDs are 256-bit hashes (SHA256)

Node A: 0x3a7f...
Node B: 0x8c12...

Distance = A XOR B
         = 0x3a7f... XOR 0x8c12...
         = 0xb66d...

Properties:
  • d(A, B) = d(B, A)  (symmetric)
  • d(A, A) = 0
  • d(A, B) + d(B, C) >= d(A, C)  (triangle inequality)

k-buckets:

Each node maintains k-buckets for distance ranges:

Bucket 0:  Distance 2^0  to 2^1   (1 hop away)
Bucket 1:  Distance 2^1  to 2^2   (2 hops)
Bucket 2:  Distance 2^2  to 2^3   (4 hops)
...
Bucket 255: Distance 2^255 to 2^256 (very far)

Each bucket stores up to K nodes (e.g., K=20)

Routing:

To send message to target T:

1. Calculate distance: d = XOR(my_id, T)
2. Find bucket for distance d
3. Select closest node N from bucket
4. Forward to N
5. Repeat until reached T

Max hops: log₂(total_nodes)

Iterative Node Lookup

find_node(TargetId) ->
    % Start with K closest known nodes
    Closest = get_k_closest(TargetId, K),
    find_node_iter(TargetId, Closest, [], K).

find_node_iter(TargetId, Closest, Queried, K) ->
    % Select ALPHA unqueried nodes to ask
    ToQuery = select_closest_unqueried(Closest, Queried, ALPHA),

    if
        ToQuery =:= [] ->
            % No more to query, return result
            lists:sublist(Closest, K);
        true ->
            % Query nodes in parallel
            Results = pmap(fun(Node) ->
                rpc:call(Node, kademlia, get_closest, [TargetId, K])
            end, ToQuery),

            % Merge results and sort by distance
            NewClosest = merge_and_sort(Closest, lists:flatten(Results), TargetId),
            NewQueried = Queried ++ ToQuery,

            % Check if target found
            case lists:member(TargetId, NewClosest) of
                true -> {found, TargetId};
                false -> find_node_iter(TargetId, NewClosest, NewQueried, K)
            end
    end.

Complexity: O(log N) lookups, O(ALPHA * log N) messages


Success Metrics

Phase 1 Success Criteria

  • ✅ QUIC Transport Works

    • Server accepts connections
    • Client can connect
    • Bidirectional streams function
    • Connection survives stream closure
  • ✅ Wire Protocol Implemented

    • Messages encode/decode correctly
    • Handshake completes successfully
    • All message types supported
  • ✅ Basic Distribution Functions

    • net_kernel:connect_node/1 works
    • nodes() shows connected nodes
    • Message sending: {Name, Node} ! Msg
    • Remote spawn: spawn(Node, Fun)
    • Process linking works
    • Monitoring works

Deliverable: Two Erlang nodes communicating over HTTP/3


Phase 2 Success Criteria

  • ✅ Node Discovery Works

    • Bootstrap discovery functional
    • mDNS discovery works locally
    • DNS SRV discovery works
  • ✅ Membership Protocol Stable

    • SWIM protocol running
    • Failures detected within 10 seconds
    • Gossip converges in O(log N) time
    • No false positives in stable network
  • ✅ Topology Self-Organizes

    • Nodes form k-regular graph
    • New nodes join smoothly
    • Departed nodes removed from topology
    • Healing after network partition
  • ✅ Routing Functions

    • Messages route through mesh
    • DHT lookup finds nodes
    • O(log N) hops for routing

Deliverable: 20+ node mesh that self-heals from failures


Phase 3 Success Criteria

  • ✅ NAT Traversal Works

    • STUN discovers public address
    • ICE negotiates connectivity
    • UDP hole punching succeeds (>80%)
    • TURN relay works as fallback
  • ✅ Real-World Scenarios

    • Home router NAT traversed
    • Corporate firewall traversed
    • Mobile hotspot NAT traversed
    • Symmetric NAT handled

Deliverable: Nodes behind different NATs forming mesh


Phase 4 Success Criteria

  • ✅ Pub/Sub Works

    • Topic subscriptions work
    • Messages routed by topic
    • Pattern matching (prefix/wildcard)
    • Scalable (not flooding all nodes)
  • ✅ RPC Works

    • Synchronous RPC calls
    • Timeouts handled correctly
    • Error propagation works
  • ✅ WAMP Compatible

    • Existing WAMP clients can connect
    • Subscribe/Publish semantics match
    • Call/Register semantics match

Deliverable: Example Platform PoC runs on Macula Mesh


Phase 5 Success Criteria

  • ✅ Security Hardened

    • TLS certificates managed
    • Message authentication
    • Rate limiting prevents DoS
    • Access control enforced
  • ✅ Production Monitoring

    • Prometheus metrics exported
    • Topology visualization works
    • Alerts fire on anomalies
    • Distributed tracing available
  • ✅ Performance Acceptable

    • <10ms latency (local)
    • <100ms latency (internet)
    • 10,000 msg/sec throughput

    • Scales to 1000+ nodes
  • ✅ Documentation Complete

    • Architecture guide published
    • API reference docs
    • Deployment guide
    • Migration from Bondy guide

Deliverable: Production-ready Macula Mesh 1.0


Conclusion

Macula HTTP/3 Mesh represents a unique opportunity to build world-class distributed infrastructure for the BEAM ecosystem. By leveraging QUIC's modern transport capabilities, we can create a mesh network that:

  1. Works anywhere - Through NATs, firewalls, mobile networks
  2. Scales effortlessly - O(log N) routing, not O(N²) connections
  3. Feels native - Standard Erlang distribution semantics
  4. Performs brilliantly - 0-RTT reconnection, no head-of-line blocking
  5. Stands out - Nobody else has this for BEAM

This is a 20-week journey that will culminate in a "Wow, how do they do it?" moment.

Next Steps:

  1. Set up development environment
  2. Integrate quicer dependency
  3. Build Week 1 deliverable (QUIC echo server/client)
  4. Start the journey! 🚀

References

QUIC & HTTP/3

Libraries

Algorithms


Document Version: 1.0 Last Updated: 2025-11-07 Author: Macula Architecture Team Status: Proposal