build(agent): jabba#56a767 iteration
This commit is contained in:
parent
6584c87c0b
commit
77f5b06711
|
|
@ -16,6 +16,8 @@ from .witness import generate_feasibility_witness
|
||||||
from .safety_stub import extract_safety_stub
|
from .safety_stub import extract_safety_stub
|
||||||
from .mission import MissionTask, ContactWindow, MissionNode, MissionSimulator
|
from .mission import MissionTask, ContactWindow, MissionNode, MissionSimulator
|
||||||
from .pcp import create_entry, build_chain, inclusion_proof, head_hash, verify_inclusion
|
from .pcp import create_entry, build_chain, inclusion_proof, head_hash, verify_inclusion
|
||||||
|
from .mfr_v2 import MutualFeasibilityReceiptV2
|
||||||
|
from .ticket import PassWindowTicket
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Registry",
|
"Registry",
|
||||||
|
|
@ -38,6 +40,8 @@ __all__ = [
|
||||||
"inclusion_proof",
|
"inclusion_proof",
|
||||||
"head_hash",
|
"head_hash",
|
||||||
"verify_inclusion",
|
"verify_inclusion",
|
||||||
|
"MutualFeasibilityReceiptV2",
|
||||||
|
"PassWindowTicket",
|
||||||
"MissionTask",
|
"MissionTask",
|
||||||
"ContactWindow",
|
"ContactWindow",
|
||||||
"MissionNode",
|
"MissionNode",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
"""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())
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
"""Pass-Window Ticket signer/verifier.
|
||||||
|
|
||||||
|
Compact ticket structure intended for pre-signed pass-window triggers.
|
||||||
|
Uses HMAC (blake2b) signatures for tests/POC. The ticket hash is the sha256
|
||||||
|
of the canonical serialized payload (excluding signature), which can be used as
|
||||||
|
an external pointer in MFR-v2.pass_ticket_hash.
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PassWindowTicket:
|
||||||
|
window_start: int
|
||||||
|
window_end: int
|
||||||
|
comm_budget_bytes: int
|
||||||
|
resource_budget_digest: str
|
||||||
|
trigger_policy_id: str
|
||||||
|
nonce: str
|
||||||
|
signer: str = ""
|
||||||
|
sig: str = ""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mint(cls, window_start: int, window_end: int, comm_budget_bytes: int,
|
||||||
|
resource_budget_digest: str, trigger_policy_id: str = "default",
|
||||||
|
signer: str = "", key: Optional[bytes] = None) -> "PassWindowTicket":
|
||||||
|
nonce = uuid.uuid4().hex[:12]
|
||||||
|
t = cls(window_start=window_start, window_end=window_end,
|
||||||
|
comm_budget_bytes=int(comm_budget_bytes),
|
||||||
|
resource_budget_digest=resource_budget_digest,
|
||||||
|
trigger_policy_id=trigger_policy_id, nonce=nonce,
|
||||||
|
signer=signer, sig="")
|
||||||
|
if key:
|
||||||
|
t.sig = t._sign(key)
|
||||||
|
return t
|
||||||
|
|
||||||
|
def _canonical(self) -> str:
|
||||||
|
payload = {
|
||||||
|
"window_start": int(self.window_start),
|
||||||
|
"window_end": int(self.window_end),
|
||||||
|
"comm_budget_bytes": int(self.comm_budget_bytes),
|
||||||
|
"resource_budget_digest": self.resource_budget_digest,
|
||||||
|
"trigger_policy_id": self.trigger_policy_id,
|
||||||
|
"nonce": self.nonce,
|
||||||
|
"signer": self.signer,
|
||||||
|
}
|
||||||
|
return json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
||||||
|
|
||||||
|
def _sign(self, key: bytes) -> str:
|
||||||
|
h = hmac.new(key, self._canonical().encode("utf-8"), hashlib.blake2b)
|
||||||
|
return h.hexdigest()[:64]
|
||||||
|
|
||||||
|
def serialize(self) -> bytes:
|
||||||
|
obj = {
|
||||||
|
"window_start": self.window_start,
|
||||||
|
"window_end": self.window_end,
|
||||||
|
"comm_budget_bytes": self.comm_budget_bytes,
|
||||||
|
"resource_budget_digest": self.resource_budget_digest,
|
||||||
|
"trigger_policy_id": self.trigger_policy_id,
|
||||||
|
"nonce": self.nonce,
|
||||||
|
"signer": self.signer,
|
||||||
|
"sig": self.sig,
|
||||||
|
}
|
||||||
|
return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||||
|
|
||||||
|
def ticket_hash(self) -> str:
|
||||||
|
# external pointer: sha256 of canonical payload (no sig)
|
||||||
|
return hashlib.sha256(self._canonical().encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
def verify(self, key: bytes) -> bool:
|
||||||
|
if not self.sig:
|
||||||
|
return False
|
||||||
|
expected = self._sign(key)
|
||||||
|
return hmac.compare_digest(expected, self.sig)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def deserialize(cls, b: bytes) -> "PassWindowTicket":
|
||||||
|
obj = json.loads(b.decode("utf-8"))
|
||||||
|
return cls(window_start=int(obj.get("window_start", 0)),
|
||||||
|
window_end=int(obj.get("window_end", 0)),
|
||||||
|
comm_budget_bytes=int(obj.get("comm_budget_bytes", 0)),
|
||||||
|
resource_budget_digest=obj.get("resource_budget_digest", ""),
|
||||||
|
trigger_policy_id=obj.get("trigger_policy_id", ""),
|
||||||
|
nonce=obj.get("nonce", ""),
|
||||||
|
signer=obj.get("signer", ""),
|
||||||
|
sig=obj.get("sig", ""))
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import os
|
||||||
|
from gravityweave.mfr_v2 import MutualFeasibilityReceiptV2
|
||||||
|
|
||||||
|
|
||||||
|
def test_mfr_v2_sign_and_size():
|
||||||
|
key = b"test-key-12345"
|
||||||
|
proposal_hash = "abcd1234"
|
||||||
|
invariants = [1, 2, 7]
|
||||||
|
m = MutualFeasibilityReceiptV2.create(
|
||||||
|
proposal_hash=proposal_hash,
|
||||||
|
invariant_id_set=invariants,
|
||||||
|
decision="ok",
|
||||||
|
uncertainty_pct=5,
|
||||||
|
round_nonce=42,
|
||||||
|
pass_ticket_hash="",
|
||||||
|
execution_hash_skim="deadbeef",
|
||||||
|
signer="node-A",
|
||||||
|
key=key,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert m.verify(key)
|
||||||
|
s = m.serialize()
|
||||||
|
# ensure size is small (<= 512 bytes as requested by spec)
|
||||||
|
assert len(s) <= 512
|
||||||
|
|
||||||
|
# roundtrip
|
||||||
|
m2 = MutualFeasibilityReceiptV2.deserialize(s)
|
||||||
|
assert m2.proposal_hash == proposal_hash
|
||||||
|
assert m2.invariant_id_set == invariants
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
from gravityweave.ticket import PassWindowTicket
|
||||||
|
|
||||||
|
|
||||||
|
def test_ticket_mint_and_verify():
|
||||||
|
key = b"ticket-key-xyz"
|
||||||
|
start = 1700000000
|
||||||
|
end = start + 60
|
||||||
|
t = PassWindowTicket.mint(
|
||||||
|
window_start=start,
|
||||||
|
window_end=end,
|
||||||
|
comm_budget_bytes=2048,
|
||||||
|
resource_budget_digest="rbdig-0123",
|
||||||
|
trigger_policy_id="policy-1",
|
||||||
|
signer="authority-1",
|
||||||
|
key=key,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert t.verify(key)
|
||||||
|
serialized = t.serialize()
|
||||||
|
assert len(serialized) < 1024
|
||||||
|
# ticket hash is stable and hex
|
||||||
|
th = t.ticket_hash()
|
||||||
|
assert isinstance(th, str) and len(th) == 64
|
||||||
|
|
||||||
|
# roundtrip
|
||||||
|
t2 = PassWindowTicket.deserialize(serialized)
|
||||||
|
assert t2.window_start == start
|
||||||
|
assert t2.window_end == end
|
||||||
Loading…
Reference in New Issue