mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-08 07:13:53 +02:00
113 lines
4.2 KiB
Python
113 lines
4.2 KiB
Python
"""Batched settlement — aggregate counts, no individual records on-chain.
|
|
|
|
Source of truth: ``infonet-economy/IMPLEMENTATION_PLAN.md`` §4.4
|
|
piece 6.
|
|
|
|
Per-redemption records on-chain would be a privacy disaster: an
|
|
observer could correlate "Operator X verified a Function Key at
|
|
14:32" with a citizen's known activities to de-anonymize them.
|
|
|
|
Instead, operators settle in **aggregate**. The chain sees only
|
|
``(operator_id, day_bucket, count)`` — verified N keys this day.
|
|
Fraud detection happens via statistical auditing rather than
|
|
per-redemption traces:
|
|
|
|
- Operator's count vs their declared population (food bank that
|
|
reports 10,000 daily verifications when their service capacity
|
|
is 200).
|
|
- Distribution shape vs other operators (significant outliers
|
|
prompt review).
|
|
- Spot audits via dispute mechanism (citizen + operator surface
|
|
receipt pair to adjudicator).
|
|
|
|
The ``BatchedSettlementBatch`` here is what the operator emits
|
|
to chain at the end of a settlement period. Receipts NEVER appear
|
|
on-chain — they remain off-chain with both parties.
|
|
|
|
Sprint 11+ scaffolding ships:
|
|
|
|
- The aggregate batch dataclass.
|
|
- A ``record_redemption`` helper that operators call locally per
|
|
successful redemption — increments the batch's counter without
|
|
storing the receipt.
|
|
- A ``finalize_batch`` step that produces the on-chain payload.
|
|
|
|
This module is **fully implementable** today — it does no
|
|
cryptography, just bookkeeping.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass
|
|
class BatchedSettlementBatch:
|
|
"""Operator-side batch counter for one settlement period.
|
|
|
|
Operators construct one of these per ``(period_id, operator_id)``
|
|
pair, increment via ``record_redemption`` per successful
|
|
redemption, and emit the finalized batch payload at period end.
|
|
|
|
The data model is intentionally minimal:
|
|
|
|
- ``period_id`` — the settlement window identifier (e.g.
|
|
``"2026-04"`` for monthly).
|
|
- ``operator_id`` — committed publicly on-chain so its
|
|
non-forgeability is anchored.
|
|
- ``successful_count`` — number of successful redemptions
|
|
(verification + fulfillment).
|
|
- ``denial_counts`` — counts per enumerated DenialCode for
|
|
audit visibility. NO per-receipt detail.
|
|
"""
|
|
|
|
period_id: str
|
|
operator_id: str
|
|
successful_count: int = 0
|
|
denial_counts: dict[str, int] = field(default_factory=dict)
|
|
finalized: bool = False
|
|
|
|
def record_redemption(self) -> None:
|
|
"""Increment the success counter. NOT idempotent — call
|
|
exactly once per successful (verification, fulfillment)
|
|
receipt pair the operator commits to."""
|
|
if self.finalized:
|
|
raise RuntimeError("batch already finalized; cannot record")
|
|
self.successful_count += 1
|
|
|
|
def record_denial(self, code: str) -> None:
|
|
"""Track a denial. Operators MUST use one of the enumerated
|
|
``DenialCode`` values — Sprint 11+ scaffolding accepts the
|
|
string for convenience but production callers should pass
|
|
the enum's ``.value``."""
|
|
if self.finalized:
|
|
raise RuntimeError("batch already finalized; cannot record")
|
|
if not isinstance(code, str) or not code:
|
|
raise ValueError("denial code must be a non-empty string")
|
|
self.denial_counts[code] = self.denial_counts.get(code, 0) + 1
|
|
|
|
def finalize(self) -> dict:
|
|
"""Produce the on-chain payload for this batch.
|
|
|
|
After ``finalize()``, ``record_redemption`` and
|
|
``record_denial`` raise. The returned dict is the canonical
|
|
batched-settlement event payload.
|
|
|
|
Privacy property: per-receipt detail is NOT in the output.
|
|
Only counts. The operator may discard receipts after
|
|
finalization (subject to local retention policy for dispute
|
|
defense).
|
|
"""
|
|
if self.finalized:
|
|
raise RuntimeError("batch already finalized")
|
|
self.finalized = True
|
|
return {
|
|
"period_id": self.period_id,
|
|
"operator_id": self.operator_id,
|
|
"successful_count": int(self.successful_count),
|
|
"denial_counts": {k: int(v) for k, v in self.denial_counts.items()},
|
|
}
|
|
|
|
|
|
__all__ = ["BatchedSettlementBatch"]
|