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
10
Makefile
10
Makefile
|
|
@ -30,7 +30,7 @@ SEARCH_Q ?= computer
|
|||
ingest-xml-attached ingest-abstract \
|
||||
ingest-grok ingest-grok-media \
|
||||
ingest-self ingest-git ingest-hg \
|
||||
verify search stats test clean clean-db clean-data help
|
||||
verify search stats test docs clean clean-db clean-data help
|
||||
|
||||
all: bootstrap fetch-cur ingest-cur verify stats ## bootstrap → fetch cur → ingest cur → verify → stats
|
||||
|
||||
|
|
@ -282,6 +282,14 @@ stats: bootstrap ## counts: documents, chunks, edges, audit chain
|
|||
test: bootstrap ## run pytest suite
|
||||
$(VENV)/bin/pytest -q
|
||||
|
||||
DOT_SRCS := $(wildcard docs/diagrams/*.dot)
|
||||
DOT_PNGS := $(DOT_SRCS:.dot=.png)
|
||||
|
||||
docs/diagrams/%.png: docs/diagrams/%.dot
|
||||
dot -Tpng $< -o $@
|
||||
|
||||
docs: $(DOT_PNGS) ## render docs/diagrams/*.dot -> .png via graphviz
|
||||
|
||||
# Reproducible micro-benchmark over a fixed slice of cur. Lets you compare
|
||||
# ETL throughput across configs and catches regressions on optimization
|
||||
# work. Override BENCH_DOCS=N (default 5000).
|
||||
|
|
|
|||
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 |
226
docs/mesh.md
Normal file
226
docs/mesh.md
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
# Mesh — multiplayer aborist
|
||||
|
||||
Aborist on a single laptop is a content-addressed forest of documents.
|
||||
Aborist across many laptops is a *gossip-able* forest. Two peers that
|
||||
ingest the same Wikipedia dump compute bit-identical `document_root`s,
|
||||
so any peer can verify another peer's claim by re-deriving the hash
|
||||
locally. Mesh is wire-and-trust scaffolding wrapped around that fact.
|
||||
|
||||
This file documents how groups manage data across each other. It pairs
|
||||
with diagrams in `docs/diagrams/`.
|
||||
|
||||
> Status: cryptographic foundation, state machine, and CLI ship today.
|
||||
> HTTP wire (`mesh sync`, `mesh serve`) is the next milestone. Protocol
|
||||
> contract below pins what wire will implement.
|
||||
|
||||
## Default off
|
||||
|
||||
No code path touches a network unless a peer flips `meta.mesh.enabled = 1`.
|
||||
Lookups, ingests, distillations, and Q&A run identically with or without
|
||||
mesh. Initialization is two commands and is reversible.
|
||||
|
||||
```
|
||||
aborist mesh init --group myteam # mint Ed25519 + X25519 keys; create epoch 0
|
||||
aborist mesh enable # flip gating flag on
|
||||
aborist mesh status # always-safe inspection
|
||||
aborist mesh disable # flag back off; keys + history stay on disk
|
||||
```
|
||||
|
||||
## Identity stack
|
||||
|
||||

|
||||
|
||||
Each peer carries:
|
||||
|
||||
- one **Ed25519** keypair for signing membership ops and gossip envelopes
|
||||
- one **X25519** keypair for ECDH key agreement (used to wrap epoch secrets)
|
||||
- one **8-hex `member_id`** (random by default; pinnable via `--member-id`)
|
||||
|
||||
Group state, replicated across every peer that participates:
|
||||
|
||||
- `mesh_roster` — per-epoch (member_id, sign_pub, dh_pub, role) tuples
|
||||
- `mesh_epochs` — per-epoch lifecycle (started_at, audit linkage, secret envelope)
|
||||
- `audit_events` — append-only Merkle chain of every state-changing op
|
||||
|
||||
Public keys are shared. Private keys never leave a peer's SQLite file.
|
||||
|
||||
## Epochs — a counter that bumps on every roster change
|
||||
|
||||

|
||||
|
||||
`init_identity()` writes epoch 0 with the founder as sole admin.
|
||||
Every subsequent `add`, `kick`, or `rotate` writes a new epoch row,
|
||||
generates a fresh 32-byte symmetric secret, and wraps that secret to
|
||||
every member of a *new* roster.
|
||||
|
||||
Old epochs are not deleted. Their rosters and audit events stay on
|
||||
disk so that historical signatures remain verifiable by anyone, forever.
|
||||
Cores never evict; mesh state never evicts either.
|
||||
|
||||
## Per-member secret envelope
|
||||
|
||||

|
||||
|
||||
A single epoch carries one symmetric secret. That one secret is wrapped
|
||||
N times, once per member, by ECDH between a rotator's X25519 priv and
|
||||
each member's X25519 pub, then sealed with ChaCha20-Poly1305 using
|
||||
`member_id` as AAD.
|
||||
|
||||
Decryption is local: a peer derives the same shared secret by ECDH
|
||||
between their own X25519 priv and a rotator's X25519 pub, then unwraps
|
||||
their own slot. An evicted peer simply has no slot, so unwrap raises
|
||||
`ValueError` — they are opaque to gossip from epoch+1 onward by design.
|
||||
|
||||
Code: `aborist/mesh/state.py::_wrap_secret_for_members` and
|
||||
`unwrap_secret_for_self`.
|
||||
|
||||
## What flows between peers
|
||||
|
||||

|
||||
|
||||
Aborist is content-addressed, so identifiers are short and bodies are
|
||||
optional. A typical sync round looks like:
|
||||
|
||||
1. **alice** signs and sends `ANNOUNCE_ROOT(document_root, source_uri,
|
||||
chunking_version, schema_version)` for everything new since last
|
||||
sync. Same for derivations, providence_cache rows, and falsifications.
|
||||
2. **bob** verifies alice's signature against her epoch-N `sign_pub`,
|
||||
then checks his local store. Anything he already has is dedup'd by
|
||||
`document_root` and dropped on the floor.
|
||||
3. On miss, bob sends `REQUEST_BODY(root)`. alice replies with
|
||||
`DELIVER_BODY(bytes, merkle_proof)`. bob verifies the proof against
|
||||
the announced root before inserting.
|
||||
4. Audit chains merge by `prev_event_hash`. A gossip insert that does
|
||||
not extend a chain consistently is rejected — mesh never silently
|
||||
forks history.
|
||||
|
||||
The five gossip message types:
|
||||
|
||||
| message | meaning | encrypted? | signed? |
|
||||
|---|---|---|---|
|
||||
| `ANNOUNCE_ROOT` | "I have these document_roots" | optional | always |
|
||||
| `ANNOUNCE_DERIVATION` | "core C derives from surfaces S₁..Sₙ via distiller D" | optional | always |
|
||||
| `ANNOUNCE_PROVIDENCE` | "I cached an answer at this 8-dim cache_key with this audit_mode" | optional | always |
|
||||
| `ANNOUNCE_FALSIFICATION` | "this cache_key is wrong (witness signature attached)" | optional | always |
|
||||
| `REQUEST_BODY` / `DELIVER_BODY` | pull bytes on local miss; recipient verifies Merkle proof before insert | optional | always |
|
||||
|
||||
Soft hashes (embeddings, TF-IDF scores) never enter wire. Only hard
|
||||
SHA-256 commitments cross the network. Per `CLAUDE.md`'s soft/hard hash
|
||||
split.
|
||||
|
||||
## Group operations — operator decision tree
|
||||
|
||||

|
||||
|
||||
A group has two roles: `admin` and `member`. Admins may add and kick.
|
||||
Any current member may rotate the secret on the same roster. Self-kick
|
||||
is rejected; an admin who wants out runs `mesh disable` instead.
|
||||
|
||||
### Add a member (admin only)
|
||||
|
||||
```
|
||||
# bob mints his keys and shares pubs (out-of-band: signal, in person, signed file).
|
||||
aborist mesh init --group myteam --member-id bob # bob's machine
|
||||
|
||||
# alice (admin) enrolls bob at her machine:
|
||||
aborist mesh add --member-id bob \
|
||||
--sign-pub <bob_sign_pub_hex> \
|
||||
--dh-pub <bob_dh_pub_hex>
|
||||
# epoch bumps. fresh secret wrapped to alice + bob.
|
||||
```
|
||||
|
||||
Bob can decrypt epoch+1 forward. Bob *cannot* reach prior epochs — by
|
||||
design, joining doesn't grant retroactive access.
|
||||
|
||||
### Kick a member (admin only)
|
||||
|
||||
```
|
||||
aborist mesh kick --member-id dave --reason "left team 2026-04-28"
|
||||
```
|
||||
|
||||
Epoch bumps. New secret wrapped to everyone *except* dave. Dave's prior
|
||||
signatures stay verifiable forever (his roster row at older epochs is
|
||||
preserved). Any AEAD-protected gossip from epoch+1 onward is opaque to
|
||||
him.
|
||||
|
||||
Cannot leave a roster with no admins. Promote first, then kick.
|
||||
|
||||
### Rotate the secret (any member)
|
||||
|
||||
```
|
||||
aborist mesh rotate --reason "scheduled monthly hygiene"
|
||||
```
|
||||
|
||||
Same roster, fresh secret. Use on suspected secret leak when no
|
||||
identifiable bad actor exists, or as periodic key hygiene.
|
||||
|
||||
### When to use which
|
||||
|
||||
| situation | command |
|
||||
|---|---|
|
||||
| starting a new federation | `mesh init` then `mesh enable` |
|
||||
| onboarding a teammate | `mesh add` (admin) |
|
||||
| someone left the team | `mesh kick` (admin) |
|
||||
| rumor of secret compromise but everyone's keys still trusted | `mesh rotate` |
|
||||
| stepping away yourself | `mesh disable` (own peer only) |
|
||||
|
||||
## Forward-secrecy guarantees in plain language
|
||||
|
||||
- **Joining is non-retroactive.** A new member cannot decrypt anything
|
||||
from before they joined. Wrapping per-epoch makes this automatic.
|
||||
- **Eviction is forward-only.** A kicked member keeps everything they
|
||||
already had. They lose access to gossip from the next epoch on.
|
||||
- **Past signatures remain verifiable forever.** Roster history is
|
||||
immutable on disk, so anyone can audit who said what at what epoch
|
||||
even after rotations.
|
||||
- **Rotation costs O(N).** One ECDH + one AEAD per member per rotate.
|
||||
Linear in roster size, not document count.
|
||||
|
||||
## Trust boundary, threat model
|
||||
|
||||
Mesh defends against:
|
||||
|
||||
- **Roster impersonation.** Every membership op is Ed25519-signed by an
|
||||
admin and chained via `audit_events.event_hash = sha256(prev || body)`.
|
||||
A peer cannot fake a `mesh_epoch_rotate` without the admin's signing key.
|
||||
- **Gossip tampering.** Every announcement is signed; recipients verify
|
||||
before insert. A peer that doesn't carry an epoch's secret cannot
|
||||
produce a valid AEAD payload for that epoch.
|
||||
- **Eviction bypass.** Evicted members have no slot in the post-rotation
|
||||
envelope. They cannot derive the new secret from any prior secret.
|
||||
- **Forking.** Audit chain `prev_event_hash` linkage rejects gossip
|
||||
inserts that do not extend a chain consistently.
|
||||
|
||||
Mesh does not defend against:
|
||||
|
||||
- **Out-of-band key distribution.** A new member's pubs must reach the
|
||||
enrolling admin via a trusted channel (signal, in person, signed file).
|
||||
Mesh has no built-in introduction protocol.
|
||||
- **Compromise of an admin's signing key.** With that key an attacker
|
||||
can forge any membership op. Mitigation: rotate admins regularly,
|
||||
keep admin count small.
|
||||
- **Traffic analysis.** Wire layer ships TLS but does not pad or mix.
|
||||
|
||||
## Related code
|
||||
|
||||
- `aborist/mesh/__init__.py` — module entrypoint, public API
|
||||
- `aborist/mesh/crypto.py` — Ed25519 + X25519 + ChaCha20-Poly1305 wrappers
|
||||
- `aborist/mesh/state.py` — DB writes + audit chain hooks; epoch rotation
|
||||
- `aborist/mesh/members.py` — `add_member`, `kick_member`, `scheduled_rotate`
|
||||
- `aborist/cli.py` — `_cmd_mesh_*` argparse handlers
|
||||
- `aborist/store.py` — `mesh_identity`, `mesh_roster`, `mesh_epochs` schema
|
||||
|
||||
## Related docs
|
||||
|
||||
- `README.md` — top-level overview, ingest flow, single-peer Q&A
|
||||
- `CLAUDE.md` — schema invariants, conventions, audit chain rules
|
||||
- `~/git/proxy.unturf.com/pkg/verified/merkle.go` — fox's Go merkle
|
||||
reference. Aborist's Python port mirrors the conventions.
|
||||
|
||||
## Rendering the diagrams
|
||||
|
||||
```
|
||||
make docs # renders docs/diagrams/*.dot to .png via graphviz
|
||||
```
|
||||
|
||||
Or directly: `dot -Tpng docs/diagrams/<file>.dot -o /tmp/out.png`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue