arborist/tests/test_mesh_chain.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

494 lines
16 KiB
Python

"""Mesh wire — per-peer audit-chain tracking.
Receiver-side enforcement that gossip envelopes from a given peer
extend a single linear chain of claims. Catches:
- Tampered ``event_hash`` (recomputation mismatch -> 400).
- Forks / replays where ``prev_event_hash`` doesn't match the last
envelope we accepted from that peer (-> 409).
- Cross-talk between peers' chains (alice's broadcasts must not
affect bob's chain state and vice versa).
Implements the spec from ``docs/mesh.md``:
> Audit chains merge by prev_event_hash. A gossip insert that does
> not extend a chain consistently is rejected — mesh never silently
> forks history.
"""
from __future__ import annotations
import time
from pathlib import Path
import pytest
from arborist.mesh import init_identity
from arborist.mesh.crypto import sign as _sign
from arborist.mesh.members import add_member
from arborist.mesh.wire import (
MeshWireServer,
TYPE_ANNOUNCE_ROOT,
WireEnvelope,
)
from arborist.store import connect
def _bootstrap_three_peer_db(tmp_path: Path):
"""Alice (admin) + bob + carol on alice's DB, both enrolled at the
same current_epoch (each ``add_member`` bumps the epoch by one).
Returns (alice_db, current_epoch, alice_id, bob_id, carol_id). Alice's
roster at ``current_epoch`` contains bob and carol so signature
verification finds their sign_pubs when receiving from either.
"""
from arborist.mesh.state import current_epoch as _current_epoch
alice_db = tmp_path / "alice.db"
bob_db = tmp_path / "bob.db"
carol_db = tmp_path / "carol.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()
c_conn = connect(carol_db)
try:
carol = init_identity(c_conn, group_name="t", member_id="carol")
finally:
c_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)
add_member(a_conn, member_id="carol", sign_pub=carol.sign_pub, dh_pub=carol.dh_pub)
epoch = _current_epoch(a_conn)
finally:
a_conn.close()
return alice_db, epoch, alice, bob, carol
def _signed_envelope(
*,
sign_priv: bytes,
sender_id: str,
body: dict,
prev_event_hash: str | None,
epoch_id: int,
ts: int | None = None,
) -> tuple[WireEnvelope, dict]:
"""Build a chain-linked, signed envelope. Returns (env, signed_dict)."""
env = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id=sender_id,
epoch_id=epoch_id,
ts=int(time.time()) if ts is None else ts,
body=body,
).with_chain(prev_event_hash=prev_event_hash)
return env, env.sign_with(sign_priv)
def _root_for(seed: str) -> str:
"""Deterministic 64-char hex doc-root marker for tests (not a real hash)."""
return (seed * 64)[:64]
# ---------------------------------------------------------------------------
# Sender-side determinism
# ---------------------------------------------------------------------------
def test_with_chain_produces_deterministic_event_hash():
"""Same envelope + same prev_event_hash -> same event_hash, every time."""
body = {
"document_root": _root_for("a"),
"source_uri": "https://x",
"chunking_version": "tok-512-v1",
"canonicalization_version": "norm-v1",
"schema_version": "v9.8.0",
}
env_base = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="alice",
epoch_id=1,
ts=1700_000_000,
body=body,
)
a = env_base.with_chain(prev_event_hash=None)
b = env_base.with_chain(prev_event_hash=None)
assert a.event_hash == b.event_hash
assert a.event_hash is not None
assert len(a.event_hash) == 64
# Different prev -> different hash.
c = env_base.with_chain(prev_event_hash="aa" * 32)
assert c.event_hash != a.event_hash
def test_event_hash_excluded_from_canonical_bytes():
"""canonical_bytes() must NOT contain event_hash (avoids circularity).
The literal hex of event_hash also must not appear, since the JSON
serialization is the only path it could leak through. (The substring
``"event_hash"`` does appear because ``prev_event_hash`` is included;
that's expected — only the *value* of event_hash is excluded.)
"""
env = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="alice",
epoch_id=1,
ts=0,
body={"document_root": _root_for("a")},
).with_chain(prev_event_hash=None)
cb = env.canonical_bytes()
assert env.event_hash is not None
assert env.event_hash.encode() not in cb
# The "event_hash" key itself is NOT serialized — only "prev_event_hash" is.
assert b'"event_hash"' not in cb
assert b'"prev_event_hash"' in cb
# ---------------------------------------------------------------------------
# Receiver-side: first envelope, chain extension, fork detection
# ---------------------------------------------------------------------------
def test_first_announce_with_prev_none_accepted(tmp_path):
"""A peer's very first envelope, prev_event_hash=None, is accepted."""
alice_db, epoch, _alice, bob, _carol = _bootstrap_three_peer_db(tmp_path)
_env, signed = _signed_envelope(
sign_priv=bob.sign_priv,
sender_id="bob",
body={"document_root": _root_for("b"), "source_uri": "https://x"},
prev_event_hash=None,
epoch_id=epoch,
)
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
status, body = srv.handle_announce(signed)
finally:
srv.stop()
assert status == 200, body
a_conn = connect(alice_db)
try:
row = a_conn.execute(
"SELECT last_event_hash, last_seq FROM mesh_peer_chains "
"WHERE peer_member_id = 'bob'"
).fetchone()
finally:
a_conn.close()
assert row is not None
assert row["last_event_hash"] == _env.event_hash
assert row["last_seq"] == 1
def test_chain_extension_accepted(tmp_path):
"""Second envelope with prev=first.event_hash extends the chain."""
alice_db, epoch, _alice, bob, _carol = _bootstrap_three_peer_db(tmp_path)
env1, signed1 = _signed_envelope(
sign_priv=bob.sign_priv,
sender_id="bob",
body={"document_root": _root_for("1"), "source_uri": "https://x1"},
prev_event_hash=None,
epoch_id=epoch,
)
env2, signed2 = _signed_envelope(
sign_priv=bob.sign_priv,
sender_id="bob",
body={"document_root": _root_for("2"), "source_uri": "https://x2"},
prev_event_hash=env1.event_hash,
epoch_id=epoch,
)
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
s1, _ = srv.handle_announce(signed1)
s2, _ = srv.handle_announce(signed2)
finally:
srv.stop()
assert s1 == 200
assert s2 == 200
a_conn = connect(alice_db)
try:
row = a_conn.execute(
"SELECT last_event_hash, last_seq FROM mesh_peer_chains "
"WHERE peer_member_id = 'bob'"
).fetchone()
finally:
a_conn.close()
assert row["last_event_hash"] == env2.event_hash
assert row["last_seq"] == 2
def test_fork_detection_rejects_409(tmp_path):
"""Second envelope with prev=GARBAGE is rejected — chain forked."""
alice_db, epoch, _alice, bob, _carol = _bootstrap_three_peer_db(tmp_path)
env1, signed1 = _signed_envelope(
sign_priv=bob.sign_priv,
sender_id="bob",
body={"document_root": _root_for("1"), "source_uri": "https://x"},
prev_event_hash=None,
epoch_id=epoch,
)
# Bob (or someone using bob's key) sends a second envelope claiming
# prev = arbitrary garbage instead of env1.event_hash.
_env2, signed2 = _signed_envelope(
sign_priv=bob.sign_priv,
sender_id="bob",
body={"document_root": _root_for("2"), "source_uri": "https://x"},
prev_event_hash="ff" * 32,
epoch_id=epoch,
)
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
s1, _ = srv.handle_announce(signed1)
s2, body = srv.handle_announce(signed2)
finally:
srv.stop()
assert s1 == 200
assert s2 == 409
assert "fork detected" in body["error"]
assert "bob" in body["error"]
# The fork rejection must NOT advance the peer's tracked chain.
a_conn = connect(alice_db)
try:
row = a_conn.execute(
"SELECT last_event_hash, last_seq FROM mesh_peer_chains "
"WHERE peer_member_id = 'bob'"
).fetchone()
finally:
a_conn.close()
assert row["last_event_hash"] == env1.event_hash
assert row["last_seq"] == 1
def test_tampered_event_hash_rejected_400(tmp_path):
"""An envelope whose event_hash doesn't match recomputation is 400."""
alice_db, epoch, _alice, bob, _carol = _bootstrap_three_peer_db(tmp_path)
env, signed = _signed_envelope(
sign_priv=bob.sign_priv,
sender_id="bob",
body={"document_root": _root_for("a"), "source_uri": "https://x"},
prev_event_hash=None,
epoch_id=epoch,
)
# Tamper with the event_hash field without re-signing. The envelope
# signature itself remains valid (event_hash is excluded from the
# signed canonical bytes), so the receiver catches this at the
# event_hash recomputation step, not the signature step.
signed["envelope"]["event_hash"] = "deadbeef" * 8
assert signed["envelope"]["event_hash"] != env.event_hash
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
status, body = srv.handle_announce(signed)
finally:
srv.stop()
assert status == 400
assert "event_hash mismatch" in body["error"]
# No peer-chain row written for a rejected envelope.
a_conn = connect(alice_db)
try:
row = a_conn.execute(
"SELECT * FROM mesh_peer_chains WHERE peer_member_id = 'bob'"
).fetchone()
finally:
a_conn.close()
assert row is None
def test_chain_tracking_advances_across_multiple_announces(tmp_path):
"""Receiver tracks last_event_hash + last_seq correctly over N announces."""
alice_db, epoch, _alice, bob, _carol = _bootstrap_three_peer_db(tmp_path)
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
prev = None
last_eh = None
for i in range(5):
env, signed = _signed_envelope(
sign_priv=bob.sign_priv,
sender_id="bob",
body={"document_root": _root_for(str(i)), "source_uri": f"https://x/{i}"},
prev_event_hash=prev,
epoch_id=epoch,
)
status, _ = srv.handle_announce(signed)
assert status == 200, f"announce {i} failed"
prev = env.event_hash
last_eh = env.event_hash
finally:
srv.stop()
a_conn = connect(alice_db)
try:
row = a_conn.execute(
"SELECT last_event_hash, last_seq FROM mesh_peer_chains "
"WHERE peer_member_id = 'bob'"
).fetchone()
finally:
a_conn.close()
assert row["last_event_hash"] == last_eh
assert row["last_seq"] == 5
def test_two_peers_have_independent_chains(tmp_path):
"""Bob's announces don't affect carol's chain state and vice versa."""
alice_db, epoch, _alice, bob, carol = _bootstrap_three_peer_db(tmp_path)
# Bob: 2 envelopes
bob_env1, bob_signed1 = _signed_envelope(
sign_priv=bob.sign_priv,
sender_id="bob",
body={"document_root": _root_for("b1"), "source_uri": "https://b1"},
prev_event_hash=None,
epoch_id=epoch,
)
bob_env2, bob_signed2 = _signed_envelope(
sign_priv=bob.sign_priv,
sender_id="bob",
body={"document_root": _root_for("b2"), "source_uri": "https://b2"},
prev_event_hash=bob_env1.event_hash,
epoch_id=epoch,
)
# Carol: 1 envelope, prev=None (her chain starts fresh — should be
# accepted even though bob has already posted).
carol_env1, carol_signed1 = _signed_envelope(
sign_priv=carol.sign_priv,
sender_id="carol",
body={"document_root": _root_for("c1"), "source_uri": "https://c1"},
prev_event_hash=None,
epoch_id=epoch,
)
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
assert srv.handle_announce(bob_signed1)[0] == 200
assert srv.handle_announce(carol_signed1)[0] == 200 # carol fresh start
assert srv.handle_announce(bob_signed2)[0] == 200 # bob extends his own chain
finally:
srv.stop()
a_conn = connect(alice_db)
try:
bob_row = a_conn.execute(
"SELECT last_event_hash, last_seq FROM mesh_peer_chains "
"WHERE peer_member_id = 'bob'"
).fetchone()
carol_row = a_conn.execute(
"SELECT last_event_hash, last_seq FROM mesh_peer_chains "
"WHERE peer_member_id = 'carol'"
).fetchone()
finally:
a_conn.close()
assert bob_row["last_event_hash"] == bob_env2.event_hash
assert bob_row["last_seq"] == 2
assert carol_row["last_event_hash"] == carol_env1.event_hash
assert carol_row["last_seq"] == 1
# ---------------------------------------------------------------------------
# Schema migration: peer-chains table appears on legacy DBs
# ---------------------------------------------------------------------------
def test_mesh_peer_chains_table_exists_after_connect(tmp_path):
"""Fresh connect() creates mesh_peer_chains via SCHEMA_SQL + migration."""
db = tmp_path / "fresh.db"
conn = connect(db)
try:
row = conn.execute(
"SELECT name FROM sqlite_master "
"WHERE type='table' AND name='mesh_peer_chains'"
).fetchone()
finally:
conn.close()
assert row is not None
def test_mesh_peer_chains_migration_idempotent(tmp_path):
"""Two connects in a row don't ALTER twice."""
db = tmp_path / "twice.db"
connect(db).close()
conn = connect(db)
try:
cnt = conn.execute(
"SELECT COUNT(*) FROM sqlite_master "
"WHERE type='table' AND name='mesh_peer_chains'"
).fetchone()[0]
finally:
conn.close()
assert cnt == 1
def test_legacy_db_without_peer_chains_migrates(tmp_path):
"""A DB created before the peer-chain rollout gains the table on open."""
import sqlite3 as _s
db = tmp_path / "legacy.db"
raw = _s.connect(db)
raw.execute("CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
raw.commit()
raw.close()
# Pre-state: no mesh_peer_chains.
raw = _s.connect(db)
pre = raw.execute(
"SELECT name FROM sqlite_master WHERE name='mesh_peer_chains'"
).fetchone()
raw.close()
assert pre is None
# Open via connect() — runs SCHEMA_SQL + migration.
conn = connect(db)
try:
post = conn.execute(
"SELECT name FROM sqlite_master WHERE name='mesh_peer_chains'"
).fetchone()
finally:
conn.close()
assert post is not None
# ---------------------------------------------------------------------------
# Backward compat: legacy envelopes (no chain fields) still accepted
# ---------------------------------------------------------------------------
def test_legacy_envelope_without_chain_fields_accepted(tmp_path):
"""A WireEnvelope with event_hash=None (legacy fixture path) is accepted
without per-peer chain enforcement. Sig check still runs."""
alice_db, epoch, _alice, bob, _carol = _bootstrap_three_peer_db(tmp_path)
# Built directly without with_chain() — both prev and event_hash are None.
env = WireEnvelope(
type=TYPE_ANNOUNCE_ROOT,
sender_id="bob",
epoch_id=epoch,
ts=int(time.time()),
body={"document_root": _root_for("a"), "source_uri": "https://x"},
)
assert env.event_hash is None
signed = env.sign_with(bob.sign_priv)
srv = MeshWireServer(alice_db, host="127.0.0.1", port=0)
try:
status, _ = srv.handle_announce(signed)
finally:
srv.stop()
assert status == 200
# Legacy path doesn't write a peer-chain row.
a_conn = connect(alice_db)
try:
row = a_conn.execute(
"SELECT * FROM mesh_peer_chains WHERE peer_member_id='bob'"
).fetchone()
finally:
a_conn.close()
assert row is None