docs: mesh multiplayer protocol with dot diagrams

Adds docs/mesh.md and five graphviz diagrams covering the federation
layer: identity stack, epoch state machine, per-member secret envelope,
gossip wire contract, and operator decision tree. Pins the protocol
contract for the upcoming HTTP wire (mesh sync, mesh serve).

Makefile gets a 'docs' target with pattern rule so PNG renders are
incremental from .dot sources.
This commit is contained in:
russell@unturf.com 2026-04-28 12:54:36 -04:00
parent 1f5bf7318d
commit c7275b5618
No known key found for this signature in database
12 changed files with 510 additions and 1 deletions

View file

@ -0,0 +1,73 @@
// aborist/mesh — what flows between peers.
//
// Aborist is content-addressed: two peers ingesting the same dump
// compute bit-identical document_roots. Gossip exchanges identifiers,
// derivations, falsifications, and witness signatures. Bulk content
// only flows on cache miss, by Merkle proof of inclusion.
//
// HTTP wire (mesh sync, mesh serve) is the next milestone; this
// diagram fixes the protocol contract that wire will implement.
//
// Render: dot -Tpng docs/diagrams/mesh-data-flow.dot -o /tmp/x.png
digraph mesh_data_flow {
rankdir=LR;
bgcolor="white";
compound=true;
node [shape=box, style="rounded,filled", fontname="Helvetica"];
edge [fontname="Helvetica", fontsize=10];
subgraph cluster_alice {
label="peer: alice (admin)";
style="rounded,filled";
fillcolor="#fff7d6";
a_docs [label="documents\n(surface + core)", fillcolor="#ffffff"];
a_deriv [label="derivations\n(surface -> core proof_blob)", fillcolor="#ffffff"];
a_prov [label="providence_cache\n(8-dim cache_key, audit_mode)", fillcolor="#ffffff"];
a_falsif [label="falsifications\n('this answer is wrong')", fillcolor="#ffffff"];
a_audit [label="audit_events\n(append-only chain)", fillcolor="#ffffff"];
}
subgraph cluster_bob {
label="peer: bob (member)";
style="rounded,filled";
fillcolor="#d6ecff";
b_docs [label="documents\n(surface + core)", fillcolor="#ffffff"];
b_deriv [label="derivations", fillcolor="#ffffff"];
b_prov [label="providence_cache", fillcolor="#ffffff"];
b_falsif [label="falsifications", fillcolor="#ffffff"];
b_audit [label="audit_events", fillcolor="#ffffff"];
}
subgraph cluster_wire {
label="gossip messages (Ed25519-signed, optional AEAD)";
style="rounded,dashed";
color="#666666";
m_root [label="ANNOUNCE_ROOT\ndocument_root, source_uri,\nchunking_version, schema_version", fillcolor="#e8ffe8"];
m_der [label="ANNOUNCE_DERIVATION\ncore_root <- surface_root(s),\ndistiller_id, proof_blob hash", fillcolor="#e8ffe8"];
m_prov [label="ANNOUNCE_PROVIDENCE\ncache_key, audit_mode,\nanswer_hash", fillcolor="#e8ffe8"];
m_fals [label="ANNOUNCE_FALSIFICATION\ncache_key, reason, signed by witness", fillcolor="#ffe0e0"];
m_pull [label="REQUEST_BODY\ndocument_root or chunk_root\n(only on local miss)", fillcolor="#fff7d6"];
m_resp [label="DELIVER_BODY\nbytes + Merkle proof of inclusion\nrecipient verifies before insert", fillcolor="#fff7d6"];
}
a_docs -> m_root [lhead=cluster_wire];
a_deriv -> m_der [lhead=cluster_wire];
a_prov -> m_prov [lhead=cluster_wire];
a_falsif -> m_fals [lhead=cluster_wire];
m_root -> b_docs [ltail=cluster_wire];
m_der -> b_deriv [ltail=cluster_wire];
m_prov -> b_prov [ltail=cluster_wire];
m_fals -> b_falsif [ltail=cluster_wire];
b_docs -> m_pull [label="miss on root", color="#cc6600"];
m_pull -> a_docs [color="#cc6600"];
a_docs -> m_resp [color="#cc6600"];
m_resp -> b_docs [label="verify proof,\nthen ingest", color="#cc6600"];
a_audit -> b_audit [label="audit chain merge\n(prev_event_hash gates inserts)", style=dashed, color="#0066cc", constraint=false];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View file

@ -0,0 +1,38 @@
// aborist/mesh — epoch state machine.
//
// Every roster mutation bumps a counter. Old epochs persist forever
// (signatures stay verifiable). New gossip is encrypted under the
// post-rotation secret, so evicted members go opaque from epoch+1.
//
// Render: dot -Tpng docs/diagrams/mesh-epoch-lifecycle.dot -o /tmp/x.png
digraph mesh_epoch_lifecycle {
rankdir=LR;
bgcolor="white";
node [shape=box, style="rounded,filled", fontname="Helvetica"];
edge [fontname="Helvetica", fontsize=10];
none [label="no mesh state\n(meta.mesh.enabled = 0)", fillcolor="#eeeeee"];
e0 [label="epoch 0\nfounder = sole admin\nreason: 'genesis'", fillcolor="#fff7d6"];
e1 [label="epoch N\nroster + envelope\nsigned by actor", fillcolor="#d6ffd6"];
eN [label="epoch N+1\nfresh secret\nfresh envelope", fillcolor="#d6ffd6"];
historic [label="every prior epoch\nstays on disk\n(roster + signatures verifiable)", fillcolor="#ffffff", shape=note];
none -> e0 [label="aborist mesh init --group <name>"];
e0 -> e1 [label="add | kick | rotate"];
e1 -> eN [label="add | kick | rotate"];
eN -> eN [label="rotate (no roster change)"];
e1 -> historic [style=dashed, arrowhead=none, color="#888888"];
eN -> historic [style=dashed, arrowhead=none, color="#888888"];
subgraph cluster_ops {
label="Operator commands";
style="rounded,dashed";
color="#666666";
node [fillcolor="#e8f0ff", shape=note];
op_add [label="aborist mesh add\n--member-id bob ...\nadmin only"];
op_kick [label="aborist mesh kick\n--member-id bob ...\nadmin only"];
op_rotate [label="aborist mesh rotate\nany current member"];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -0,0 +1,50 @@
// aborist/mesh — group operator decision tree.
//
// When a group steward (admin) faces a roster question, this diagram
// shows which CLI verb solves it and what guarantee comes out the
// other side.
//
// Render: dot -Tpng docs/diagrams/mesh-group-decisions.dot -o /tmp/x.png
digraph mesh_group_decisions {
rankdir=TB;
bgcolor="white";
node [fontname="Helvetica"];
edge [fontname="Helvetica", fontsize=10];
start [label="group steward has a question", shape=ellipse, style="filled", fillcolor="#eeeeee"];
q_first [label="is mesh\ninitialized\nyet?", shape=diamond, style="filled", fillcolor="#fff7d6"];
q_role [label="is the question\nabout people\nor secrets?", shape=diamond, style="filled", fillcolor="#fff7d6"];
q_who [label="add, remove,\nor refresh?", shape=diamond, style="filled", fillcolor="#fff7d6"];
q_admin [label="are you\nadmin in\ncurrent epoch?", shape=diamond, style="filled", fillcolor="#fff7d6"];
a_init [label="aborist mesh init --group <name>\nthen mesh enable\n(creates epoch 0; founder = sole admin)", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
a_add [label="aborist mesh add\n--member-id <id>\n--sign-pub <hex>\n--dh-pub <hex>\n[--role admin]", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
a_kick [label="aborist mesh kick\n--member-id <id>\n--reason '...'", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
a_rotate [label="aborist mesh rotate\n--reason '...'\n(any current member)", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
g_admin [label="permission denied\n(only admins of the\ncurrent epoch may add or kick)", shape=box, style="rounded,filled", fillcolor="#ffe0e0"];
g_add [label="new member can decrypt\nepoch+1 forward;\ncannot reach prior epochs", shape=note, style="filled", fillcolor="#ffffff"];
g_kick [label="kicked member's prior\nsignatures stay valid forever;\nopaque to gossip from epoch+1 on", shape=note, style="filled", fillcolor="#ffffff"];
g_rot [label="fresh secret on the same roster;\nuse on suspected secret leak\nor scheduled hygiene", shape=note, style="filled", fillcolor="#ffffff"];
start -> q_first;
q_first -> a_init [label="no"];
q_first -> q_role [label="yes"];
q_role -> q_who [label="people"];
q_role -> a_rotate [label="just rotate the secret"];
q_who -> q_admin [label="add or kick"];
q_who -> a_rotate [label="refresh secret only"];
q_admin -> a_add [label="yes (admin) -> add"];
q_admin -> a_kick [label="yes (admin) -> kick"];
q_admin -> g_admin [label="no"];
a_add -> g_add [arrowhead=none, style=dotted];
a_kick -> g_kick [arrowhead=none, style=dotted];
a_rotate -> g_rot [arrowhead=none, style=dotted];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View file

@ -0,0 +1,51 @@
// aborist/mesh — cryptographic identity stack per peer.
//
// Each peer carries two keypairs and one shared epoch secret.
// Keys are raw 32-byte forms; everything lives in the standard SQLite db.
//
// Render: dot -Tpng docs/diagrams/mesh-identity-stack.dot -o /tmp/x.png
digraph mesh_identity_stack {
rankdir=TB;
bgcolor="white";
node [shape=box, style="rounded,filled", fontname="Helvetica"];
edge [fontname="Helvetica", fontsize=10];
subgraph cluster_peer {
label="One peer (e.g. alice)";
style="rounded,dashed";
color="#666666";
sign_priv [label="Ed25519 priv\n(sign_priv, 32B)", fillcolor="#ffe8a3"];
sign_pub [label="Ed25519 pub\n(sign_pub, 32B)\nshared with group", fillcolor="#fff7d6"];
dh_priv [label="X25519 priv\n(dh_priv, 32B)", fillcolor="#a3d8ff"];
dh_pub [label="X25519 pub\n(dh_pub, 32B)\nshared with group", fillcolor="#d6ecff"];
member_id [label="member_id\n8 hex chars", fillcolor="#eeeeee"];
sign_priv -> sign_pub [label="public_key()"];
dh_priv -> dh_pub [label="public_key()"];
}
subgraph cluster_group {
label="Per-epoch group state";
style="rounded,dashed";
color="#666666";
roster [label="mesh_roster\n(epoch, member_id, sign_pub, dh_pub, role)", fillcolor="#e8ffe8"];
epoch [label="mesh_epochs\n(epoch_id, secret_envelope, started_event_hash)", fillcolor="#e8ffe8"];
secret [label="symmetric epoch secret\n32 random bytes\nnever stored in clear", fillcolor="#ffd6d6"];
epoch -> secret [label="wrapped per member via ECDH"];
}
audit [label="audit_events\n(prev_event_hash, event_hash)\nappend-only Merkle chain", fillcolor="#ffffff", shape=note];
sign_priv -> audit [label="signs each\nmembership op", style=dashed];
sign_pub -> roster [label="enrolled at"];
dh_pub -> roster [label="enrolled at"];
dh_priv -> secret [label="ECDH-unwraps\nself's envelope entry", style=dashed, color="#0066cc"];
member_id -> roster [label="keys by"];
{rank=same; sign_priv; dh_priv;}
{rank=same; sign_pub; dh_pub;}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -0,0 +1,63 @@
// aborist/mesh — per-member secret envelope.
//
// Every epoch carries one fresh 32-byte symmetric secret. That single
// secret is wrapped N times, once per member, using ECDH between the
// rotator's X25519 priv and each member's X25519 pub. Each member
// unwraps only their own slot. Evicted members get no slot at all.
//
// Render: dot -Tpng docs/diagrams/mesh-secret-envelope.dot -o /tmp/x.png
digraph mesh_secret_envelope {
rankdir=LR;
bgcolor="white";
node [shape=box, style="rounded,filled", fontname="Helvetica"];
edge [fontname="Helvetica", fontsize=10];
secret [label="epoch_N_secret\n32 random bytes", fillcolor="#ffd6d6", shape=cylinder];
subgraph cluster_wrap {
label="rotate_epoch() — done by actor (admin)";
style="rounded,dashed";
color="#666666";
ecdh_a [label="ECDH(rotator_dh_priv,\n alice_dh_pub)\n -> shared_a (HKDF)", fillcolor="#e8f0ff"];
ecdh_b [label="ECDH(rotator_dh_priv,\n bob_dh_pub)\n -> shared_b (HKDF)", fillcolor="#e8f0ff"];
ecdh_c [label="ECDH(rotator_dh_priv,\n carol_dh_pub)\n -> shared_c (HKDF)", fillcolor="#e8f0ff"];
wrap_a [label="ChaCha20-Poly1305\n(shared_a, nonce_a, secret,\n aad=alice.member_id)"];
wrap_b [label="ChaCha20-Poly1305\n(shared_b, nonce_b, secret,\n aad=bob.member_id)"];
wrap_c [label="ChaCha20-Poly1305\n(shared_c, nonce_c, secret,\n aad=carol.member_id)"];
secret -> wrap_a;
secret -> wrap_b;
secret -> wrap_c;
ecdh_a -> wrap_a [label="key"];
ecdh_b -> wrap_b [label="key"];
ecdh_c -> wrap_c [label="key"];
}
envelope [label="mesh_epochs.secret_envelope (JSON)\n{\n alice: {nonce_b64, ct_b64},\n bob: {nonce_b64, ct_b64},\n carol: {nonce_b64, ct_b64}\n}", fillcolor="#fff7d6", shape=note];
wrap_a -> envelope [label="alice slot"];
wrap_b -> envelope [label="bob slot"];
wrap_c -> envelope [label="carol slot"];
subgraph cluster_unwrap {
label="Each peer unwraps only their own slot";
style="rounded,dashed";
color="#666666";
node [fillcolor="#d6ffd6"];
u_alice [label="alice:\nECDH(alice_dh_priv,\n rotator_dh_pub)\nthen AEAD-decrypt slot.alice"];
u_bob [label="bob:\nECDH(bob_dh_priv,\n rotator_dh_pub)\nthen AEAD-decrypt slot.bob"];
u_carol [label="carol:\nECDH(carol_dh_priv,\n rotator_dh_pub)\nthen AEAD-decrypt slot.carol"];
}
envelope -> u_alice;
envelope -> u_bob;
envelope -> u_carol;
evicted [label="evicted dave\nno slot in envelope\nValueError on unwrap", fillcolor="#ffe0e0", shape=octagon];
envelope -> evicted [label="dave NOT included", color="#cc0000", style=dashed];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB