arborist/tests/test_mesh_wire.py
russell@unturf.com 8d6961fcc1
aborist/arborist
modified:   .gitlab-ci.yml
	modified:   bench/qa_questions.txt
	modified:   bench/qa_sweep.py
	modified:   bench/run.sh
	modified:   docs/TICKETS.md
	modified:   docs/_source/README.md
	modified:   docs/_source/_ext/makefile_targets.py
	modified:   docs/_source/api/cli.rst
	modified:   docs/_source/api/distill.rst
	modified:   docs/_source/api/mesh.rst
	modified:   docs/_source/api/qa.rst
	modified:   docs/_source/api/retrieval.rst
	modified:   docs/_source/api/storage.rst
	modified:   docs/_source/api/substrate.rst
	modified:   docs/_source/concepts.rst
	modified:   docs/_source/conf.py
	modified:   docs/_source/cookbook.rst
	modified:   docs/_source/index.rst
	modified:   docs/_source/license.rst
	modified:   docs/_source/quickstart.rst
	modified:   docs/bench-maxing.md
	modified:   docs/benchmarks.md
	modified:   docs/cti-architecture.md
	modified:   docs/diagrams/aborist-modules.dot
	modified:   docs/diagrams/aborist-modules.svg
	modified:   docs/diagrams/mesh-data-flow.dot
	modified:   docs/diagrams/mesh-epoch-lifecycle.dot
	modified:   docs/diagrams/mesh-epoch-lifecycle.svg
	modified:   docs/diagrams/mesh-group-decisions.dot
	modified:   docs/diagrams/mesh-group-decisions.svg
	modified:   docs/diagrams/mesh-identity-stack.dot
	modified:   docs/diagrams/mesh-secret-envelope.dot
	modified:   docs/mesh.md
	modified:   docs/qa-modes-bench.md
	modified:   docs/seven-point-program.md
	modified:   docs/tickets/ticket-000001-retrieval-keywords-audit-gap.md
	modified:   docs/tickets/ticket-000002-reference-frame-polarity-contract.md
	modified:   docs/tickets/ticket-000003-anchor-class-warrant.md
	modified:   docs/tickets/ticket-000005-label-ladder-migration.md
	modified:   docs/tickets/ticket-000006-bench-emergent-findings.md
	modified:   docs/tickets/ticket-000007-query-layer-hyphen-fold.md
	modified:   docs/tickets/ticket-000008-broad-quantifier-preflight-guard.md
	modified:   docs/tickets/ticket-000009-quantifier-preflight-dag-binding.md
	modified:   docs/tickets/ticket-000010-metacognition-preflight-guard.md
	modified:   docs/tickets/ticket-000011-soft-preflight-hint-sidecar.md
	modified:   scripts/backfill_concepts.py
	modified:   scripts/bench_emergent.py
	modified:   tests/crawler/test_async_web_fetcher.py
	modified:   tests/crawler/test_bridge.py
	modified:   tests/crawler/test_web_fetch.py
	modified:   tests/test_bench_qa_sweep.py
	modified:   tests/test_burn.py
	modified:   tests/test_burn_doc.py
	modified:   tests/test_claim_lattice.py
	modified:   tests/test_cli_render.py
	modified:   tests/test_compress.py
	modified:   tests/test_concepts.py
	modified:   tests/test_dag.py
	modified:   tests/test_directives.py
	modified:   tests/test_distill.py
	modified:   tests/test_distill_recursive.py
	modified:   tests/test_evict.py
	modified:   tests/test_frame.py
	modified:   tests/test_grok_source.py
	modified:   tests/test_html_source.py
	modified:   tests/test_ingest.py
	modified:   tests/test_inspect.py
	modified:   tests/test_journal.py
	modified:   tests/test_keys.py
	modified:   tests/test_llm_context_base.py
	modified:   tests/test_merkle.py
	modified:   tests/test_mesh.py
	modified:   tests/test_mesh_aead.py
	modified:   tests/test_mesh_chain.py
	modified:   tests/test_mesh_cli.py
	modified:   tests/test_mesh_cli_pull.py
	modified:   tests/test_mesh_wire.py
	modified:   tests/test_mesh_wire_e2e.py
	modified:   tests/test_metacognition.py
	modified:   tests/test_migration_audit_mode.py
	modified:   tests/test_providence_source.py
	modified:   tests/test_qa.py
	modified:   tests/test_qa_quality_live.py
	modified:   tests/test_quantifier_caps.py
	modified:   tests/test_quantifier_classifier.py
	modified:   tests/test_quantifier_phase4.py
	modified:   tests/test_quantifier_reminder.py
	modified:   tests/test_query.py
	modified:   tests/test_reclassify.py
	modified:   tests/test_repair.py
	modified:   tests/test_resume.py
	modified:   tests/test_snapshot.py
	modified:   tests/test_soft_preflight.py
	modified:   tests/test_tfidf.py
	modified:   tests/test_vcs_source.py
	modified:   tests/test_verify.py
	modified:   tests/test_verify_json.py
	modified:   tests/test_versioned_ingest.py
	modified:   tests/test_warrant.py
	modified:   tests/test_wikipedia_old.py
	modified:   tests/test_wikipedia_xml.py
	modified:   tests/test_wikitext.py
2026-05-07 09:31:49 -04:00

447 lines
14 KiB
Python

"""Mesh wire — unit tests for envelopes + signature verification.
These tests don't bind a network port. End-to-end HTTP exchange is
exercised in tests/test_mesh_wire_e2e.py.
"""
from __future__ import annotations
import time
from pathlib import Path
import pytest
from arborist.mesh import init_identity
from arborist.mesh.members import add_member
from arborist.mesh.crypto import (
generate_dh_keypair,
generate_signing_keypair,
)
from arborist.mesh.wire import (
ALL_TYPES,
PATH_ANNOUNCE,
PATH_INFO,
PATH_REQUEST,
TYPE_ANNOUNCE_FALSIFICATION,
TYPE_ANNOUNCE_ROOT,
TYPE_DELIVER_BODY,
TYPE_REQUEST_BODY,
WIRE_VERSION,
WireEnvelope,
verify_envelope_sig,
)
from arborist.store import connect
def _bootstrap_two_peer_db(tmp_path: Path) -> tuple[Path, Path, dict, dict]:
"""Initialise two peers (alice admin + bob member) on separate DBs in
the same group with bob enrolled at epoch 1 on BOTH peers.
Returns (alice_db, bob_db, alice_identity, bob_identity).
"""
alice_db = tmp_path / "alice.db"
bob_db = tmp_path / "bob.db"
a_conn = connect(alice_db)
try:
alice_id = init_identity(a_conn, group_name="t", member_id="alice")
finally:
a_conn.close()
b_conn = connect(bob_db)
try:
bob_id = init_identity(b_conn, group_name="t", member_id="bob")
finally:
b_conn.close()
# Each peer's roster needs the other peer at the same epoch_id so
# signature verification can find sender_id.sign_pub. Add bob at
# alice's DB; add alice at bob's DB. Same epoch (1) on both sides.
a_conn = connect(alice_db)
try:
add_member(
a_conn,
member_id="bob",
sign_pub=bob_id.sign_pub,
dh_pub=bob_id.dh_pub,
role="member",
)
finally:
a_conn.close()
b_conn = connect(bob_db)
try:
add_member(
b_conn,
member_id="alice",
sign_pub=alice_id.sign_pub,
dh_pub=alice_id.dh_pub,
role="member",
)
finally:
b_conn.close()
return alice_db, bob_db, alice_id, bob_id
# ---------------------------------------------------------------------------
# Envelope canonicalization + sign / verify
# ---------------------------------------------------------------------------
def test_canonical_bytes_stable_across_field_order():
"""Same data, different in-memory dict order → same canonical bytes."""
a = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="alice",
epoch_id=1,
ts=100,
body={"document_root": "ab" * 32, "source_uri": "https://x"},
)
b = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="alice",
epoch_id=1,
ts=100,
body={"source_uri": "https://x", "document_root": "ab" * 32},
)
assert a.canonical_bytes() == b.canonical_bytes()
def test_signed_round_trip():
"""sign_with -> from_signed reconstructs the same envelope and sig."""
sign_priv, _sign_pub = generate_signing_keypair()
env = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="alice",
epoch_id=0,
ts=int(time.time()),
body={"document_root": "00" * 32, "source_uri": "https://x"},
)
signed = env.sign_with(sign_priv)
env2, sig2 = WireEnvelope.from_signed(signed)
assert env == env2
assert env.canonical_bytes() == env2.canonical_bytes()
assert isinstance(sig2, bytes)
assert len(sig2) == 64 # Ed25519 signature length
def test_from_signed_rejects_unknown_type():
sign_priv, _ = generate_signing_keypair()
env = WireEnvelope(
type="ANNOUNCE_ROOT", # valid
sender_id="alice",
epoch_id=0,
ts=0,
body={},
)
signed = env.sign_with(sign_priv)
signed["envelope"]["type"] = "GARBAGE"
with pytest.raises(ValueError, match="unknown envelope type"):
WireEnvelope.from_signed(signed)
def test_from_signed_rejects_wrong_version():
sign_priv, _ = generate_signing_keypair()
env = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="alice",
epoch_id=0,
ts=0,
body={},
)
signed = env.sign_with(sign_priv)
signed["envelope"]["v"] = 99
with pytest.raises(ValueError, match="unsupported wire version"):
WireEnvelope.from_signed(signed)
def test_from_signed_rejects_missing_envelope():
with pytest.raises(ValueError, match="missing envelope"):
WireEnvelope.from_signed({"sig_b64": "AA=="})
def test_from_signed_rejects_bad_base64_sig():
sign_priv, _ = generate_signing_keypair()
env = WireEnvelope(type=TYPE_ANNOUNCE_ROOT, sender_id="x", epoch_id=0, ts=0, body={})
signed = env.sign_with(sign_priv)
signed["sig_b64"] = "not-base64!!"
with pytest.raises(ValueError, match="not valid base64"):
WireEnvelope.from_signed(signed)
def test_all_types_constant_completeness():
"""If someone adds a new TYPE_* constant they must add it to ALL_TYPES."""
expected = {
"ANNOUNCE_ROOT",
"ANNOUNCE_DERIVATION",
"ANNOUNCE_PROVIDENCE",
"ANNOUNCE_FALSIFICATION",
"REQUEST_BODY",
"DELIVER_BODY",
}
assert ALL_TYPES == expected
def test_paths_are_namespaced_under_mesh():
"""Defends against accidental collisions with future top-level paths."""
for p in (PATH_ANNOUNCE, PATH_INFO, PATH_REQUEST):
assert p.startswith("/mesh/")
# ---------------------------------------------------------------------------
# verify_envelope_sig — roster-bound check
# ---------------------------------------------------------------------------
def test_verify_envelope_sig_accepts_known_member(tmp_path):
alice_db, _bob_db, alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path)
env = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="alice",
epoch_id=1,
ts=int(time.time()),
body={"document_root": "11" * 32, "source_uri": "https://x"},
)
sig_payload = env.canonical_bytes()
from arborist.mesh.crypto import sign as _sign
sig = _sign(alice_id.sign_priv, sig_payload)
a_conn = connect(alice_db)
try:
assert verify_envelope_sig(a_conn, env, sig) is True
finally:
a_conn.close()
def test_verify_envelope_sig_rejects_tampered_body(tmp_path):
alice_db, _bob_db, alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path)
env_orig = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="alice",
epoch_id=1,
ts=100,
body={"document_root": "aa" * 32, "source_uri": "https://x"},
)
from arborist.mesh.crypto import sign as _sign
sig = _sign(alice_id.sign_priv, env_orig.canonical_bytes())
env_tampered = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="alice",
epoch_id=1,
ts=100,
body={"document_root": "bb" * 32, "source_uri": "https://x"}, # changed
)
a_conn = connect(alice_db)
try:
assert verify_envelope_sig(a_conn, env_tampered, sig) is False
finally:
a_conn.close()
def test_verify_envelope_sig_rejects_unknown_sender(tmp_path):
alice_db, _bob_db, _alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path)
sign_priv, _sign_pub = generate_signing_keypair()
env = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="mallory", # not in roster
epoch_id=1,
ts=0,
body={"document_root": "00" * 32, "source_uri": "https://x"},
)
from arborist.mesh.crypto import sign as _sign
sig = _sign(sign_priv, env.canonical_bytes())
a_conn = connect(alice_db)
try:
assert verify_envelope_sig(a_conn, env, sig) is False
finally:
a_conn.close()
def test_verify_envelope_sig_rejects_wrong_key_for_known_member(tmp_path):
"""Mallory steals alice's member_id but signs with her own key."""
alice_db, _bob_db, _alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path)
rogue_priv, _rogue_pub = generate_signing_keypair()
env = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="alice", # claims to be alice
epoch_id=1,
ts=0,
body={"document_root": "00" * 32, "source_uri": "https://x"},
)
from arborist.mesh.crypto import sign as _sign
sig = _sign(rogue_priv, env.canonical_bytes())
a_conn = connect(alice_db)
try:
assert verify_envelope_sig(a_conn, env, sig) is False
finally:
a_conn.close()
# ---------------------------------------------------------------------------
# Server.handle_announce — direct (no HTTP)
# ---------------------------------------------------------------------------
def test_handle_announce_writes_audit_event(tmp_path):
"""Bob sends ANNOUNCE_ROOT to Alice's server; Alice's chain gains an event."""
from arborist.mesh.wire import MeshWireServer
alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path)
a_conn = connect(alice_db)
try:
before = a_conn.execute("SELECT COUNT(*) FROM audit_events").fetchone()[0]
finally:
a_conn.close()
env = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="bob",
epoch_id=1,
ts=int(time.time()),
body={
"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",
},
)
signed = env.sign_with(bob_id.sign_priv)
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
status, body = srv.handle_announce(signed)
assert status == 200, body
assert body["wire_type"] == TYPE_ANNOUNCE_ROOT
assert "audit_event_hash" in body
finally:
srv.stop()
a_conn = connect(alice_db)
try:
after = a_conn.execute("SELECT COUNT(*) FROM audit_events").fetchone()[0]
last = a_conn.execute(
"SELECT event_type, body FROM audit_events ORDER BY seq DESC LIMIT 1"
).fetchone()
finally:
a_conn.close()
assert after == before + 1
assert last["event_type"] == "mesh_received"
import json as _json
body_json = _json.loads(last["body"])
assert body_json["wire_type"] == TYPE_ANNOUNCE_ROOT
assert body_json["sender_id"] == "bob"
def test_handle_announce_rejects_bad_sig(tmp_path):
"""Sig from wrong key → 401, no audit event."""
from arborist.mesh.wire import MeshWireServer
alice_db, _bob_db, _alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path)
rogue_priv, _ = generate_signing_keypair()
env = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="bob",
epoch_id=1,
ts=0,
body={"document_root": "00" * 32, "source_uri": "x"},
)
from arborist.mesh.crypto import sign as _sign
sig = _sign(rogue_priv, env.canonical_bytes())
import base64 as _b64
signed = {"envelope": env.__dict__, "sig_b64": _b64.b64encode(sig).decode()}
a_conn = connect(alice_db)
try:
before = a_conn.execute("SELECT COUNT(*) FROM audit_events").fetchone()[0]
finally:
a_conn.close()
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
status, body = srv.handle_announce(signed)
assert status == 401
assert "signature" in body["error"].lower()
finally:
srv.stop()
a_conn = connect(alice_db)
try:
after = a_conn.execute("SELECT COUNT(*) FROM audit_events").fetchone()[0]
finally:
a_conn.close()
assert after == before # rejected: no event written
def test_handle_announce_rejects_request_body_type(tmp_path):
"""The /announce endpoint refuses REQUEST_BODY / DELIVER_BODY."""
from arborist.mesh.wire import MeshWireServer
alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path)
env = WireEnvelope(
type=TYPE_REQUEST_BODY,
sender_id="bob",
epoch_id=1,
ts=0,
body={"root": "00" * 32},
)
signed = env.sign_with(bob_id.sign_priv)
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
status, body = srv.handle_announce(signed)
assert status == 400
assert "rejects" in body["error"]
finally:
srv.stop()
def test_handle_request_404_on_unknown_root(tmp_path):
"""Asking for a doc the responder doesn't have returns 404 cleanly."""
from arborist.mesh.wire import MeshWireServer
alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path)
env = WireEnvelope(
type=TYPE_REQUEST_BODY,
sender_id="bob",
epoch_id=1,
ts=0,
body={"root": "ff" * 32},
)
signed = env.sign_with(bob_id.sign_priv)
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
status, body = srv.handle_announce(signed) # wrong endpoint type → 400
assert status == 400
status, body = srv.handle_request(signed)
assert status == 404
finally:
srv.stop()
def test_announce_falsification_round_trips(tmp_path):
"""Falsifications propagate as ANNOUNCE_FALSIFICATION."""
from arborist.mesh.wire import MeshWireServer
alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path)
env = WireEnvelope(
type=TYPE_ANNOUNCE_FALSIFICATION,
sender_id="bob",
epoch_id=1,
ts=0,
body={"cache_key": "ab" * 32, "reason": "answer was wrong: cited X but X is fictional"},
)
signed = env.sign_with(bob_id.sign_priv)
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
status, body = srv.handle_announce(signed)
assert status == 200
assert body["wire_type"] == TYPE_ANNOUNCE_FALSIFICATION
finally:
srv.stop()