mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-10-12 01:48:58 +02:00
release: prepare v0.9.7
This commit is contained in:
1 parent
ea457f27da
commit
28b3bd5ebf
670 files changed
+187059
-14005
No files matched your search
@@ -0,0 +1,92 @@
|
||||
"""Nullifiers — one-time-use markers per (key, operator) pair.
|
||||
|
||||
Source of truth: ``infonet-economy/IMPLEMENTATION_PLAN.md`` §4.4
|
||||
piece 2.
|
||||
|
||||
For each Function Key + operator combination, the nullifier is
|
||||
|
||||
nullifier = SHA-256(secret || operator_id)
|
||||
|
||||
Properties this gives us:
|
||||
|
||||
- **One-time-use per operator.** The operator records the nullifier
|
||||
on first use; subsequent attempts with the same nullifier are
|
||||
rejected (denial code ``NULLIFIER_ALREADY_SEEN``).
|
||||
- **Cross-operator unlinkability.** Different ``operator_id``s
|
||||
produce different nullifiers for the same secret. Two operators
|
||||
comparing notes cannot determine that the same key was used at
|
||||
both — they see two unrelated 32-byte strings.
|
||||
- **No identity leakage.** The nullifier is a hash; the secret is
|
||||
never exposed.
|
||||
|
||||
Operators MUST commit ``operator_id`` publicly so its non-forgeability
|
||||
is anchored on chain. Nullifier derivation depends on a
|
||||
non-forgeable ``operator_id`` (an attacker who could impersonate an
|
||||
operator could harvest nullifiers).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
def derive_nullifier(*, secret: bytes, operator_id: str) -> str:
|
||||
"""Return the hex SHA-256 of ``secret || operator_id`` (UTF-8 for
|
||||
operator_id).
|
||||
|
||||
Stable across reboots / sessions / operating systems — the same
|
||||
inputs always produce the same output. That's the whole property
|
||||
a nullifier needs: deterministic and unforgeable.
|
||||
"""
|
||||
if not isinstance(secret, (bytes, bytearray)):
|
||||
raise TypeError("secret must be bytes")
|
||||
if not isinstance(operator_id, str) or not operator_id:
|
||||
raise ValueError("operator_id must be a non-empty string")
|
||||
h = hashlib.sha256()
|
||||
h.update(bytes(secret))
|
||||
h.update(b"|") # explicit separator so concatenation is unambiguous
|
||||
h.update(operator_id.encode("utf-8"))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
@dataclass
|
||||
class NullifierTracker:
|
||||
"""Operator-side store of seen nullifiers.
|
||||
|
||||
Sprint 11+ runway: this is the in-memory reference implementation.
|
||||
Production operators use a persistent, atomic-write store
|
||||
(database row + uniqueness constraint) so the "already-seen"
|
||||
check is robust to crashes between the check and the receipt.
|
||||
|
||||
The interface is designed for that: ``check_and_record`` is the
|
||||
only mutation method, and it's atomic — checks then records as
|
||||
one operation. Production wraps this in a database transaction.
|
||||
"""
|
||||
|
||||
seen: set[str] = field(default_factory=set)
|
||||
|
||||
def has_seen(self, nullifier: str) -> bool:
|
||||
return nullifier in self.seen
|
||||
|
||||
def check_and_record(self, nullifier: str) -> bool:
|
||||
"""Return ``True`` if the nullifier was unseen (and is now
|
||||
recorded). Return ``False`` if it was already seen — the
|
||||
operator MUST then issue a denial with code
|
||||
``NULLIFIER_ALREADY_SEEN``.
|
||||
|
||||
The check + record is atomic by design: a concurrent caller
|
||||
racing with this method will not produce two ``True`` results
|
||||
for the same nullifier. (In-memory: trivially atomic. Production:
|
||||
wrap in DB unique-insert.)
|
||||
"""
|
||||
if nullifier in self.seen:
|
||||
return False
|
||||
self.seen.add(nullifier)
|
||||
return True
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NullifierTracker",
|
||||
"derive_nullifier",
|
||||
]
|
||||
Reference in new issue
Block a user