A master's thesis project demonstrating Zero-Knowledge Machine Learning (ZKML) in the financial sector. The system verifies a client's creditworthiness using a logistic regression model without revealing sensitive financial data on the blockchain or to external servers.
ZK-SNARK proof generation runs locally in the browser (WebAssembly via SnarkJS). The smart contract on Sepolia only verifies the proof and anchors the final decision.
The project is divided into 4 modules:
-
ml-model/(Python, scikit-learn)- Trains a classifier on the German Credit dataset.
- Exports quantized weights, scaler parameters, and
model_config.json.
-
zero-knowledge/(Circom, SnarkJS)- Circom circuit replicating fixed-point model inference.
- Builds
credit_classifier.wasmand proving keys (Groth16).
-
blockchain/(Solidity, Hardhat Ignition)Groth16Verifier.sol— on-chain proof verifier.CreditRegistry.sol— verifies proof and stores(isApproved, timestamp)per user and application ID.
-
web/(Angular 21, Tailwind CSS, ethers.js)- SPA with wallet connection (MetaMask).
- Loads
model_config.json, preprocesses form inputs (StandardScaler + fixed-point), generates proof in-browser, anchors decision on Sepolia.
Form (raw features)
→ StandardScaler + SCALE=1000 (model_config.json)
→ Circom private inputs x[4]
→ Groth16 proof + public signal y (0/1)
→ CreditRegistry.anchorCreditDecision()
→ On-chain: isApproved, timestamp, applicationId
Raw form values (duration, credit_amount, etc.) never leave the browser as plain calldata. Only the ZK proof and public decision bit are sent in the transaction.
Fixed-point features are encoded as unsigned 32-bit integers (two's complement) before being passed to the Circom circuit, because Num2Bits(32) requires non-negative field elements.
- Node.js 18+
- Python 3 (for model training)
- MetaMask with Sepolia ETH
- npm
cd ml-model
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt # if present, or install sklearn/openml manually
python train.py
cp model_config.json ../web/public/model_config.jsonAfter retraining, always copy the updated model_config.json to web/public/ and rebuild/redeploy the Circom verifier if weights changed.
cd zero-knowledge
# follow your existing circom/snarkjs build pipeline
cp build/credit_classifier_js/credit_classifier.wasm ../web/public/
cp keys/credit_classifier_0001.zkey ../web/public/
cp ../blockchain/contracts/Verifier.sol # regenerate after circuit changecd blockchain
npm install
npx hardhat compile
npx hardhat ignition deploy ignition/modules/CreditZKMLModule.ts --network sepolia --resetUpdate NG_APP_REGISTRY_ADDRESS in web/.env (copy from web/.env.example if needed) with the deployed CreditRegistry address from the Ignition output. The value is injected at build time via web/src/environments/environment.ts (auto-generated on npm start / npm run build).
cd web
npm install
ng serveThe repo root includes netlify.toml (base directory web/, publish dist/web/browser).
- Connect the GitHub repo in Netlify.
- Site configuration → Environment variables — add:
NG_APP_REGISTRY_ADDRESS= your deployedCreditRegistryaddress (same as inweb/.env.example)- optional:
NG_APP_ENV=production(Netlify setsNODE_ENV=productionduring build anyway)
- Deploy.
npm run buildrunsprebuild→env:generate, which reads Netlify env vars and bakes the address into the bundle.
Local .env is not uploaded (gitignored). Only Netlify dashboard variables are used in CI.
SPA routing (/about) is handled by netlify.toml redirects.
cd web
npm testThis verifies that exampleRaw from model_config.json produces the same fixed-point features ([-240, -28, -870, -1015]), score (786), and decision (1) as ml-model/train.py.
Use these form values (from exampleRaw):
| Field | Value |
|---|---|
| Duration | 18 |
| Credit Amount | 3190 |
| Installment Commitment | 2 |
| Age | 24 |
Expected model decision: Credit approved.
Per (userAddress, applicationId) the registry stores:
isApproved— model decision (0 or 1)timestamp— block timeexists— replay protection flag
The contract does not persist raw features, the ZK proof, or model weights. Private inputs remain in the browser.
- Verify, don't trust: The verifier checks the Groth16 proof against the deployed circuit.
- Private inputs:
x[4]are witness inputs in Circom; only public outputyis revealed in the proof. - Preprocessing parity: The Angular app uses the same StandardScaler parameters and fixed-point arithmetic as the Python training pipeline.
zkml-credit-project/
├── ml-model/ # Training + model_config.json
├── zero-knowledge/ # Circom circuit + keys
├── blockchain/ # Solidity + Hardhat Ignition
└── web/ # Angular frontend
└── public/
├── model_config.json
├── credit_classifier.wasm
└── credit_classifier_0001.zkey