Skip to content

subho004/ppdc-confidential-graph

Repository files navigation

Privacy-Preserving Financial Data Consortium

License: MIT Python 3.14 FastAPI uv Ruff Type checked: mypy

A research-grade FastAPI demonstration where multiple banks collaboratively detect fraud across a shared, anonymous relationship graph — without ever exposing raw customer PII.

Privacy-preserving set intersection (PSI), homomorphic encryption (HE), differential privacy (DP), and graph-based fraud propagation, wired into a single runnable dashboard. Research/education only — not production-hardened.

Table of contents

Highlights

  • Anonymous by construction — raw PAN/Aadhaar/phone/email/name/address is never stored; only HMAC/blinded tokens, ciphertexts, and anonymous UUIDs.
  • Cross-bank linkage with zero shared PII via ECDH PSI + MinHash/LSH fuzzy resolution and union-find consolidation.
  • Encrypted risk attributes — Paillier additive HE (CKKS/TenSEAL when a wheel is available), decryptable only by the key-holder.
  • DP-protected analytics with a persisted, enforced privacy budget (429 once exhausted).
  • Graph fraud propagation — weighted diffusion and Personalized PageRank over the anonymous relationship graph.
  • One command to see it all — a single /demo/run call (or dashboard button) runs the full pipeline end to end.

Screenshots

Fraud propagation over the anonymous graph

Dashboard on load Active bank selected
Dashboard on load — banks loaded, graph stats shown. Active source selected — acting as a specific bank.
DP analytics Risk lookup
DP analytics — noised totals + remaining ε budget. Authorized risk lookup — key-holder decrypts one entity.
Bank contribution
Bank console — push records as the active source.

📚 Documentation: docs/notes.md (deep architecture, algorithms & citations) · docs/future.md (scaling, real-world deployment & broader scope).

The full loop: seed → contribute (PSI) → resolve → graph → encrypt (HE) → propagate fraud → DP-protected analytics, with four enforced invariants:

  • I1 — no raw PAN/Aadhaar/phone/email/name/address is ever stored; only HMAC/blinded tokens, ciphertexts, and anonymous UUIDs.
  • I2 — ground-truth fraud labels live only in the synthetic-data module and are used only for validation, never by the live code path.
  • I3 — every DP analytics query debits a persisted privacy budget; an exhausted budget returns 429.
  • I4 — HE plaintext is recoverable only by the key-holder; keys never enter any table or API response.

Architecture (layers)

routes (app/api/v1/*)   → validate → call service → response envelope
services (app/services)  → orchestrate use-cases, own engines via injected deps
engines (app/engines)    → pure algorithms: PSI · resolution · HE · DP · graph
repositories (app/repos) → SQLite access + in-memory graph singleton
models / schemas         → SQLAlchemy ORM · Pydantic DTOs

Storage: SQLite only (data/consortium.db, file-backed, WAL). The graph is derived state rebuilt from SQLite on startup. Set DATABASE_URL=sqlite+aiosqlite:///:memory: for tests.

Crypto backends degrade gracefully. CKKS (TenSEAL) has no Python 3.14 wheel yet, so the HE backend falls back to Paillier (auto default); set HE_BACKEND=null for fast, no-encryption runs. DP uses OpenDP.

Uses Python 3.14.2 and uv for fast, reproducible dependency management.

Prerequisites

Install uv (see the docs):

curl -LsSf https://astral.sh/uv/install.sh | sh

Setup

  1. Create a virtual environment with the pinned Python version:
uv venv --python 3.14.2
  1. Activate it:
source .venv/bin/activate
  1. Install dependencies:
uv pip install -r requirements.txt

Run the app

Start the FastAPI server with Uvicorn:

uv run uvicorn main:app --reload

Then open:

  • Dashboard (Bootstrap + Cytoscape): http://127.0.0.1:8000/ui/
  • OpenAPI docs: http://127.0.0.1:8000/docs
  • Health check: http://127.0.0.1:8000/ or /health

One-click demo

The fastest path — generate data, seed, resolve, encrypt, propagate, and run DP analytics in a single call (or click ⚡ One-click demo in the dashboard):

curl -X POST localhost:8000/api/v1/demo/run \
  -H 'content-type: application/json' \
  -d '{"banks":5,"customers":2000,"seed":7}'

Using the dashboard (/ui/)

(Screenshots of every panel are at the top of this README.)

The dashboard is a single page split into a left control column and a right visualization column. A typical session:

  1. Click ⚡ One-click demo (top of Synthetic Data) — this generates data, seeds it, resolves identities, encrypts risk, runs propagation, and queries DP analytics. The graph, top-risk list, and metrics all populate.
  2. Pick a bank in the Active source dropdown (top-right) and use the Bank Console to push more records as that bank — simulating a second/third input feeding the same consortium.
  3. Re-run propagation, animate the spread, look up an exact risk, and query DP analytics until the budget is exhausted.

What each control does

Navbar

Control What it does
Active source (dropdown) Selects which registered bank you are "acting as". Every Bank Console action is submitted from this bank — the no-auth way to simulate multiple input sources.
status pill (right) Shows the last API call and any error (e.g. budget exhausted).

🏦 Bank Console — the active input source

Control What it does
Add (name + CODE) Registers a new bank (POST /banks/register) and selects it as active.
local ref / phone / email / PAN + Submit record Submits one raw record as the active bank. The values are HMAC-tokenized server-side at the bank boundary (the bank's local step) and then ingested — raw PII is never stored (POST /banks/{id}/simulate-contribution). Auto-runs resolution so cross-bank links form immediately.
Push 10 random Generates 10 random tokenized records (chained by shares edges) and ingests them as the active bank — quick way to grow a source.
View this bank's batches Lists the active bank's contribution batches (GET /banks/{id}/contributions).

Try this: register ALPHA and BETA, submit a record under ALPHA with phone 9991112222, then one under BETA with 999-111-2222. After resolution they collapse into one anonymous entity — cross-bank linkage with zero shared PII.

1 · Synthetic Data

Control What it does
Banks / Customers / Seed Parameters for generation; the seed makes datasets reproducible.
⚡ One-click demo Runs the whole pipeline (POST /demo/run).
Generate Writes CSV/JSON dataset files only (POST /datasets/generate).
Seed Loads the dataset files through the real ingest + resolution pipeline (POST /datasets/seed).
Reset Wipes all data and clears the graph, keeping HE keys (POST /admin/prune). Dataset files are preserved, so you can re-seed the same world.

2 · Fraud Propagation

Control What it does
Algorithm Weighted diffusion (bounded, decaying BFS from seeds) or Personalized PageRank (fraud-proximity stationary distribution).
Top K How many highest-scoring nodes to return.
Run propagation Seeds from filed fraud reports, scores every node, writes the scores back encrypted, and renders the top-risk list + the neighborhood of the riskiest node (POST /fraud/propagation/run).
▶ Animate risk spread Replays the diffusion frontier hop-by-hop over the rendered graph (client-side animation).

3 · Risk Lookup (authorized)

Control What it does
anonymous entity id + Decrypt exact risk Authorized, access-logged exact lookup — the key-holder decrypts that one entity's risk/exposure ciphertext (POST /queries/risk). Leave blank to use the top node.

4 · DP Analytics & Budget

Control What it does
Query DP summary Returns differentially-private consortium totals (Laplace-noised) and the remaining privacy budget (GET /analytics/summary, /analytics/budget/status). Repeated queries spend the budget; once exhausted the call returns 429 and the panel says so.

Right column

Panel What it shows
Anonymous Relationship Graph Cytoscape view of the riskiest node's 2-hop neighborhood; node colour = relative risk, edge width = relationship strength. Nodes are anonymous IDs only.
Top fraud-proximity Ranked list of highest-scoring entities with score bars.
Consortium metrics Banks/records/entities/edges, DP-noised counts, and validation precision/recall/F1 vs the synthetic ground truth.

Seed / prune / re-seed (data keeps coming after the demo)

The SQLite store is disposable; the dataset files are the durable seed.

# 1. (Re)generate dataset files into demo/datasets/
uv run python scripts/regenerate_datasets.py          # edit constants for scale

# 2. Prune ALL data (drops + recreates tables, clears graph; keeps HE keys)
uv run python scripts/reset.py                        # set RESET_KEYS=True to rotate

# 3. Re-seed from the dataset files through the real ingest pipeline
uv run python scripts/seed.py

Equivalent HTTP API (against a running server / the dashboard "Reset" button):

curl -X POST localhost:8000/api/v1/admin/prune        -d '{"reset_keys":false}' -H 'content-type: application/json'
curl -X POST localhost:8000/api/v1/datasets/generate  -d '{"banks":5,"customers":2000,"seed":7}' -H 'content-type: application/json'
curl -X POST localhost:8000/api/v1/datasets/seed

Hard reset: rm -f data/consortium.db* then restart (schema is recreated empty).

Incremental additions need no prune — POST /api/v1/banks/{id}/contributions again (each call is a tracked batch; tokens dedupe) then POST /api/v1/entities/resolve.

Test

uv run pytest -v                                       # in-memory SQLite, null HE
uv run ruff check . && uv run mypy --explicit-package-bases .

Docker

Build and run the API with Docker Compose:

docker compose up --build

Then open the same URLs as a local run — dashboard at http://127.0.0.1:8000/ui/, docs at /docs.

What the image does:

  • Multi-stage build on python:3.14.2-slim; dependencies are installed with uv into an isolated virtualenv, then copied into a lean runtime stage.
  • Runs as a non-root user and ships a HEALTHCHECK against /health.

Persistence. Consortium state (the SQLite DB, HE keys, and snapshots under data/) is stored in a named volume, so it survives down/up and rebuilds:

docker compose down          # stops containers, keeps the volume + your data
docker compose down -v       # also removes the volume — wipes all state

Configuration. Compose reads an optional .env file (copy from .env.sample). At minimum, override the tokenization secret for any non-local use:

# .env
HMAC_SECRET=<a-long-random-secret>
HE_BACKEND=auto        # auto | ckks | paillier | null

HOST, PORT, and DATABASE_URL are fixed to container-appropriate values in docker-compose.yml, so anything you set for those in .env is ignored inside the container.

API reference (summary)

All paths below are prefixed with /api/v1. All responses use the {status, message, data} envelope; list endpoints paginate. Full interactive docs at /docs.

Area Endpoints
Banks & contributions POST /banks/register · GET /banks/list · DELETE /banks/{id}/remove · POST /banks/{id}/contributions (tokenized) · POST /banks/{id}/simulate-contribution (raw, tokenized server-side) · GET /banks/{id}/contributions · GET /contributions/{batch_id}/status
Entity resolution POST /entities/resolve · GET /entities/list · GET /entities/{id}/profile · POST /entities/{id}/merge · POST /entities/{id}/split
Graph GET /graph/nodes/{id}/neighborhood · GET /graph/stats · POST /graph/edges/add · DELETE /graph/edges/{id}/remove · POST /graph/rebuild
Fraud & queries POST /fraud/reports · GET /fraud/reports/list · POST /fraud/propagation/run · GET /fraud/propagation/runs · POST /queries/risk
Analytics (DP) GET /analytics/summary · GET /analytics/regions · GET /analytics/budget/status
Datasets / admin / demo POST /datasets/generate · GET /datasets/list · POST /datasets/seed · POST /admin/prune · POST /admin/snapshot · POST /admin/restore · POST /demo/run

References & sources

Core stack

Private entity resolution (PSI + fuzzy linkage + consolidation)

  • ECDH / DH-based PSI (app/engines/psi/ecdh.py) — commutative double-blinding. Design mirrors OpenMined/PSI and IETF draft-wang-ppm-ecdh-psi. The prime-order group is RFC 3526 (2048-bit MODP). Foundational PSI: Meadows, A More Efficient Cryptographic Matchmaking Protocol (1986); Huberman, Franklin & Hogg (1999).
  • MinHash + LSH blocking (app/engines/resolution/minhash_lsh.py) — Broder, On the Resemblance and Containment of Documents (MinHash, 1997); Indyk & Motwani, Approximate Nearest Neighbors (LSH, STOC 1998); Leskovec, Rajaraman & Ullman, Mining of Massive Datasets, ch. 3. Hashing via mmh3 (MurmurHash3). Private record linkage via LSH+PSI: arXiv:2203.14284.
  • Union-find (app/engines/resolution/union_find.py) — Tarjan, Efficiency of a Good but Not Linear Set Union Algorithm (JACM 1975); path compression + union by rank.
  • Local tokenization (utils/hashing.py) — HMAC-SHA256, RFC 2104 / FIPS 198-1.

Homomorphic encryption (encrypted risk attributes)

  • Paillier additive HE (app/engines/he/paillier_phe.py) via python-paillier (phe) — Paillier, Public-Key Cryptosystems Based on Composite Degree Residuosity Classes (EUROCRYPT 1999).
  • CKKS approximate HE (app/engines/he/ckks_tenseal.py) via TenSEAL — Cheon, Kim, Kim & Song, Homomorphic Encryption for Arithmetic of Approximate Numbers (ASIACRYPT 2017). Production successor OpenFHE (§13.2). Note: no TenSEAL wheel for Python 3.14 yet — the app falls back to Paillier.

Differential privacy (protected analytics)

Graph algorithms (fraud propagation)

  • NetworkX (networkx) — graph store & algorithms.
  • Personalized PageRank (app/engines/graph/pagerank.py) — Page, Brin, Motwani & Winograd, The PageRank Citation Ranking (1999); Haveliwala, Topic-Sensitive PageRank (WWW 2002).
  • Weighted BFS risk diffusion (app/engines/graph/diffusion.py) — bounded, decaying label/influence spread (cf. independent-cascade / heat-diffusion models).
  • Future — GNNs (§13.3): GNNs for Financial Fraud Detection: A Review (arXiv:2411.05815); GraphSAGE (Hamilton et al., NeurIPS 2017); GAT (Veličković et al., ICLR 2018).

Synthetic data & frontend

Threat model & invariants are summarized above and detailed in docs/notes.md.

Notes

  • uv run executes commands inside the project environment automatically — no manual activation needed.
  • For production deployments, replace --reload with a production-ready configuration.

Contributing

Contributions are welcome! This is a research/education project, so improvements to correctness, clarity, and the cryptographic backends are especially valued.

  1. Fork the repo and create a feature branch.
  2. Set up the environment (see Setup) and install the pre-commit hooks:
    uv run pre-commit install
  3. Make your change with tests, then run the checks:
    uv run pytest -v
    uv run ruff check . && uv run mypy --explicit-package-bases .
  4. Open a pull request describing the change and, where relevant, which of the four invariants (I1–I4) it touches.

Please keep the privacy invariants intact — no raw PII should ever be persisted or returned by an endpoint.

License

Released under the MIT License.

Disclaimer: This project is a demonstration for research and educational purposes. It is not audited or hardened for production use with real personal or financial data.

About

Preserving Data Consortium (PPDC) demonstration for collaborative, graph-based financial fraud detection using PSI, Homomorphic Encryption, and Differential Privacy.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors