diff --git a/README.md b/README.md index 5bf219a..5d3b64f 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,28 @@ -# FeedTrust: Blockchain-backed Access Control & Provenance for Cross-Venue Market Data Feeds +FeedTrust: Blockchain-backed Access Control & Provenance for Cross-Venue Market Data Feeds Overview -- FeedTrust provides a policy-driven access layer between data venues and trading pipelines. It enforces who can access which data signals (raw quotes, aggregates, latency metrics) under defined conditions and produces cryptographic proofs of access and data lineage anchored to a Merkle-based ledger. +- FeedTrust is a modular, open-source layer that sits between market data venues and algo trading pipelines to enforce fine-grained, policy-driven data access while preserving auditability and traceability across venues. It codifies access policies into a machine-checkable DSL, provides cryptographic provenance proofs, and maintains a tamper-evident ledger for data lineage. -Architecture (Python MVP) -- Policy DSL (policy.py): A lightweight DSL for declaring access rules. -- Adapters (adapters/): Toy FIX and WebSocket adapters that convert feeds into a canonical signal format. -- Aggregator (aggregation.py): Cross-venue data mixing with provenance tagging. -- Provenance Ledger (ledger_merkle.py): Merkle-tree based proofs of data lineage. -- Core (core.py): Orchestrates policy checks, ledger interactions, and end-to-end flow. -- Tests (tests/): Unit and integration tests for policy enforcement, ledger proofs, and end-to-end flow. +Architecture (production-ready mindset) +- Policy DSL core and compiler (feedtrust/policy.py) +- Provenance ledger (Merkle-based) with proofs (feedtrust/ledger_merkle.py) +- Adapters for feeds (FIX/WebSocket) to canonical signals (feedtrust/adapters/*) +- Lightweight aggregation layer for cross-venue signals (feedtrust/aggregation.py) +- Core orchestrator to enforce policies and log provenance (feedtrust/core.py) -Getting Started +MVP Scope (as implemented in this repo) +- Tiny policy DSL with allow and deny rules +- Two toy adapters bridging FIX and WebSocket feeds to canonical signals +- Merkle-based provenance ledger with a simple proof mechanism +- Simple cross-venue aggregator for price-like signals +- End-to-end flow: ingest adapter signals, log to ledger, generate aggregates, expose root proof + +How to Run +- Prerequisites: Python 3.11+ +- Install test dependencies: pytest - Run tests: ./test.sh - Build package: python -m build -Contribution -- This project emphasizes minimal, well-scoped changes with strong test coverage. -- See AGENTS.md for contributor guidelines. +Notes +- This project is designed as an MVP and a proof-of-concept toward a production-grade, contract-governed data-gateway with verifiable provenance. +- See AGENTS.md for repository conventions and testing commands. diff --git a/feedtrust/policy.py b/feedtrust/policy.py index d57776a..ab45512 100644 --- a/feedtrust/policy.py +++ b/feedtrust/policy.py @@ -3,39 +3,66 @@ from typing import List, Dict, Any class PolicyEngine: def __init__(self): - self._policies: List[Dict[str, Any]] = [] + # Separate allow and deny policies for straightforward evaluation order + self._policies: List[Dict[str, Any]] = [] # allowed rules + self._denies: List[Dict[str, Any]] = [] # denied rules def load_policies(self, dsl: str) -> None: # Very small DSL parser for a single-line policy per call. + # Supports two forms: + # allow ... + # deny ... # Example: # allow subject="traderA" venue="venue1" signal="price" action="read" latency_ms=5 volume_cap=1000 regulatory="none" + # deny subject="traderB" venue="venue1" signal="price" action="read" latency_ms=0 for line in [l.strip() for l in dsl.splitlines() if l.strip()]: if not line: continue - if not line.startswith("allow"): - continue - # Remove leading 'allow' - rest = line[len("allow"):].strip() - policy: Dict[str, Any] = {} - # Split by spaces, then parse key=value parts - parts = rest.split() - for part in parts: - if "=" not in part: - continue - k, v = part.split("=", 1) - v = v.strip().strip('"') - # cast common types - if v.isdigit(): - policy[k] = int(v) - else: - policy[k] = v - # Normalize some keys - policy.setdefault("latency_ms", 0) - policy.setdefault("volume_cap", None) - self._policies.append(policy) + # Determine policy type + if line.startswith("allow"): + rest = line[len("allow"):].strip() + policy: Dict[str, Any] = {} + parts = rest.split() + for part in parts: + if "=" not in part: + continue + k, v = part.split("=", 1) + v = v.strip().strip('"') + if v.isdigit(): + policy[k] = int(v) + else: + policy[k] = v + policy.setdefault("latency_ms", 0) + policy.setdefault("volume_cap", None) + self._policies.append(policy) + elif line.startswith("deny"): + rest = line[len("deny"):].strip() + policy: Dict[str, Any] = {} + parts = rest.split() + for part in parts: + if "=" not in part: + continue + k, v = part.split("=", 1) + v = v.strip().strip('"') + if v.isdigit(): + policy[k] = int(v) + else: + policy[k] = v + policy.setdefault("latency_ms", 0) + policy.setdefault("volume_cap", None) + self._denies.append(policy) def check_access(self, subject: str, venue: str, signal: str, action: str) -> bool: - # Simple matcher: any policy that matches all provided fields grants access + # Deny policies take precedence: if any deny matches, deny access + for p in self._denies: + if ( + p.get("subject") in (subject, "any") + and p.get("venue") in (venue, "any") + and p.get("signal") == signal + and p.get("action") == action + ): + return False + # Otherwise, evaluate allow policies for p in self._policies: if ( p.get("subject") in (subject, "any") diff --git a/test.sh b/test.sh old mode 100644 new mode 100755