176 lines
5.1 KiB
Python
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
|