arborist/tests/test_mesh_wire_e2e.py
russell@unturf.com f141babde1
mesh: HTTP gossip wire — signed envelopes + 5 message types
Implements the protocol contract pinned in docs/mesh.md. Each peer
runs a stdlib ThreadingHTTPServer; clients send Ed25519-signed
envelopes carrying one of:

  ANNOUNCE_ROOT           document_root + source_uri + versions
  ANNOUNCE_DERIVATION     core_root <- surface_roots (proof-blob hash)
  ANNOUNCE_PROVIDENCE     cache_key + audit_mode + answer_hash
  ANNOUNCE_FALSIFICATION  cache_key + reason
  REQUEST_BODY/DELIVER_BODY  pull-on-miss + Merkle-root verify

WireEnvelope canonicalizes via sorted-key separator-tight JSON; the
canonical bytes are what get signed. Receivers verify the signature
against sender_id's sign_pub looked up in mesh_roster at the
envelope's epoch_id — non-members of that epoch produce no valid
signature, so they're silently rejected (401, no audit event).

Each accepted ANNOUNCE_* writes one 'mesh_received' event into the
local audit chain whose body is the full signed envelope (the
remote's signature stays attached for non-repudiation). The
receiver's chain stays internally consistent because we append in
receive-order.

REQUEST_BODY/DELIVER_BODY: the responder ships the document text
plus per-chunk leaf hashes; the client re-derives the Merkle root
from those leaves and refuses to return any body whose leaves
don't reconstruct the requested root.

Tests: 17 unit (envelope canonicalization, sig verify against
roster, type validation, tampered-body rejection, audit-event
write) + 5 end-to-end (two real HTTP peers on ephemeral ports
exchanging announces and bodies). 22/22 green.

Deferred to later commits, per docs/mesh.md:
- CLI verbs (mesh serve, mesh sync) — pending downstream merge
- per-peer chain-of-claims tracking (catchup, replay, fork detection)
- optional AEAD encryption of message bodies (contract says optional)
2026-04-28 16:57:04 -04:00

186 lines
6.4 KiB
Python

"""Mesh wire end-to-end: two real HTTP peers exchange gossip.
Each peer binds an ephemeral port via host=127.0.0.1, port=0 and runs
ThreadingHTTPServer in a daemon thread. The other peer's MeshWireClient
talks to it over real httpx + TCP. No mocking — this exercises the
full request/response path including JSON serialization, HTTP framing,
and threaded handler dispatch.
Tests run under timeout via daemon threads; if a test hangs the worst
case is the test process exits and pytest times out the test, leaving
no zombie threads (daemon=True).
"""
from __future__ import annotations
from pathlib import Path
import pytest
from aborist.document import Document
from aborist.ingest import ingest_source
from aborist.mesh import init_identity
from aborist.mesh.members import add_member
from aborist.mesh.wire import (
MeshWireClient,
MeshWireServer,
TYPE_ANNOUNCE_ROOT,
)
from aborist.store import connect
@pytest.fixture
def two_peers(tmp_path):
"""Spin up alice + bob with mutual rosters and HTTP servers running."""
alice_db = tmp_path / "alice.db"
bob_db = tmp_path / "bob.db"
a_conn = connect(alice_db)
try:
alice = init_identity(a_conn, group_name="t", member_id="alice")
finally:
a_conn.close()
b_conn = connect(bob_db)
try:
bob = init_identity(b_conn, group_name="t", member_id="bob")
finally:
b_conn.close()
a_conn = connect(alice_db)
try:
add_member(a_conn, member_id="bob", sign_pub=bob.sign_pub, dh_pub=bob.dh_pub)
finally:
a_conn.close()
b_conn = connect(bob_db)
try:
add_member(b_conn, member_id="alice", sign_pub=alice.sign_pub, dh_pub=alice.dh_pub)
finally:
b_conn.close()
alice_srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
bob_srv = MeshWireServer(bob_db, host="127.0.0.1", port=0)
alice_srv.start_in_thread()
bob_srv.start_in_thread()
try:
yield {
"alice": {"db": alice_db, "srv": alice_srv, "id": alice},
"bob": {"db": bob_db, "srv": bob_srv, "id": bob},
}
finally:
alice_srv.stop()
bob_srv.stop()
def test_info_endpoint_returns_member_id(two_peers):
"""GET /mesh/info returns the responder's identity (no signature required)."""
with MeshWireClient(two_peers["bob"]["db"], two_peers["alice"]["srv"].url) as c:
info = c.info()
assert info["member_id"] == "alice"
assert info["group_name"] == "t"
assert info["current_epoch"] == 1
def test_announce_root_round_trip(two_peers):
"""Bob announces a doc_root to Alice; Alice's audit chain gains a
`mesh_received` event with the announce body."""
with MeshWireClient(two_peers["bob"]["db"], two_peers["alice"]["srv"].url) as c:
resp = c.announce_root(
document_root="ab" * 32,
source_uri="https://en.wikipedia.org/wiki/Cloud_Strife",
chunking_version="tok-512-v1",
canonicalization_version="norm-v1",
schema_version="v9.8.0",
)
assert resp["ok"] is True
assert resp["wire_type"] == TYPE_ANNOUNCE_ROOT
assert isinstance(resp["audit_event_hash"], str)
assert len(resp["audit_event_hash"]) == 64
a_conn = connect(two_peers["alice"]["db"])
try:
last = a_conn.execute(
"SELECT event_type, body, subject_root FROM audit_events "
"ORDER BY seq DESC LIMIT 1"
).fetchone()
finally:
a_conn.close()
assert last["event_type"] == "mesh_received"
assert last["subject_root"] == "ab" * 32
import json
body = json.loads(last["body"])
assert body["wire_type"] == TYPE_ANNOUNCE_ROOT
assert body["sender_id"] == "bob"
assert body["envelope_body"]["source_uri"] == "https://en.wikipedia.org/wiki/Cloud_Strife"
class _OneDocSource:
"""One-shot source for tests — yields a single Document."""
source_type = "wire_e2e"
def __init__(self, uri: str, content: str, title: str):
self._doc = Document(
uri=uri, content=content, source_type=self.source_type, title=title
)
def iter_documents(self):
yield self._doc
def _ingest_one(db_path, *, uri: str, content: str, title: str) -> str:
"""Ingest one document, return its document_root."""
conn = connect(db_path)
try:
ingest_source(conn, _OneDocSource(uri, content, title))
row = conn.execute(
"SELECT document_root FROM documents WHERE document_uri=?", (uri,)
).fetchone()
finally:
conn.close()
assert row is not None, f"document not found post-ingest for {uri!r}"
return row["document_root"]
def test_request_body_pulls_doc_with_verified_merkle(two_peers):
"""Alice has a doc; Bob requests it. Client verifies the delivered
Merkle root against the requested root before returning."""
document_root = _ingest_one(
two_peers["alice"]["db"],
uri="https://en.wikipedia.org/wiki/test",
content="Hello world. The quick brown fox jumps over the lazy dog.",
title="Test",
)
with MeshWireClient(two_peers["bob"]["db"], two_peers["alice"]["srv"].url) as c:
delivered = c.request_body(root=document_root)
assert delivered["root"] == document_root
assert delivered["document_uri"] == "https://en.wikipedia.org/wiki/test"
assert "Hello world" in delivered["text"]
assert isinstance(delivered["leaves_hex"], list)
assert len(delivered["leaves_hex"]) >= 1
def test_request_body_404_on_unknown_root(two_peers):
"""Asking for a root the responder doesn't have raises HTTPStatusError."""
import httpx
with MeshWireClient(two_peers["bob"]["db"], two_peers["alice"]["srv"].url) as c:
with pytest.raises(httpx.HTTPStatusError):
c.request_body(root="ff" * 32)
def test_request_body_rejects_tampered_response(two_peers, monkeypatch):
"""If a peer returns a DELIVER_BODY whose leaves don't Merkle-derive
to the claimed root, the client raises before returning."""
document_root = _ingest_one(
two_peers["alice"]["db"],
uri="https://en.wikipedia.org/wiki/x",
content="real content of the document",
title="X",
)
with MeshWireClient(two_peers["bob"]["db"], two_peers["alice"]["srv"].url) as c:
from aborist.mesh import wire as _wire
monkeypatch.setattr(_wire, "_merkle_root_matches", lambda *a, **k: False)
with pytest.raises(ValueError, match="Merkle-derive"):
c.request_body(root=document_root)