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:
parent
1f5bf7318d
commit
c7275b5618
12 changed files with 510 additions and 1 deletions
73
docs/diagrams/mesh-data-flow.dot
Normal file
73
docs/diagrams/mesh-data-flow.dot
Normal 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];
|
||||
}
|
||||
BIN
docs/diagrams/mesh-data-flow.png
Normal file
BIN
docs/diagrams/mesh-data-flow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
38
docs/diagrams/mesh-epoch-lifecycle.dot
Normal file
38
docs/diagrams/mesh-epoch-lifecycle.dot
Normal 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"];
|
||||
}
|
||||
}
|
||||
BIN
docs/diagrams/mesh-epoch-lifecycle.png
Normal file
BIN
docs/diagrams/mesh-epoch-lifecycle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
50
docs/diagrams/mesh-group-decisions.dot
Normal file
50
docs/diagrams/mesh-group-decisions.dot
Normal 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];
|
||||
}
|
||||
BIN
docs/diagrams/mesh-group-decisions.png
Normal file
BIN
docs/diagrams/mesh-group-decisions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
51
docs/diagrams/mesh-identity-stack.dot
Normal file
51
docs/diagrams/mesh-identity-stack.dot
Normal 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;}
|
||||
}
|
||||
BIN
docs/diagrams/mesh-identity-stack.png
Normal file
BIN
docs/diagrams/mesh-identity-stack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
63
docs/diagrams/mesh-secret-envelope.dot
Normal file
63
docs/diagrams/mesh-secret-envelope.dot
Normal 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];
|
||||
}
|
||||
BIN
docs/diagrams/mesh-secret-envelope.png
Normal file
BIN
docs/diagrams/mesh-secret-envelope.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
Loading…
Add table
Add a link
Reference in a new issue