diff --git a/marketmesh_privacy_preserving_federated_/core.py b/marketmesh_privacy_preserving_federated_/core.py index 93f9301..6f27b93 100644 --- a/marketmesh_privacy_preserving_federated_/core.py +++ b/marketmesh_privacy_preserving_federated_/core.py @@ -21,14 +21,16 @@ class Contract: class DeltaSync: """Simplified delta-sync payload carrying aggregated signals with a version vector.""" - def __init__(self, contract_id: str, version_vector: Dict[str, int], payload: Dict[str, float], hash_: str): + def __init__(self, contract_id: str, version_vector: Dict[str, int], payload: Dict[str, float], hash_: str, timestamp: float | None = None): self.contract_id = contract_id self.version_vector = dict(version_vector) self.payload = dict(payload) self.hash = hash_ + self.timestamp = timestamp def __repr__(self) -> str: - return f"DeltaSync(contract_id={self.contract_id}, version_vector={self.version_vector}, hash={self.hash})" + ts = f", timestamp={self.timestamp}" if self.timestamp is not None else "" + return f"DeltaSync(contract_id={self.contract_id}, version_vector={self.version_vector}, hash={self.hash}{ts})" def merge(self, other: "DeltaSync") -> "DeltaSync": """Deterministically merge two DeltaSync payloads for the same contract. @@ -70,6 +72,12 @@ class DeltaSync: "version_vector": merged_version_vector, "payload": merged_payload, } + # If either delta carries a timestamp, include it in the hash to enable replay protection + if (self.timestamp is not None) or (other.timestamp is not None): + ts_self = self.timestamp if self.timestamp is not None else 0.0 + ts_other = other.timestamp if other.timestamp is not None else 0.0 + merged_ts = max(ts_self, ts_other) + payload_for_hash["timestamp"] = merged_ts merged_hash = hashlib.sha256( json.dumps(payload_for_hash, sort_keys=True).encode("utf-8") ).hexdigest()