mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-08 07:13:53 +02:00
222 lines
7.1 KiB
Python
222 lines
7.1 KiB
Python
"""Two-phase commit receipts + enumerated denial codes.
|
|
|
|
Source of truth: ``infonet-economy/IMPLEMENTATION_PLAN.md`` §4.4
|
|
pieces 5-6.
|
|
|
|
Two phases per redemption:
|
|
|
|
1. **Verification receipt** (operator → citizen). Operator signs:
|
|
``(receipt_id, operator_id, day_bucket, nullifier_prefix)``.
|
|
Note: NO timestamp (day-bucket only), NO node_id, NO nullifier
|
|
in full (a prefix that's still distinct enough for fraud auditing
|
|
but doesn't leak the full unforgeable nullifier).
|
|
|
|
2. **Fulfillment receipt** (citizen → operator). Citizen counter-
|
|
signs the verification receipt after service is rendered. Both
|
|
parties hold a copy.
|
|
|
|
Receipts are NEVER published on-chain. They surface only in
|
|
disputes. Settlement to chain happens through batched aggregation
|
|
(``batched_settlement.py``).
|
|
|
|
Denial codes are an **enumerated** set with exactly three values.
|
|
Operators cannot reject for freeform reasons — that would be a
|
|
discrimination vector. The three reasons are:
|
|
|
|
- ``INVALID_SIGNATURE`` — challenge-response verification failed.
|
|
- ``NULLIFIER_ALREADY_SEEN`` — the (key, operator) pair has already
|
|
redeemed once.
|
|
- ``RATE_LIMIT_EXCEEDED`` — operator-defined throttle (per-day,
|
|
per-hour, etc.) prevents this redemption.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import secrets
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from enum import Enum
|
|
|
|
|
|
class DenialCode(str, Enum):
|
|
"""Enumerated rejection reasons. Adding a new code is a hard fork."""
|
|
INVALID_SIGNATURE = "invalid_signature"
|
|
NULLIFIER_ALREADY_SEEN = "nullifier_already_seen"
|
|
RATE_LIMIT_EXCEEDED = "rate_limit_exceeded"
|
|
|
|
|
|
def _day_bucket(timestamp: float) -> str:
|
|
"""Return the UTC day in ``YYYY-MM-DD`` form for ``timestamp``.
|
|
|
|
Day-level granularity prevents fine-grained timestamp metadata
|
|
from becoming a de-anonymization vector. An operator that issued
|
|
100 receipts on the same day cannot link them by timestamp —
|
|
they all carry the same day_bucket.
|
|
"""
|
|
return datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%d")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Receipt:
|
|
"""One side of the two-phase commit.
|
|
|
|
``role`` is either ``"verification"`` (Phase 1, operator-signed)
|
|
or ``"fulfillment"`` (Phase 2, citizen counter-signed).
|
|
"""
|
|
role: str
|
|
receipt_id: str
|
|
operator_id: str
|
|
day_bucket: str
|
|
nullifier_prefix: str
|
|
signature: bytes
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ReceiptPair:
|
|
"""Both phases of a successful redemption.
|
|
|
|
Held by both citizen and operator. Surfaces only on dispute —
|
|
the chain never sees these.
|
|
"""
|
|
verification: Receipt
|
|
fulfillment: Receipt
|
|
|
|
|
|
def _sign(secret: bytes, body: bytes) -> bytes:
|
|
return hmac.new(secret, body, hashlib.sha256).digest()
|
|
|
|
|
|
def _verify(secret: bytes, body: bytes, signature: bytes) -> bool:
|
|
expected = _sign(secret, body)
|
|
return hmac.compare_digest(expected, signature)
|
|
|
|
|
|
def _receipt_body(*, role: str, receipt_id: str, operator_id: str,
|
|
day_bucket: str, nullifier_prefix: str) -> bytes:
|
|
return b"|".join([
|
|
b"function_key_receipt",
|
|
role.encode("utf-8"),
|
|
receipt_id.encode("utf-8"),
|
|
operator_id.encode("utf-8"),
|
|
day_bucket.encode("utf-8"),
|
|
nullifier_prefix.encode("utf-8"),
|
|
])
|
|
|
|
|
|
def issue_verification_receipt(
|
|
*,
|
|
operator_id: str,
|
|
operator_secret: bytes,
|
|
nullifier: str,
|
|
timestamp: float,
|
|
receipt_id: str | None = None,
|
|
nullifier_prefix_len: int = 8,
|
|
) -> Receipt:
|
|
"""Operator-side: issue a Phase-1 verification receipt.
|
|
|
|
``nullifier_prefix`` is the first ``nullifier_prefix_len`` hex
|
|
chars of the full nullifier — enough for the operator to dispute
|
|
later (fraud auditing) but NOT enough to identify the citizen
|
|
cross-operator. 8 hex chars = 32 bits = ~4 billion possible
|
|
prefixes, statistically unlinkable across operators.
|
|
"""
|
|
if not isinstance(nullifier, str) or len(nullifier) < nullifier_prefix_len:
|
|
raise ValueError("nullifier must be a hex string of sufficient length")
|
|
rid = receipt_id or secrets.token_hex(16)
|
|
prefix = nullifier[:nullifier_prefix_len]
|
|
day = _day_bucket(timestamp)
|
|
body = _receipt_body(
|
|
role="verification", receipt_id=rid, operator_id=operator_id,
|
|
day_bucket=day, nullifier_prefix=prefix,
|
|
)
|
|
sig = _sign(operator_secret, body)
|
|
return Receipt(
|
|
role="verification",
|
|
receipt_id=rid,
|
|
operator_id=operator_id,
|
|
day_bucket=day,
|
|
nullifier_prefix=prefix,
|
|
signature=sig,
|
|
)
|
|
|
|
|
|
def counter_sign_fulfillment(
|
|
*,
|
|
verification: Receipt,
|
|
citizen_secret: bytes,
|
|
) -> Receipt:
|
|
"""Citizen-side: counter-sign a verification receipt to acknowledge
|
|
service rendered.
|
|
|
|
The fulfillment receipt has the same field values as the
|
|
verification receipt (linking them to the same redemption) but
|
|
is signed with the CITIZEN's secret instead of the operator's.
|
|
Together they form a ``ReceiptPair``.
|
|
"""
|
|
if verification.role != "verification":
|
|
raise ValueError("input must be a Phase-1 verification receipt")
|
|
body = _receipt_body(
|
|
role="fulfillment", receipt_id=verification.receipt_id,
|
|
operator_id=verification.operator_id, day_bucket=verification.day_bucket,
|
|
nullifier_prefix=verification.nullifier_prefix,
|
|
)
|
|
sig = _sign(citizen_secret, body)
|
|
return Receipt(
|
|
role="fulfillment",
|
|
receipt_id=verification.receipt_id,
|
|
operator_id=verification.operator_id,
|
|
day_bucket=verification.day_bucket,
|
|
nullifier_prefix=verification.nullifier_prefix,
|
|
signature=sig,
|
|
)
|
|
|
|
|
|
def verify_receipt_pair(
|
|
*,
|
|
pair: ReceiptPair,
|
|
operator_secret: bytes,
|
|
citizen_secret: bytes,
|
|
) -> bool:
|
|
"""Verify both signatures on a ``ReceiptPair``.
|
|
|
|
Useful in dispute resolution — both parties can independently
|
|
confirm the pair is genuine.
|
|
"""
|
|
if pair.verification.role != "verification":
|
|
return False
|
|
if pair.fulfillment.role != "fulfillment":
|
|
return False
|
|
if pair.verification.receipt_id != pair.fulfillment.receipt_id:
|
|
return False
|
|
if pair.verification.operator_id != pair.fulfillment.operator_id:
|
|
return False
|
|
v_body = _receipt_body(
|
|
role="verification", receipt_id=pair.verification.receipt_id,
|
|
operator_id=pair.verification.operator_id,
|
|
day_bucket=pair.verification.day_bucket,
|
|
nullifier_prefix=pair.verification.nullifier_prefix,
|
|
)
|
|
if not _verify(operator_secret, v_body, pair.verification.signature):
|
|
return False
|
|
f_body = _receipt_body(
|
|
role="fulfillment", receipt_id=pair.fulfillment.receipt_id,
|
|
operator_id=pair.fulfillment.operator_id,
|
|
day_bucket=pair.fulfillment.day_bucket,
|
|
nullifier_prefix=pair.fulfillment.nullifier_prefix,
|
|
)
|
|
if not _verify(citizen_secret, f_body, pair.fulfillment.signature):
|
|
return False
|
|
return True
|
|
|
|
|
|
__all__ = [
|
|
"DenialCode",
|
|
"Receipt",
|
|
"ReceiptPair",
|
|
"counter_sign_fulfillment",
|
|
"issue_verification_receipt",
|
|
"verify_receipt_pair",
|
|
]
|