113 lines
4.5 KiB
Python
113 lines
4.5 KiB
Python
|
|
"""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
|