idea94-portaledger-verifiab.../src/portaledger/core.py

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],
}