Implement hosted Target Ledger append API (WP-0006-T04)

migrations/0002_ledger.sql adds ledger_entries with an identity-column
sequence for exact append order and no UPDATE/DELETE grant for trf_app.
src/target_revenue/ledger.py: append_entry() rejects caller-supplied
previous_entry_hash/signature, enforces per-Licensor phase ownership,
serializes concurrent appends via pg_advisory_xact_lock, computes the
chain tip and signs with the Trust Service instance's own Ed25519 key
(service/keys.py), reusing validation.py's checks unchanged. Adds
POST/GET /phases/{id}/ledger and an unauthenticated GET /public-key.

Also fixes a route-ordering bug found while wiring this in: phase IDs
never needed the {phase_id:path} converter (they contain colons, not
slashes), and its greedy matching was swallowing /ledger-suffixed
paths into the plain GET /phases/{id} route.

tests/test_ledger_hosting.py (8 Docker-gated tests) exercises hash-chain
linkage, forged-field rejection, cross-Licensor isolation, currency and
duplicate-id rejection, DB-privilege enforcement, signature
verification via the public-key endpoint, and the task's own
highest-priority property: append -> export -> offline fold reproduces
the exact expected Development/Remission Credit and Outstanding Target.
This commit is contained in:
tegwick 2026-07-29 21:34:27 +02:00
parent a419178c08
commit 5064815e77
7 changed files with 567 additions and 12 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 (Phase/Extension Registry hosting) done; T04 (Ledger append API) 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) done; T05 (Metrics) 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,33 @@
-- WP-0006-T04: hosted Target Ledger append API.
-- Depends on migrations/0001_registries.sql (phase_manifests, licensors).
BEGIN;
CREATE TABLE IF NOT EXISTS ledger_entries (
sequence bigint GENERATED ALWAYS AS IDENTITY,
entry_id text NOT NULL,
phase_id text NOT NULL REFERENCES phase_manifests(phase_id),
entry jsonb NOT NULL,
previous_entry_hash text NOT NULL,
signature text NOT NULL,
recognized_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (phase_id, sequence),
UNIQUE (entry_id)
);
-- Ordering within a Phase must be exactly append order for the fold and
-- hash chain to mean anything; index supports the ORDER BY sequence read
-- path (registry/ledger.py get_ledger()).
CREATE INDEX IF NOT EXISTS ledger_entries_phase_sequence_idx
ON ledger_entries (phase_id, sequence);
GRANT SELECT, INSERT ON ledger_entries TO trf_app;
-- Deliberately no UPDATE, no DELETE, and no direct control over `sequence`
-- (GENERATED ALWAYS AS IDENTITY — trf_app cannot even attempt to set it) for
-- trf_app: append-only is a database fact here, matching phase_manifests
-- and extensions in migrations/0001_registries.sql, not merely an API
-- design intention (ADR-0002 compensating guardrail 2).
GRANT USAGE, SELECT ON ledger_entries_sequence_seq TO trf_app;
COMMIT;

View file

@ -0,0 +1,132 @@
"""Hosted Target Ledger append API (WP-0006-T04).
The highest-risk property this module owns: any party who independently
computes `fold.fold_outstanding_target` over an exported Manifest + Ledger
must get exactly the same Outstanding Target the hosted service itself
would report. This module therefore does nothing clever it authenticates
the write, validates the entry with the same offline `validation.py`
checks Stage 0 already tested, computes `previous_entry_hash` and
`signature` itself (never trusting client-supplied values for either), and
appends. The fold is never computed or stored here; `fold.py` remains the
single source of truth, run fresh from whatever a caller exports.
"""
from __future__ import annotations
from typing import Any
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from psycopg import Connection
from psycopg.errors import UniqueViolation
from psycopg.types.json import Jsonb
from . import hashing, validation
from .registry import Licensor, RegistrationError, get_phase_manifest
def append_entry(
conn: Connection,
licensor: Licensor,
phase_id: str,
entry_input: dict[str, Any],
signing_key: Ed25519PrivateKey,
) -> dict[str, Any]:
"""Validate, chain, sign, and persist one Target Ledger entry.
`entry_input` must not include `previous_entry_hash` or `signature`
those are exclusively server-computed. Supplying them is rejected
outright rather than silently overwritten, so a caller never mistakes
a value it sent for one the service actually used.
"""
if "previous_entry_hash" in entry_input or "signature" in entry_input:
raise RegistrationError(
"previous_entry_hash and signature are computed by the Trust "
"Service instance and must not be supplied by the caller"
)
manifest = get_phase_manifest(conn, phase_id)
if manifest is None:
raise RegistrationError(f"phase {phase_id!r} is not registered")
manifest_licensor = conn.execute(
"SELECT licensor_id FROM phase_manifests WHERE phase_id = %s", (phase_id,)
).fetchone()[0]
if manifest_licensor != licensor.licensor_id:
raise RegistrationError(
"this token is not authorized to append to this Phase's ledger"
)
if entry_input.get("phase") != phase_id:
raise RegistrationError(
f"entry.phase {entry_input.get('phase')!r} does not match "
f"the target Phase {phase_id!r}"
)
# Serialize concurrent appends to this Phase so `previous_entry_hash`
# always reflects a real, uncontested chain tip — a transaction-scoped
# advisory lock, released automatically at commit/rollback.
conn.execute("SELECT pg_advisory_xact_lock(hashtext(%s))", (phase_id,))
tip = conn.execute(
"""
SELECT entry FROM ledger_entries
WHERE phase_id = %s
ORDER BY sequence DESC
LIMIT 1
""",
(phase_id,),
).fetchone()
previous_entry_hash = hashing.entry_hash(tip[0]) if tip else hashing.GENESIS
candidate = {**entry_input, "previous_entry_hash": previous_entry_hash}
try:
validation.validate_ledger_entry(candidate)
except validation.ConformanceError as exc:
raise RegistrationError(
"ledger entry rejected: " + "; ".join(exc.errors)
) from exc
currency_errors = validation.check_currency_consistency(manifest, [candidate])
if currency_errors:
raise RegistrationError(
"ledger entry rejected: " + "; ".join(currency_errors)
)
signed = {**candidate, "signature": hashing.sign_record(candidate, signing_key)}
try:
conn.execute(
"""
INSERT INTO ledger_entries
(entry_id, phase_id, entry, previous_entry_hash, signature, recognized_at)
VALUES (%s, %s, %s, %s, %s, %s)
""",
(
signed["id"],
phase_id,
Jsonb(signed),
previous_entry_hash,
signed["signature"],
signed["recognized_at"],
),
)
except UniqueViolation as exc:
raise RegistrationError(
f"entry id {signed['id']!r} already exists"
) from exc
return signed
def get_ledger(conn: Connection, phase_id: str) -> list[dict[str, Any]]:
"""Return a Phase's entries in exact append order (fold input order)."""
rows = conn.execute(
"""
SELECT entry FROM ledger_entries
WHERE phase_id = %s
ORDER BY sequence ASC
""",
(phase_id,),
).fetchall()
return [row[0] for row in rows]

View file

@ -1,12 +1,12 @@
"""FastAPI surface for the hosted Phase Registry and Extension Registry
(WP-0006-T03). Only registration and read endpoints live here the Target
Ledger append API (T04), Metrics (T05), and Attestation (T06) are separate
components per `specs/TechnicalSpecificationDocument.md` §4.1 and are not
implemented in this module.
"""FastAPI surface for the hosted Phase Registry, Extension Registry
(WP-0006-T03), and Target Ledger append API (WP-0006-T04). Metrics (T05)
and Conversion Attestation (T06) are separate components per
`specs/TechnicalSpecificationDocument.md` §4.1 and are not implemented in
this module.
Every route delegates to `target_revenue.registry`; this file's only job is
HTTP framing (status codes, request/response shape) and reading the bearer
token, not conformance logic.
Every route delegates to `target_revenue.registry` / `target_revenue.ledger`;
this file's only job is HTTP framing (status codes, request/response shape,
bearer-token extraction), not conformance or chaining logic.
"""
from __future__ import annotations
@ -18,7 +18,8 @@ from fastapi import Depends, FastAPI, HTTPException, Request
from psycopg import Connection
from psycopg_pool import ConnectionPool
from .. import registry
from .. import ledger, registry
from . import keys
app = FastAPI(title="Target Revenue Trust Service — Registries", version="0.1.0")
@ -34,6 +35,12 @@ def get_pool() -> ConnectionPool:
return app.state.pool
def get_signing_key():
if not hasattr(app.state, "signing_key"):
app.state.signing_key = keys.load_signing_key()
return app.state.signing_key
def get_connection():
pool = get_pool()
with pool.connection() as conn:
@ -64,7 +71,7 @@ def register_phase(
return {"phase_id": manifest["phase"]["id"], "status": "registered"}
@app.get("/phases/{phase_id:path}")
@app.get("/phases/{phase_id}")
def read_phase(phase_id: str, conn: Connection = Depends(get_connection)) -> dict[str, Any]:
manifest = registry.get_phase_manifest(conn, phase_id)
if manifest is None:
@ -97,3 +104,39 @@ def read_extension(
if result is None:
raise HTTPException(status_code=404, detail="extension not found")
return result
@app.get("/public-key")
def read_public_key() -> dict[str, str]:
"""The Ed25519 public key ledger entry signatures verify against.
Deliberately unauthenticated: an external verifier must be able to
check a signature without trusting anything about this API's own
access control (TrustServicePRD §3 point 2, offline verifiability).
"""
return {"algorithm": "ed25519", "public_key_hex": keys.public_key_hex(get_signing_key())}
@app.post("/phases/{phase_id}/ledger", status_code=201)
def append_ledger_entry(
phase_id: str,
entry: dict[str, Any],
licensor: registry.Licensor = Depends(get_licensor),
conn: Connection = Depends(get_connection),
signing_key=Depends(get_signing_key),
) -> dict[str, Any]:
try:
return ledger.append_entry(conn, licensor, phase_id, entry, signing_key)
except registry.RegistrationError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
@app.get("/phases/{phase_id}/ledger")
def read_ledger(
phase_id: str, conn: Connection = Depends(get_connection)
) -> list[dict[str, Any]]:
"""Public: ledger facts are public per FR-10 (aggregate figures,
confidential evidence *references* not the evidence itself)."""
if registry.get_phase_manifest(conn, phase_id) is None:
raise HTTPException(status_code=404, detail="phase not found")
return ledger.get_ledger(conn, phase_id)

View file

@ -0,0 +1,42 @@
"""Trust Service instance signing key.
Per ADR-0002: the Ed25519 signature over each Ledger entry is a guarantee
independent of the per-Licensor API token it is what an external party
verifies without trusting the token/access-control layer at all (TSD §3.2,
TrustServicePRD §3 point 2). This module loads that instance key from
`TRF_SIGNING_KEY_HEX` (a 32-byte hex-encoded Ed25519 seed) or, if unset,
generates an ephemeral one for local development/testing only an
ephemeral key means every restart invalidates prior signatures' verifiable
association with "this instance," which is fine for a dev loop and not
acceptable for any real deployment.
"""
from __future__ import annotations
import os
import warnings
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
_ENV_VAR = "TRF_SIGNING_KEY_HEX"
def load_signing_key() -> Ed25519PrivateKey:
hex_seed = os.environ.get(_ENV_VAR)
if hex_seed:
return Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_seed))
warnings.warn(
f"{_ENV_VAR} not set — generating an ephemeral signing key. "
"Do not use this in any deployment where signatures must remain "
"verifiable across restarts.",
stacklevel=2,
)
return Ed25519PrivateKey.generate()
def public_key_hex(private_key: Ed25519PrivateKey) -> str:
public_key: Ed25519PublicKey = private_key.public_key()
return public_key.public_bytes_raw().hex()

View file

@ -0,0 +1,276 @@
"""Integration tests for WP-0006-T04 (hosted Target Ledger append API).
Same ephemeral, disposable Postgres-via-Docker pattern as
test_registry_hosting.py (never the shared state-hub instance). The single
property this file tests most aggressively, per the task's own framing:
appending entries through the hosted API and then folding the exported
result offline (`fold.fold_outstanding_target`) must produce exactly the
same Outstanding Target a hosted metrics/attestation component would later
report a hosted service that silently diverges from the offline fold is
the highest-risk defect this component could introduce.
"""
from __future__ import annotations
import shutil
import subprocess
import time
import uuid
from pathlib import Path
import pytest
psycopg = pytest.importorskip("psycopg")
pytest.importorskip("fastapi")
from fastapi.testclient import TestClient # noqa: E402
from conftest import golden_manifest # noqa: E402
REPO_ROOT = Path(__file__).resolve().parents[1]
MIGRATIONS = [
REPO_ROOT / "migrations" / "0001_registries.sql",
REPO_ROOT / "migrations" / "0002_ledger.sql",
]
pytestmark = pytest.mark.skipif(
shutil.which("docker") is None, reason="docker not available"
)
@pytest.fixture(scope="module")
def pg_container():
name = f"trf-test-pg-ledger-{uuid.uuid4().hex[:8]}"
subprocess.run(
[
"docker", "run", "--rm", "-d",
"--name", name,
"-e", "POSTGRES_PASSWORD=postgres",
"-e", "POSTGRES_DB=target_revenue_test",
"-p", "127.0.0.1::5432",
"postgres:16-alpine",
],
check=True, capture_output=True,
)
try:
port_out = subprocess.run(
["docker", "port", name, "5432/tcp"], check=True, capture_output=True, text=True
).stdout.strip()
host_port = port_out.split(":")[-1]
dsn = f"host=127.0.0.1 port={host_port} dbname=target_revenue_test user=postgres password=postgres"
for _ in range(60):
try:
with psycopg.connect(dsn, connect_timeout=1):
break
except psycopg.OperationalError:
time.sleep(0.5)
else:
raise RuntimeError("postgres container did not become ready in time")
with psycopg.connect(dsn) as conn:
for migration in MIGRATIONS:
conn.execute(migration.read_text(encoding="utf-8"))
conn.commit()
token = "test-token-acme"
conn.execute(
"INSERT INTO licensors (token, licensor_id) VALUES (%s, %s)",
(token, "acme-corp"),
)
other_token = "test-token-other"
conn.execute(
"INSERT INTO licensors (token, licensor_id) VALUES (%s, %s)",
(other_token, "other-corp"),
)
conn.commit()
app_dsn = (
f"host=127.0.0.1 port={host_port} dbname=target_revenue_test "
f"user=trf_app password=changeme-in-deployment"
)
yield {
"admin_dsn": dsn,
"app_dsn": app_dsn,
"token": token,
"other_token": other_token,
}
finally:
subprocess.run(["docker", "stop", name], capture_output=True)
@pytest.fixture()
def client(pg_container, monkeypatch):
monkeypatch.setenv("TRF_DATABASE_URL", pg_container["app_dsn"])
monkeypatch.setenv("TRF_SIGNING_KEY_HEX", "11" * 32)
from target_revenue.service import app as app_module
if hasattr(app_module.app.state, "pool"):
app_module.app.state.pool.close()
del app_module.app.state.pool
if hasattr(app_module.app.state, "signing_key"):
del app_module.app.state.signing_key
with TestClient(app_module.app) as c:
yield c
def auth_headers(token: str):
return {"Authorization": f"Bearer {token}"}
@pytest.fixture()
def registered_phase(client, pg_container):
manifest = golden_manifest()
manifest["phase"]["id"] = manifest["phase"]["id"] + "-ledger-test-" + uuid.uuid4().hex[:6]
resp = client.post("/phases", json=manifest, headers=auth_headers(pg_container["token"]))
assert resp.status_code == 201, resp.text
return manifest
def _entry(phase_id: str, entry_id: str, entry_type: str, amount: float, recognized_at: str):
return {
"id": entry_id,
"phase": phase_id,
"type": entry_type,
"amount": amount,
"currency": "USD",
"recognized_at": recognized_at,
"evidence_reference": f"confidential:evidence:{entry_id}",
"extension": {"id": "trsl:extension:development-license", "version": "1.0"},
}
def test_append_and_hash_chain_links(client, pg_container, registered_phase):
phase_id = registered_phase["phase"]["id"]
e1 = _entry(phase_id, "trsl:entry:ledgertest0001", "development-credit", 1000, "2026-08-01T00:00:00Z")
r1 = client.post(f"/phases/{phase_id}/ledger", json=e1, headers=auth_headers(pg_container["token"]))
assert r1.status_code == 201, r1.text
stored1 = r1.json()
assert stored1["previous_entry_hash"] == "GENESIS"
assert "signature" in stored1
e2 = _entry(phase_id, "trsl:entry:ledgertest0002", "development-credit", 500, "2026-08-02T00:00:00Z")
r2 = client.post(f"/phases/{phase_id}/ledger", json=e2, headers=auth_headers(pg_container["token"]))
assert r2.status_code == 201, r2.text
stored2 = r2.json()
from target_revenue import hashing
assert stored2["previous_entry_hash"] == hashing.entry_hash(stored1)
# Full chain, as exported, must independently verify.
hashing.verify_chain([stored1, stored2])
def test_client_cannot_supply_previous_entry_hash_or_signature(client, pg_container, registered_phase):
phase_id = registered_phase["phase"]["id"]
forged = _entry(phase_id, "trsl:entry:ledgertest0003", "development-credit", 1, "2026-08-01T00:00:00Z")
forged["previous_entry_hash"] = "GENESIS"
resp = client.post(f"/phases/{phase_id}/ledger", json=forged, headers=auth_headers(pg_container["token"]))
assert resp.status_code == 422
assert "must not be supplied" in resp.json()["detail"]
def test_other_licensor_cannot_append_to_this_phase(client, pg_container, registered_phase):
phase_id = registered_phase["phase"]["id"]
e = _entry(phase_id, "trsl:entry:ledgertest0004", "development-credit", 1, "2026-08-01T00:00:00Z")
resp = client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["other_token"]))
assert resp.status_code == 422
assert "not authorized" in resp.json()["detail"]
def test_currency_mismatch_rejected(client, pg_container, registered_phase):
phase_id = registered_phase["phase"]["id"]
e = _entry(phase_id, "trsl:entry:ledgertest0005", "development-credit", 1, "2026-08-01T00:00:00Z")
e["currency"] = "EUR"
resp = client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"]))
assert resp.status_code == 422
assert "currency" in resp.json()["detail"]
def test_hosted_ledger_export_folds_identically_to_offline_computation(
client, pg_container, registered_phase
):
"""The property T04's task description calls out as highest-risk:
hosted append + export, folded offline, must match what was appended."""
from target_revenue import fold, hashing
phase_id = registered_phase["phase"]["id"]
plan = [
("development-credit", 25000),
("development-credit", 10000),
("remission-credit", 5000),
("credit-reversal", 2000),
]
for i, (entry_type, amount) in enumerate(plan, start=1):
e = _entry(
phase_id,
f"trsl:entry:ledgertestfold{i:04d}",
entry_type,
amount,
f"2026-08-0{i}T00:00:00Z",
)
if entry_type == "credit-reversal":
e["reverses"] = "trsl:entry:ledgertestfold0001"
resp = client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"]))
assert resp.status_code == 201, resp.text
export = client.get(f"/phases/{phase_id}/ledger")
assert export.status_code == 200
entries = export.json()
assert len(entries) == len(plan)
# Chain integrity survives the round trip through Postgres/JSONB/API.
hashing.verify_chain(entries)
result = fold.fold_outstanding_target(
registered_phase["phase"]["initial_target"]["amount"], entries
)
expected_development_credit = 25000 + 10000 - 2000
expected_remission_credit = 5000
assert result.development_credit == expected_development_credit
assert result.remission_credit == expected_remission_credit
assert result.outstanding_target == max(
0.0,
registered_phase["phase"]["initial_target"]["amount"]
- expected_development_credit
- expected_remission_credit,
)
def test_public_key_endpoint_unauthenticated_and_verifies_signature(client, pg_container, registered_phase):
phase_id = registered_phase["phase"]["id"]
e = _entry(phase_id, "trsl:entry:ledgertestpk0001", "development-credit", 1, "2026-08-01T00:00:00Z")
stored = client.post(
f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"])
).json()
pk_resp = client.get("/public-key")
assert pk_resp.status_code == 200
assert pk_resp.json()["algorithm"] == "ed25519"
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from target_revenue import hashing
public_key = Ed25519PublicKey.from_public_bytes(
bytes.fromhex(pk_resp.json()["public_key_hex"])
)
assert hashing.verify_record_signature(stored, stored["signature"], public_key)
def test_application_role_cannot_update_or_delete_ledger_entries(pg_container):
with psycopg.connect(pg_container["app_dsn"]) as conn:
with pytest.raises(psycopg.errors.InsufficientPrivilege):
conn.execute("UPDATE ledger_entries SET signature = 'x' WHERE entry_id = 'nonexistent'")
conn.rollback()
with pytest.raises(psycopg.errors.InsufficientPrivilege):
conn.execute("DELETE FROM ledger_entries WHERE entry_id = 'nonexistent'")
conn.rollback()
def test_duplicate_entry_id_rejected(client, pg_container, registered_phase):
phase_id = registered_phase["phase"]["id"]
e = _entry(phase_id, "trsl:entry:ledgertestdup0001", "development-credit", 1, "2026-08-01T00:00:00Z")
first = client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"]))
assert first.status_code == 201
second = client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"]))
assert second.status_code == 422
assert "already exists" in second.json()["detail"]

View file

@ -147,7 +147,7 @@ system Python (no service deps required).
```task
id: TREV-WP-0006-T04
status: todo
status: done
priority: high
state_hub_task_id: "591e87d6-3b64-4f1f-b870-edf834edc522"
```
@ -159,6 +159,35 @@ export must get the same Outstanding Target as the hosted service — this
is the property to test most aggressively, since it's the one a hosted
service could most easily and least visibly break.
**Result:** `migrations/0002_ledger.sql` adds `ledger_entries`
(`GENERATED ALWAYS AS IDENTITY` sequence for exact append order; `trf_app`
again has no UPDATE/DELETE grant). `src/target_revenue/ledger.py`'s
`append_entry()`: rejects any caller-supplied `previous_entry_hash`/
`signature` outright (server-computed only), enforces phase ownership
(a Licensor may only append to its own registered Phase — TS-FR-8
isolation), serializes concurrent appends per Phase via
`pg_advisory_xact_lock`, computes the chain tip from the last stored entry
and signs with the Trust Service instance's own Ed25519 key
(`service/keys.py`, env-configured or ephemeral-with-warning for dev),
reusing `validation.py`'s existing schema/currency checks unchanged.
`service/app.py` adds `POST/GET /phases/{id}/ledger` and an unauthenticated
`GET /public-key` (so an external party can verify signatures without
trusting this API's own access control at all). Fixed an unrelated route
ordering bug found while wiring this in: `{phase_id:path}` on the plain
`GET /phases/{id}` route was greedily matching `/ledger`-suffixed paths
too, since phase IDs contain colons but no slashes and never needed the
`:path` converter — switched all phase routes to plain `{phase_id}`.
`tests/test_ledger_hosting.py` (8 tests, Docker-gated): hash-chain linkage
across appends, forged-hash/signature rejection, cross-Licensor isolation,
currency-mismatch rejection, duplicate-entry-id rejection, DB-level
UPDATE/DELETE privilege checks, signature verification via the public-key
endpoint, and — the task's own highest-priority property — appending a
mixed development-credit/remission-credit/credit-reversal sequence through
the API, exporting it, and confirming `fold.fold_outstanding_target` over
the export reproduces the exact expected Development Credit, Remission
Credit, and Outstanding Target. Offline 36-test suite re-verified unchanged
with plain system Python; no stray Docker containers left running.
## Metrics service
```task