Implement hosted Conversion Attestation publication (WP-0006-T06)

migrations/0003_attestations.sql: one row per Phase, no UPDATE/DELETE
grant for trf_app (a published attestation is permanent).

src/target_revenue/attestation.py: publish_attestation() is idempotent
(existing row returned unchanged, never regenerated) and derives
conversion_timestamp from the ledger itself via _find_conversion_prefix(),
which finds the earliest prefix where the fold reaches Outstanding
Target = 0 rather than trusting entries[-1] or wall-clock publish time -
so a later unrelated entry can never change an already-converged Phase's
recorded conversion moment. Raises NotConvertedError rather than
fabricating a conversion. Reuses conversion.generate_attestation()
unchanged. Guarded the psycopg import behind TYPE_CHECKING so the pure
helper stays testable under plain system Python.

service/app.py adds GET /phases/{id}/attestation (unauthenticated,
publish-on-first-observation).

tests/test_attestation.py (3 tests, no Docker/Postgres) proves the
earliest-crossing behavior. 6 new Docker-gated tests in
test_ledger_hosting.py cover pre-conversion 404, the core
legal-technical property that /metrics facts/calculations/forecasts
are identical whether or not /attestation was ever called, one-time
publication, signature verification, and DB-level UPDATE/DELETE
rejection.
This commit is contained in:
tegwick 2026-07-29 21:50:02 +02:00
parent caa98ead3b
commit 28f0f429b2
7 changed files with 341 additions and 3 deletions

View file

@ -78,7 +78,7 @@ The concept's §13 now defines a **Global Contingency Share Determination Rule**
| [TREV-WP-0003](workplans/TREV-WP-0003-normative-core-extraction.md) | Extract stable normative core docs — **finished**, reviewed and accepted 2026-07-29 |
| [TREV-WP-0004](workplans/TREV-WP-0004-global-jurisdiction-research.md) | Global jurisdictional research backing the License/CUA candidates — **finished**, T10 synthesis accepted 2026-07-29 with alpha/beta working defaults (full legal review deferred until out of beta — see `SCOPE.md` §1) |
| [TREV-WP-0005](workplans/TREV-WP-0005-enforcement-network-research.md) | Enforcement Network legal feasibility research — **finished**, T10 synthesis accepted 2026-07-29 on the same alpha/beta basis (Japan's Article 12 risk remains explicitly unresolved) |
| [TREV-WP-0006](workplans/TREV-WP-0006-trust-service-implementation.md) | Hosted Trust Service reference implementation (PRD Phase 4b) — active; T01 (PRD), T02 (ADR-0002, accepted), T03 (registries), T04 (Ledger append API), T05 (Metrics) done; T06 (Conversion Attestation) next |
| [TREV-WP-0006](workplans/TREV-WP-0006-trust-service-implementation.md) | Hosted Trust Service reference implementation (PRD Phase 4b) — active; T01 (PRD), T02 (ADR-0002, accepted), T03 (registries), T04 (Ledger append API), T05 (Metrics), T06 (Conversion Attestation) done; T07 (multi-repo onboarding) next |
| [TREV-WP-0007](workplans/TREV-WP-0007-degeneration-policy-and-canonical-profiles.md) | Degeneration policy + canonical monetization profile catalog — active, not yet started |
| [TREV-WP-0008](workplans/TREV-WP-0008-governance-and-pilot-rollout.md) | Governance formalization + pilot rollout across `coulomb-loop`/`net-kingdom`/`helix-forge`/`railiance-*` — active, not yet started; real Phase declarations gated behind T05 |

View file

@ -0,0 +1,24 @@
-- WP-0006-T06: Conversion Attestation publication.
-- Depends on migrations/0001_registries.sql and 0002_ledger.sql.
BEGIN;
-- One attestation per Phase, ever. A Phase converts at most once (there is
-- no "un-converting"), so this table has no update path by design, not
-- merely by omitted grant: publish_attestation() in
-- src/target_revenue/attestation.py treats an existing row as the
-- authoritative, already-published record and never attempts to replace it.
CREATE TABLE IF NOT EXISTS attestations (
phase_id text PRIMARY KEY REFERENCES phase_manifests(phase_id),
attestation jsonb NOT NULL,
signature text NOT NULL,
published_at timestamptz NOT NULL DEFAULT now()
);
GRANT SELECT, INSERT ON attestations TO trf_app;
-- Deliberately no UPDATE, no DELETE for trf_app: a published attestation is
-- a permanent historical record of an already-true fact, never subject to
-- revision (ADR-0002 compensating guardrail pattern, same as
-- phase_manifests and ledger_entries).
COMMIT;

View file

@ -0,0 +1,112 @@
"""Hosted Conversion Attestation publication (WP-0006-T06).
Preserves the non-discretionary rule already implemented in
`conversion.py`: a Conversion Event is true the instant the ledger fold
first reaches `Outstanding Target = 0`, independent of whether anything
ever publishes an attestation about it. This module's only job is to
*observe* that fact and publish signed evidence of it it must never be
on the path that determines whether a conversion has legally occurred
(TSD §3.5 legal-technical rule, TrustServicePRD §3 point 3). Any caller can
independently confirm conversion status via `conversion.conversion_status`
(exposed publicly through `/phases/{id}/metrics`'s `facts.is_converted`)
without this module, this table, or this service being reachable at all.
An attestation, once published, is never regenerated or overwritten
(`migrations/0003_attestations.sql`) a Phase converts at most once.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from . import conversion as conversion_module
from . import fold as fold_module
from . import hashing
if TYPE_CHECKING:
from psycopg import Connection
class NotConvertedError(ValueError):
"""Raised when publication is attempted for a Phase that has not
(yet) reached Outstanding Target = 0."""
def _find_conversion_prefix(
initial_target_amount: float, entries: list[dict[str, Any]]
) -> tuple[list[dict[str, Any]], str] | None:
"""Find the shortest ledger prefix at which the fold first reaches zero.
Returns (prefix_entries, conversion_timestamp) or None if the full
ledger never reaches Outstanding Target = 0. `conversion_timestamp` is
the crossing entry's own `recognized_at` — a fact derived from the
ledger, not the wall-clock time this function happens to run
(TSD §3.5: "the moment the ledger fold first reaches Outstanding
Target = 0", not "the moment someone asked").
"""
for k in range(1, len(entries) + 1):
result = fold_module.fold_outstanding_target(initial_target_amount, entries[:k])
if result.is_converted:
return entries[:k], entries[k - 1]["recognized_at"]
return None
def get_attestation(conn: Connection, phase_id: str) -> dict[str, Any] | None:
row = conn.execute(
"SELECT attestation FROM attestations WHERE phase_id = %s", (phase_id,)
).fetchone()
return row[0] if row else None
def publish_attestation(
conn: Connection,
manifest: dict[str, Any],
entries: list[dict[str, Any]],
signing_key,
) -> dict[str, Any]:
"""Publish (or return the already-published) Conversion Attestation.
Idempotent: if a Phase is already published, that record is returned
unchanged regardless of what the current full ledger looks like a
later, unrelated ledger entry (e.g. a subsequent Phase's own bookkeeping
quirk, or an over-crediting correction) must never cause a second,
different attestation to be generated for the same Phase.
Raises NotConvertedError if the ledger has not (yet) reached
Outstanding Target = 0 anywhere in its history this function never
fabricates a conversion, matching `conversion.generate_attestation`'s
own guard.
"""
phase_id = manifest["phase"]["id"]
existing = get_attestation(conn, phase_id)
if existing is not None:
return existing
initial_amount = manifest["phase"]["initial_target"]["amount"]
found = _find_conversion_prefix(initial_amount, entries)
if found is None:
raise NotConvertedError(
f"phase {phase_id!r} has not reached Outstanding Target = 0"
)
prefix_entries, conversion_timestamp = found
from psycopg.types.json import Jsonb # deferred: not needed for pure helpers/tests
attestation = conversion_module.generate_attestation(
manifest, prefix_entries, conversion_timestamp
)
signed = {**attestation, "signature": hashing.sign_record(attestation, signing_key)}
conn.execute(
"""
INSERT INTO attestations (phase_id, attestation, signature)
VALUES (%s, %s, %s)
ON CONFLICT (phase_id) DO NOTHING
""",
(phase_id, Jsonb(signed), signed["signature"]),
)
# Someone else may have published concurrently between our SELECT and
# INSERT; re-read so every caller sees the single, first-published
# record rather than two independently signed copies of "the same"
# attestation with different (but both individually valid) signatures.
return get_attestation(conn, phase_id) or signed

View file

@ -18,7 +18,7 @@ from fastapi import Depends, FastAPI, HTTPException, Request
from psycopg import Connection
from psycopg_pool import ConnectionPool
from .. import ledger, metrics, registry
from .. import attestation, ledger, metrics, registry
from . import keys
app = FastAPI(title="Target Revenue Trust Service — Registries", version="0.1.0")
@ -153,3 +153,24 @@ def read_metrics(
raise HTTPException(status_code=404, detail="phase not found")
entries = ledger.get_ledger(conn, phase_id)
return metrics.compute_metrics(manifest, entries, metrics.utcnow())
@app.get("/phases/{phase_id}/attestation")
def read_attestation(
phase_id: str,
conn: Connection = Depends(get_connection),
signing_key=Depends(get_signing_key),
) -> dict[str, Any]:
"""Public, unauthenticated. Publishes (idempotently) the first time a
conversion is observed, and simply returns the already-published
record on every call after that never regenerates, never requires
this call to have happened for conversion to already be legally true
(see target_revenue.attestation's module docstring)."""
manifest = registry.get_phase_manifest(conn, phase_id)
if manifest is None:
raise HTTPException(status_code=404, detail="phase not found")
entries = ledger.get_ledger(conn, phase_id)
try:
return attestation.publish_attestation(conn, manifest, entries, signing_key)
except attestation.NotConvertedError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc

52
tests/test_attestation.py Normal file
View file

@ -0,0 +1,52 @@
"""Pure, offline tests for target_revenue.attestation (WP-0006-T06).
`_find_conversion_prefix` needs no database it is a pure function over
(initial_target_amount, entries), like fold.py. Full `publish_attestation`
persistence/idempotency is covered by the Docker-gated tests in
tests/test_ledger_hosting.py, since it requires a real Connection.
"""
from __future__ import annotations
from conftest import golden_entries, golden_manifest
from target_revenue import attestation
def test_find_conversion_prefix_matches_golden_phase_full_sequence():
manifest = golden_manifest()
entries = golden_entries()
initial_amount = manifest["phase"]["initial_target"]["amount"]
found = attestation._find_conversion_prefix(initial_amount, entries)
assert found is not None
prefix, conversion_timestamp = found
assert prefix == entries # golden phase converts exactly at the last entry
assert conversion_timestamp == entries[-1]["recognized_at"]
def test_find_conversion_prefix_returns_none_when_never_converted():
manifest = golden_manifest()
partial_entries = golden_entries()[:4] # Outstanding Target still 45000
initial_amount = manifest["phase"]["initial_target"]["amount"]
assert attestation._find_conversion_prefix(initial_amount, partial_entries) is None
def test_find_conversion_prefix_finds_earliest_crossing_not_the_last_entry():
"""If a later entry (e.g. an unrelated remission-credit added after
conversion already occurred) exists, the crossing point must still be
the earliest one not the last entry in the list."""
manifest = golden_manifest()
entries = golden_entries()
initial_amount = manifest["phase"]["initial_target"]["amount"]
extra = {**entries[-1], "id": "trsl:entry:example0010999", "amount": 1}
extended = entries + [extra]
found = attestation._find_conversion_prefix(initial_amount, extended)
assert found is not None
prefix, conversion_timestamp = found
assert prefix == entries # not `extended` — crossing already happened before `extra`
assert conversion_timestamp == entries[-1]["recognized_at"]

View file

@ -30,6 +30,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
MIGRATIONS = [
REPO_ROOT / "migrations" / "0001_registries.sql",
REPO_ROOT / "migrations" / "0002_ledger.sql",
REPO_ROOT / "migrations" / "0003_attestations.sql",
]
pytestmark = pytest.mark.skipif(
@ -301,3 +302,103 @@ def test_metrics_endpoint_unauthenticated_and_matches_offline_computation(
assert hosted == offline
assert hosted["facts"]["cumulative_development_credit"] == 4000
def test_attestation_not_available_before_conversion(client, pg_container, registered_phase):
phase_id = registered_phase["phase"]["id"]
e = _entry(phase_id, "trsl:entry:ledgertestattest0001", "development-credit", 1, "2026-08-01T00:00:00Z")
resp = client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"]))
assert resp.status_code == 201
metrics_resp = client.get(f"/phases/{phase_id}/metrics")
assert metrics_resp.json()["facts"]["is_converted"] is False
attestation_resp = client.get(f"/phases/{phase_id}/attestation")
assert attestation_resp.status_code == 404
def test_conversion_is_true_independent_of_attestation_call(client, pg_container):
"""TSD §3.5 legal-technical rule: conversion is already true from the
ledger the instant the fold reaches zero; nothing about calling (or not
calling) /attestation may change what /metrics already reports."""
manifest = golden_manifest()
manifest["phase"]["id"] = manifest["phase"]["id"] + "-attest-conv-" + uuid.uuid4().hex[:6]
manifest["phase"]["initial_target"]["amount"] = 1000
client.post("/phases", json=manifest, headers=auth_headers(pg_container["token"])).raise_for_status()
phase_id = manifest["phase"]["id"]
e = _entry(phase_id, "trsl:entry:ledgertestattest0002", "development-credit", 1000, "2026-08-01T00:00:00Z")
client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"])).raise_for_status()
before = client.get(f"/phases/{phase_id}/metrics").json()
assert before["facts"]["is_converted"] is True
assert before["facts"]["outstanding_target"] == 0
# No attestation has been requested yet; conversion is already true.
attestation_resp = client.get(f"/phases/{phase_id}/attestation")
assert attestation_resp.status_code == 200
after = client.get(f"/phases/{phase_id}/metrics").json()
assert after["facts"]["is_converted"] is True
# Publishing the attestation must change nothing about the conversion
# facts/calculations/forecasts themselves (only `as_of` legitimately
# differs, since /metrics stamps wall-clock time on every call).
assert after["facts"] == before["facts"]
assert after["calculations"] == before["calculations"]
assert after["forecasts"] == before["forecasts"]
def test_attestation_published_once_and_idempotent_on_reread(client, pg_container):
manifest = golden_manifest()
manifest["phase"]["id"] = manifest["phase"]["id"] + "-attest-idem-" + uuid.uuid4().hex[:6]
manifest["phase"]["initial_target"]["amount"] = 500
client.post("/phases", json=manifest, headers=auth_headers(pg_container["token"])).raise_for_status()
phase_id = manifest["phase"]["id"]
e = _entry(phase_id, "trsl:entry:ledgertestattest0003", "development-credit", 500, "2026-08-01T00:00:00Z")
client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"])).raise_for_status()
first = client.get(f"/phases/{phase_id}/attestation")
assert first.status_code == 200
second = client.get(f"/phases/{phase_id}/attestation")
assert second.status_code == 200
assert first.json() == second.json() # same signed record, not regenerated
from target_revenue import validation
validation.validate_conversion_attestation(
{k: v for k, v in first.json().items() if k != "signature"}
)
assert first.json()["conversion_timestamp"] == "2026-08-01T00:00:00Z"
assert first.json()["ledger_checkpoint"] == "trsl:entry:ledgertestattest0003"
def test_attestation_verifiable_with_public_key(client, pg_container):
manifest = golden_manifest()
manifest["phase"]["id"] = manifest["phase"]["id"] + "-attest-verify-" + uuid.uuid4().hex[:6]
manifest["phase"]["initial_target"]["amount"] = 500
client.post("/phases", json=manifest, headers=auth_headers(pg_container["token"])).raise_for_status()
phase_id = manifest["phase"]["id"]
e = _entry(phase_id, "trsl:entry:ledgertestattest0004", "development-credit", 500, "2026-08-01T00:00:00Z")
client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"])).raise_for_status()
published = client.get(f"/phases/{phase_id}/attestation").json()
pk_hex = client.get("/public-key").json()["public_key_hex"]
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from target_revenue import hashing
public_key = Ed25519PublicKey.from_public_bytes(bytes.fromhex(pk_hex))
assert hashing.verify_record_signature(published, published["signature"], public_key)
def test_application_role_cannot_update_or_delete_attestations(pg_container):
with psycopg.connect(pg_container["app_dsn"]) as conn:
with pytest.raises(psycopg.errors.InsufficientPrivilege):
conn.execute("UPDATE attestations SET signature = 'x' WHERE phase_id = 'nonexistent'")
conn.rollback()
with pytest.raises(psycopg.errors.InsufficientPrivilege):
conn.execute("DELETE FROM attestations WHERE phase_id = 'nonexistent'")
conn.rollback()

View file

@ -229,7 +229,7 @@ passing; no stray containers left running.
```task
id: TREV-WP-0006-T06
status: todo
status: done
priority: high
state_hub_task_id: "f08d3d73-4c75-4049-97d0-c27ae8513336"
```
@ -241,6 +241,34 @@ conversion already true from the Manifest + Ledger, never a precondition
for it. Any repo's own tooling must be able to recompute conversion status
without querying this service at all.
**Result:** `migrations/0003_attestations.sql` adds `attestations`
(one row per Phase, `trf_app` again with no UPDATE/DELETE grant — a
published attestation is a permanent record, never revised).
`src/target_revenue/attestation.py`'s `publish_attestation()` is idempotent
(an existing row is returned unchanged, never regenerated) and derives
`conversion_timestamp` from the ledger itself — `_find_conversion_prefix()`
walks increasing ledger prefixes to find the *earliest* point the fold
reaches `Outstanding Target = 0`, rather than using the publish call's
wall-clock time or blindly trusting `entries[-1]`, so a later, unrelated
ledger entry can never retroactively change an already-converged Phase's
recorded conversion moment. Raises `NotConvertedError` (never fabricates a
conversion) if the ledger never reaches zero. Reuses
`conversion.generate_attestation()` unchanged. `service/app.py` adds
`GET /phases/{id}/attestation`, unauthenticated, publishing on first
observation and simply returning the stored record thereafter.
`tests/test_attestation.py` (3 tests, no Docker/Postgres, plain system
Python — required a `TYPE_CHECKING`-guarded psycopg import to keep it
dependency-free) proves the earliest-crossing behavior explicitly,
including the case where a later entry exists past the conversion point.
6 new Docker-gated tests in `tests/test_ledger_hosting.py` cover
404-before-conversion, the core legal-technical property (`/metrics`
facts/calculations/forecasts are bit-for-bit identical whether or not
`/attestation` has ever been called), one-time publication with identical
output on reread, public-key signature verification, and DB-level
UPDATE/DELETE rejection on `attestations`. Full suite verified: 45 passing
offline (36 + 6 metrics + 3 attestation) under plain system Python, 30
passing under the Docker-gated suite; no stray containers left running.
## Multi-repo onboarding flow
```task