deltatrace-deterministic-re.../deltatrace/dsl.py

176 lines
5.1 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, Any, Optional, List
import time
@dataclass
class LocalEvent:
event_id: str
event_type: str # e.g., "MDTick", "Signal", etc.
asset: str
venue: str
timestamp: float
payload: Dict[str, Any]
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class PlanDelta:
delta_id: str
timestamp: float
author: str
contract_id: str
delta_payload: Dict[str, Any]
signature: str
@dataclass
class OrderEvent:
order_id: str
timestamp: float
side: str
quantity: int
price: float
provenance: str = ""
delta_id: Optional[str] = None
@dataclass
class FillEvent:
fill_id: str
timestamp: float
order_id: str
quantity: int
price: float
venue: str = ""
delta_id: Optional[str] = None
@dataclass
class RiskCheck:
check_id: str
timestamp: float
result: bool
budget_used: float
details: Dict[str, Any] = field(default_factory=dict)
@dataclass
class AuditLog:
log_id: str
timestamp: float
action: str
payload: Dict[str, Any]
signature: str
@dataclass
class Metadata:
version: str
nonce: str
source_adapter: str
@dataclass
class TraceGraph:
nodes: List[object] = field(default_factory=list)
edges: List[tuple] = field(default_factory=list) # (src_id, dst_id, tag)
def add_node(self, node: object) -> object:
self.nodes.append(node)
return node
def add_edge(self, src_id: str, dst_id: str, tag: str) -> None:
self.edges.append((src_id, dst_id, tag))
def merkle_root(self) -> str:
"""Deterministic Merkle root for the trace graph.
This implementation avoids Python's non-deterministic hash() across
runs by serializing each node to a canonical JSON representation using
its public fields. The resulting bytestrings are hashed in a Merkle-like
reduction to produce a stable root string.
"""
import hashlib
import json
def _canonicalize(obj: object) -> Dict[str, object]:
# If the node is a dataclass or has __dict__, extract a stable dict
if hasattr(obj, "__dict__"):
# Ensure all nested attributes are also representable
data = getattr(obj, "__dict__")
# Deeply serialize to JSON-safe form by iterating keys
return {k: v for k, v in data.items()}
# Fallback: convert to string representation
return {"repr": str(obj)}
items = []
for n in self.nodes:
canon = _canonicalize(n)
# Use a stable JSON representation of the node
serialized = json.dumps(canon, sort_keys=True, separators=(",", ":"))
items.append(serialized.encode())
if not items:
return ""
level = [hashlib.sha256(i).hexdigest() for i in items]
while len(level) > 1:
next_level = []
for i in range(0, len(level), 2):
left = level[i]
right = level[i + 1] if i + 1 < len(level) else left
next_level.append(hashlib.sha256((left + right).encode()).hexdigest())
level = next_level
return level[0]
def merkle_proof(self, index: int) -> List[Dict[str, str]]:
"""Compute a Merkle-like proof for the node at the given index.
This returns a list of dicts, each describing the sibling hash at a
particular tree level required to recompute the Merkle root. Each entry
has:
- direction: 'left' or 'right' indicating the position of the sibling
- hash: the hexadecimal digest of the sibling node at that level
Note: This is a lightweight, deterministic proof suitable for auditing
provenance in MVP deployments. It relies on a fixed leaf ordering (the
order of self.nodes) and a deterministic leaf hashing similar to merkle_root.
"""
import hashlib
import json
def _canonicalize(obj: object) -> Dict[str, object]:
if hasattr(obj, "__dict__"):
data = getattr(obj, "__dict__")
return {k: v for k, v in data.items()}
return {"repr": str(obj)}
if index < 0 or index >= len(self.nodes):
raise IndexError("index out of range for merkle_proof")
# Build leaf hashes from canonicalized node representations
leaves: List[str] = []
for n in self.nodes:
canon = _canonicalize(n)
serialized = json.dumps(canon, sort_keys=True, separators=(",", ":"))
leaves.append(hashlib.sha256(serialized.encode()).hexdigest())
proof: List[Dict[str, str]] = []
idx = index
level = leaves
while len(level) > 1:
# Sibling hash for the current index
if idx % 2 == 0:
sib_index = idx + 1
direction = "right"
else:
sib_index = idx - 1
direction = "left"
if sib_index < len(level):
proof.append({"direction": direction, "hash": level[sib_index]})
else:
# If no true sibling (odd last item), replicate the node itself
proof.append({"direction": direction, "hash": level[idx]})
# Compute next level
next_level: List[str] = []
for i in range(0, len(level), 2):
left = level[i]
right = level[i + 1] if i + 1 < len(level) else level[i]
next_level.append(hashlib.sha256((left + right).encode()).hexdigest())
level = next_level
idx = idx // 2
return proof