251 lines
8.4 KiB
Python
251 lines
8.4 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import sqlite3
|
|
import time
|
|
from dataclasses import dataclass, asdict
|
|
from typing import Any, Dict, List, Optional, cast
|
|
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
from cryptography.hazmat.primitives import serialization
|
|
|
|
|
|
def _hash(data: Any) -> str:
|
|
h = hashlib.sha256()
|
|
h.update(json.dumps(data, sort_keys=True, default=str).encode())
|
|
return h.hexdigest()
|
|
|
|
|
|
def _canonical_payload(data: Any) -> bytes:
|
|
return json.dumps(data, sort_keys=True, default=str, separators=(",", ":")).encode()
|
|
|
|
|
|
@dataclass
|
|
class LocalProblem:
|
|
problem_id: str
|
|
assets: List[str]
|
|
objectives: List[str]
|
|
|
|
|
|
@dataclass
|
|
class SharedVariable:
|
|
name: str
|
|
value: Any
|
|
version: int = 0
|
|
|
|
|
|
@dataclass
|
|
class Attestation:
|
|
signer_pubkey_pem: bytes
|
|
signature: bytes
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"signer_pubkey_pem": self.signer_pubkey_pem.decode(),
|
|
"signature": self.signature.hex(),
|
|
}
|
|
|
|
def verify(self, message: bytes) -> bool:
|
|
public_key = serialization.load_pem_public_key(self.signer_pubkey_pem)
|
|
try:
|
|
cast(Any, public_key).verify(self.signature, message)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
@dataclass
|
|
class PlanDelta:
|
|
delta_id: str
|
|
changes: Dict[str, Any]
|
|
timestamp: float
|
|
delta_hash: str = ""
|
|
attestation: Optional[Attestation] = None
|
|
|
|
def compute_hash(self) -> str:
|
|
self.delta_hash = _hash({"delta_id": self.delta_id, "changes": self.changes, "timestamp": self.timestamp})
|
|
return self.delta_hash
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
payload = asdict(self)
|
|
if self.attestation is not None:
|
|
payload["attestation"] = self.attestation.to_dict()
|
|
return payload
|
|
|
|
|
|
@dataclass
|
|
class ProvenanceEvent:
|
|
event_id: str
|
|
experiment_id: str
|
|
actor: str
|
|
timestamp: float
|
|
contract_id: str
|
|
delta_hash: str
|
|
attestation: Attestation
|
|
policy_flags: List[str]
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
d = asdict(self)
|
|
d["attestation"] = self.attestation.to_dict()
|
|
return d
|
|
|
|
|
|
@dataclass
|
|
class ProvenanceRecord:
|
|
event: ProvenanceEvent
|
|
verified: bool
|
|
|
|
|
|
class PortaLedger:
|
|
def __init__(self, db_path: str = ":memory:"):
|
|
self.conn = sqlite3.connect(db_path)
|
|
self._init_schema()
|
|
self._signer = Ed25519PrivateKey.generate()
|
|
|
|
def _init_schema(self):
|
|
cur = self.conn.cursor()
|
|
cur.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS provenance_events (
|
|
event_id TEXT PRIMARY KEY,
|
|
experiment_id TEXT,
|
|
actor TEXT,
|
|
timestamp REAL,
|
|
contract_id TEXT,
|
|
delta_hash TEXT,
|
|
signer_pubkey_pem BLOB,
|
|
attestation BLOB,
|
|
policy_flags TEXT
|
|
)
|
|
"""
|
|
)
|
|
cur.execute(
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS idx_provenance_experiment
|
|
ON provenance_events (experiment_id, timestamp)
|
|
"""
|
|
)
|
|
cur.execute(
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS idx_provenance_contract
|
|
ON provenance_events (contract_id, timestamp)
|
|
"""
|
|
)
|
|
self.conn.commit()
|
|
|
|
def sign_delta(self, delta: PlanDelta) -> PlanDelta:
|
|
delta.compute_hash()
|
|
# Sign the delta hash as a simple attestation
|
|
data = delta.delta_hash.encode()
|
|
signature = self._signer.sign(data)
|
|
pub = self._signer.public_key().public_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
)
|
|
delta.attestation = Attestation(signer_pubkey_pem=pub, signature=signature)
|
|
return delta
|
|
|
|
def verify_delta(self, delta: PlanDelta) -> bool:
|
|
if not delta.delta_hash:
|
|
delta.compute_hash()
|
|
if not delta.attestation:
|
|
return False
|
|
return delta.attestation.verify(delta.delta_hash.encode())
|
|
|
|
def log_provenance(self, event: ProvenanceEvent) -> None:
|
|
cur = self.conn.cursor()
|
|
cur.execute(
|
|
"INSERT OR REPLACE INTO provenance_events (event_id, experiment_id, actor, timestamp, contract_id, delta_hash, signer_pubkey_pem, attestation, policy_flags) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
(
|
|
event.event_id,
|
|
event.experiment_id,
|
|
event.actor,
|
|
event.timestamp,
|
|
event.contract_id,
|
|
event.delta_hash,
|
|
event.attestation.signer_pubkey_pem if event.attestation else None,
|
|
event.attestation.signature if event.attestation else None,
|
|
json.dumps(event.policy_flags),
|
|
),
|
|
)
|
|
self.conn.commit()
|
|
|
|
def record_event(self, experiment_id: str, actor: str, contract_id: str, delta: PlanDelta, policy_flags: Optional[List[str]] = None) -> ProvenanceEvent:
|
|
signed = self.sign_delta(delta)
|
|
attestation = signed.attestation
|
|
assert attestation is not None
|
|
event = ProvenanceEvent(
|
|
event_id=_hash({"experiment_id": experiment_id, "contract_id": contract_id, "delta_hash": signed.delta_hash, "ts": time.time()}),
|
|
experiment_id=experiment_id,
|
|
actor=actor,
|
|
timestamp=time.time(),
|
|
contract_id=contract_id,
|
|
delta_hash=signed.delta_hash,
|
|
attestation=attestation,
|
|
policy_flags=policy_flags or [],
|
|
)
|
|
self.log_provenance(event)
|
|
return event
|
|
|
|
def fetch_event(self, event_id: str) -> Optional[ProvenanceEvent]:
|
|
cur = self.conn.cursor()
|
|
cur.execute("SELECT event_id, experiment_id, actor, timestamp, contract_id, delta_hash, signer_pubkey_pem, attestation, policy_flags FROM provenance_events WHERE event_id = ?", (event_id,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
return None
|
|
attestation = Attestation(signer_pubkey_pem=row[6] or b"", signature=row[7] or b"")
|
|
event = ProvenanceEvent(event_id=row[0], experiment_id=row[1], actor=row[2], timestamp=row[3], contract_id=row[4], delta_hash=row[5], attestation=attestation, policy_flags=json.loads(row[8]))
|
|
return event
|
|
|
|
def query_events(self, experiment_id: Optional[str] = None, actor: Optional[str] = None, contract_id: Optional[str] = None) -> List[ProvenanceEvent]:
|
|
clauses = []
|
|
params: List[Any] = []
|
|
if experiment_id is not None:
|
|
clauses.append("experiment_id = ?")
|
|
params.append(experiment_id)
|
|
if actor is not None:
|
|
clauses.append("actor = ?")
|
|
params.append(actor)
|
|
if contract_id is not None:
|
|
clauses.append("contract_id = ?")
|
|
params.append(contract_id)
|
|
query = "SELECT event_id, experiment_id, actor, timestamp, contract_id, delta_hash, signer_pubkey_pem, attestation, policy_flags FROM provenance_events"
|
|
if clauses:
|
|
query += " WHERE " + " AND ".join(clauses)
|
|
query += " ORDER BY timestamp ASC"
|
|
cur = self.conn.cursor()
|
|
cur.execute(query, tuple(params))
|
|
rows = cur.fetchall()
|
|
events: List[ProvenanceEvent] = []
|
|
for row in rows:
|
|
events.append(
|
|
ProvenanceEvent(
|
|
event_id=row[0],
|
|
experiment_id=row[1],
|
|
actor=row[2],
|
|
timestamp=row[3],
|
|
contract_id=row[4],
|
|
delta_hash=row[5],
|
|
attestation=Attestation(signer_pubkey_pem=row[6] or b"", signature=row[7] or b""),
|
|
policy_flags=json.loads(row[8]),
|
|
)
|
|
)
|
|
return events
|
|
|
|
def verify_event(self, event_id: str) -> bool:
|
|
event = self.fetch_event(event_id)
|
|
if event is None:
|
|
return False
|
|
if not event.attestation.signer_pubkey_pem or not event.attestation.signature:
|
|
return False
|
|
return event.attestation.verify(event.delta_hash.encode())
|
|
|
|
def aggregate_privacy_budget(self, events: List[ProvenanceEvent]) -> Dict[str, Any]:
|
|
return {
|
|
"event_count": len(events),
|
|
"unique_actors": sorted({event.actor for event in events}),
|
|
"delta_hashes": [event.delta_hash for event in events],
|
|
}
|