idea182-implementation/gravityweave/mfr_v2.py

120 lines
4.6 KiB
Python

"""Mutual Feasibility Receipt v2 (MFR-v2)
Compact, byte-stable receipt suitable for DTN-era custody handoffs. This
implementation focuses on a small JSON representation with deterministic
serialization and an HMAC-based signature for tests/POC. Fields are chosen to
keep serialized size <= 512 bytes in typical use.
Fields (compact):
- receipt_id: short random id
- proposal_hash: short hex digest of proposal
- invariant_id_set: list[int] (small ints)
- decision: "ok"/"partial"/"deny"
- uncertainty_pct: coarse uncertainty percent (0..100)
- pass_ticket_hash: optional hex pointer to pre-signed ticket
- execution_hash_skim: optional short hex prefix (e.g., blake2b prefix)
- round_nonce, ts, signer, sig
"""
from dataclasses import dataclass
from typing import List, Optional
import time
import uuid
import json
import hmac
import hashlib
@dataclass
class MutualFeasibilityReceiptV2:
receipt_id: str
proposal_hash: str
invariant_id_set: List[int]
decision: str
uncertainty_pct: int
round_nonce: int
ts: float
pass_ticket_hash: str = ""
execution_hash_skim: str = ""
signer: str = ""
sig: str = ""
@classmethod
def create(cls, proposal_hash: str, invariant_id_set: List[int], decision: str,
uncertainty_pct: int = 0, round_nonce: int = 0,
pass_ticket_hash: Optional[str] = None,
execution_hash_skim: Optional[str] = None,
signer: str = "", key: Optional[bytes] = None) -> "MutualFeasibilityReceiptV2":
rid = uuid.uuid4().hex[:12]
ts = time.time()
r = cls(receipt_id=rid, proposal_hash=proposal_hash,
invariant_id_set=list(invariant_id_set), decision=decision,
uncertainty_pct=int(uncertainty_pct), round_nonce=int(round_nonce), ts=ts,
pass_ticket_hash=pass_ticket_hash or "",
execution_hash_skim=execution_hash_skim or "",
signer=signer, sig="")
if key:
r.sig = r._sign(key)
return r
def _canonical_payload(self) -> str:
# deterministic and compact JSON over authoritative fields (exclude sig)
payload = {
"receipt_id": self.receipt_id,
"proposal_hash": self.proposal_hash,
"invariant_id_set": self.invariant_id_set,
"decision": self.decision,
"uncertainty_pct": int(self.uncertainty_pct),
"round_nonce": int(self.round_nonce),
"ts": int(self.ts),
"pass_ticket_hash": self.pass_ticket_hash,
"execution_hash_skim": self.execution_hash_skim,
"signer": self.signer,
}
return json.dumps(payload, sort_keys=True, separators=(",", ":"))
def _sign(self, key: bytes) -> str:
h = hmac.new(key, self._canonical_payload().encode("utf-8"), hashlib.blake2b)
# use hex digest truncated to 64 chars to keep sizes small
return h.hexdigest()[:64]
def verify(self, key: bytes) -> bool:
if not self.sig:
return False
expected = self._sign(key)
return hmac.compare_digest(expected, self.sig)
def serialize(self) -> bytes:
obj = {
"receipt_id": self.receipt_id,
"proposal_hash": self.proposal_hash,
"invariant_id_set": self.invariant_id_set,
"decision": self.decision,
"uncertainty_pct": self.uncertainty_pct,
"round_nonce": self.round_nonce,
"ts": self.ts,
"pass_ticket_hash": self.pass_ticket_hash,
"execution_hash_skim": self.execution_hash_skim,
"signer": self.signer,
"sig": self.sig,
}
return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode("utf-8")
@classmethod
def deserialize(cls, b: bytes) -> "MutualFeasibilityReceiptV2":
obj = json.loads(b.decode("utf-8"))
return cls(receipt_id=obj.get("receipt_id", ""),
proposal_hash=obj.get("proposal_hash", ""),
invariant_id_set=[int(x) for x in obj.get("invariant_id_set", [])],
decision=obj.get("decision", "deny"),
uncertainty_pct=int(obj.get("uncertainty_pct", 0)),
round_nonce=int(obj.get("round_nonce", 0)),
ts=float(obj.get("ts", 0.0)),
pass_ticket_hash=obj.get("pass_ticket_hash", ""),
execution_hash_skim=obj.get("execution_hash_skim", ""),
signer=obj.get("signer", ""),
sig=obj.get("sig", ""))
@property
def size_bytes(self) -> int:
return len(self.serialize())