Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c1c40db
fix(vite): add COEP header to enable SharedArrayBuffer for snarkjs WA…
IyanuOluwaJesuloba Jul 17, 2026
d3f0e8d
fix(circuit): incorporate avg_balance into reputation score computation
IyanuOluwaJesuloba Jul 17, 2026
a1f8e09
fix(frontend): correct Silver tier color in ReputationCard and ProofH…
IyanuOluwaJesuloba Jul 17, 2026
713c5db
fix(contracts): add byte-length validation in reputation_registry sub…
IyanuOluwaJesuloba Jul 17, 2026
d597712
feat(sdk): export proof encoding helpers; remove dead code in ProofGe…
IyanuOluwaJesuloba Jul 17, 2026
3be4a55
feat(sdk): add buildSubmitProofTransaction to NulliusClient
IyanuOluwaJesuloba Jul 18, 2026
03d6f3d
fix(sdk): replace fixed-interval polling with exponential backoff in …
IyanuOluwaJesuloba Jul 18, 2026
178f094
test(contracts): add quote and send unit tests to payment_gate
IyanuOluwaJesuloba Jul 18, 2026
09197b1
chore(env): add VITE_FEE_COLLECTOR and VITE_NATIVE_TOKEN to .env.example
IyanuOluwaJesuloba Jul 18, 2026
e3a76cc
docs: add LICENSE file; fix placeholder GitHub link; update README
IyanuOluwaJesuloba Jul 18, 2026
3df1c6c
refactor(sdk/frontend): document public signal ordering; remove dead …
IyanuOluwaJesuloba Jul 18, 2026
a57b18c
feat(frontend): wire ErrorBoundary to configurable remote error repor…
IyanuOluwaJesuloba Jul 18, 2026
10395e8
chore(sdk): pin runtime dependencies to exact versions for reproducib…
IyanuOluwaJesuloba Jul 18, 2026
62f8cc0
docs(changelog): update CHANGELOG with all improvements from this ses…
IyanuOluwaJesuloba Jul 18, 2026
5ab9c62
fix(scripts): add WASM size validation and transient RPC retry to dep…
IyanuOluwaJesuloba Jul 18, 2026
73efc5e
refactor(sdk/frontend): centralise TIER_COLORS in sdk/src/types.ts
IyanuOluwaJesuloba Jul 18, 2026
9c5a76f
feat(a11y): improve accessibility in ProofGenerator
IyanuOluwaJesuloba Jul 18, 2026
ae423ae
ci: fix WASM target, add feat/** triggers, WASM size check, security …
IyanuOluwaJesuloba Jul 18, 2026
62ee3f0
chore(scripts): add contracts:lint, contracts:fmt, test:all npm scripts
IyanuOluwaJesuloba Jul 18, 2026
63a96b8
fix(scripts): graceful degradation in e2e_test when SECRET_KEY is not…
IyanuOluwaJesuloba Jul 18, 2026
ed06ca2
test(contracts): add submit_proof integration tests to reputation_reg…
IyanuOluwaJesuloba Jul 18, 2026
a94ec1b
feat(frontend): add clear history button to ProofHistory
IyanuOluwaJesuloba Jul 18, 2026
b3cfc97
feat(sdk/frontend): add getLimit to NulliusClient; show tx limit in P…
IyanuOluwaJesuloba Jul 18, 2026
b801843
chore/bun
IyanuOluwaJesuloba Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Nullius — Environment variables
# Copy this file to .env and fill in values after deploying.
# Never commit .env to version control.

# Stellar network (testnet or mainnet)
STELLAR_NETWORK=testnet

# Deployed contract IDs (written by scripts/deploy.js)
GROTH16_VERIFIER_ID=
REPUTATION_REGISTRY_ID=
PAYMENT_GATE_ID=

# Deployer key alias (used by stellar-cli)
STELLAR_DEPLOYER_ALIAS=deployer

# Optional: override RPC endpoint
# STELLAR_RPC_URL=https://soroban-testnet.stellar.org

# ----------------------------------------------------------------
# Frontend (Vite) — prefix with VITE_ so they are exposed to the browser
# ----------------------------------------------------------------

# Contract IDs for the frontend build (mirrors the above without VITE_ prefix)
VITE_GROTH16_VERIFIER_ID=
VITE_REPUTATION_REGISTRY_ID=
VITE_PAYMENT_GATE_ID=

# Soroban-wrapped XLM token address on the target network.
# Testnet: CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
# Mainnet: update after mainnet deployment
VITE_NATIVE_TOKEN=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC

# Treasury address that collects protocol fees.
# In the demo this defaults to the sender (fees are circular).
# Set to a real treasury address before any mainnet deployment.
VITE_FEE_COLLECTOR=

# Optional: end-to-end test secret key (funded testnet account)
# Used by scripts/e2e_test.js — never set this in a shared CI environment
# SECRET_KEY=

# Optional: remote error reporting endpoint (e.g. Sentry ingest or custom)
# When set, ErrorBoundary sends structured JSON via navigator.sendBeacon
# VITE_ERROR_ENDPOINT=
95 changes: 92 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ name: CI

on:
push:
branches: [main, develop]
branches:
- main
- develop
- "feat/**"
- "fix/**"
- "chore/**"
pull_request:
branches: [main, develop]

Expand All @@ -23,7 +28,8 @@ jobs:
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
# wasm32v1-none is the current Soroban WASM target (soroban-sdk ≥ 21)
targets: wasm32v1-none

- name: Cache Cargo registry
uses: actions/cache@v4
Expand All @@ -45,7 +51,7 @@ jobs:
run: cargo test --all

- name: Build WASM release artifacts
run: cargo build --target wasm32-unknown-unknown --release
run: cargo build --target wasm32v1-none --release

# ----------------------------------------------------------------
# SDK type-check + build
Expand Down Expand Up @@ -95,3 +101,86 @@ jobs:

- name: Build frontend
run: npm run build:frontend

# ----------------------------------------------------------------
# WASM artifact size report (runs after contracts build)
# Fails the build if any contract exceeds 100 KB — keeps contract
# costs predictable and catches accidental dependency bloat.
# ----------------------------------------------------------------
wasm-size:
name: WASM Size Check
runs-on: ubuntu-latest
needs: contracts
steps:
- uses: actions/checkout@v4

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32v1-none

- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Build WASM release artifacts
run: cargo build --target wasm32v1-none --release

- name: Report and check WASM sizes
run: |
WASM_DIR="target/wasm32v1-none/release"
MAX_BYTES=102400 # 100 KB
FAIL=0
for f in groth16_verifier reputation_registry payment_gate; do
FILE="$WASM_DIR/${f}.wasm"
if [ -f "$FILE" ]; then
SIZE=$(wc -c < "$FILE")
echo "$f: ${SIZE} bytes ($(echo "scale=1; $SIZE/1024" | bc) KB)"
if [ "$SIZE" -gt "$MAX_BYTES" ]; then
echo " ERROR: exceeds 100 KB limit"
FAIL=1
fi
else
echo "WARNING: $FILE not found"
fi
done
exit $FAIL

# ----------------------------------------------------------------
# Dependency security audit
# ----------------------------------------------------------------
audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm install

- name: Audit npm dependencies
# --audit-level=high: fail only on high/critical vulns, not moderate
run: npm audit --audit-level=high
continue-on-error: true # advisory only for now; remove to enforce

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable

- name: Install cargo-audit
run: cargo install cargo-audit --locked

- name: Audit Cargo dependencies
run: cargo audit
continue-on-error: true # advisory only for now; remove to enforce
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ circuits/keys/verification_key.json
.contract_addresses.json
sdk/src/contract_ids.json

# Environment
# Environment — ignore real secrets, but track the example template
.env
.env.*
.env.local
.env.*.local

# OS
.DS_Store
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,35 @@ Versions follow [Semantic Versioning](https://semver.org/).
- Wallet connection error surfaced in the connect screen UI
- `CONTRIBUTING.md` — development setup, code style, and PR checklist
- `CHANGELOG.md` — this file
- `NulliusClient.buildSubmitProofTransaction(walletAddress, bundle)` — builds unsigned proof submission XDR for Freighter signing; frontend no longer duplicates transaction construction
- `encodeG1`, `encodeG2`, `encodeScalar` exported from SDK — shared by `contracts.ts` and available to consumers; removes duplicate implementations in the frontend
- Byte-length validation in `reputation_registry::submit_proof` — explicit panics with descriptive messages for wrong-length proof components, before the cross-contract call
- `quote` and `send` unit tests for `payment_gate` — mock registry stub, Silver/Unverified fee checks, zero-amount and over-limit rejections
- `VITE_NATIVE_TOKEN`, `VITE_FEE_COLLECTOR`, `VITE_ERROR_ENDPOINT`, `VITE_*` contract IDs added to `.env.example`
- Remote error reporting in `ErrorBoundary` via `navigator.sendBeacon` — fires when `VITE_ERROR_ENDPOINT` is set; swallows failures so reporting never crashes the app
- MIT `LICENSE` file
- Score formula breakdown table in README

### Changed
- `ProofGenerator` no longer imports `Keypair` from `@stellar/stellar-sdk`
- `App.tsx` wraps each tab panel in `ErrorBoundary`
- `groth16_verifier` test module refactored into helper functions for readability
- Silver tier color corrected from `#6b7280` (grey, indistinct from Unverified) to `#94a3b8` (silver-slate) in `ReputationCard` and `ProofHistory`
- `PaymentWidget` reads `VITE_NATIVE_TOKEN` and `VITE_FEE_COLLECTOR` at runtime with testnet fallbacks
- `useFreighter` hook no longer exports unused `sign` method; components call `signTransaction` directly
- `sdk/package.json` runtime dependencies pinned to exact versions (`@stellar/stellar-sdk@12.3.0`, `snarkjs@0.7.6`, `circomlibjs@0.1.7`)
- `.gitignore` `.env.*` exclusion narrowed to `.env.local` and `.env.*.local` so `.env.example` is tracked
- `waitForConfirmation` in SDK replaced fixed 1500ms poll with exponential backoff (1 s → 2 s → 4 s → 8 s, 30 s total budget)
- README deployed contracts table: TBD replaced with reference to `sdk/src/contract_ids.json`
- Placeholder GitHub link `your-repo/nullius` in footer replaced with `nullius-zk/nullius`

### Fixed
- `quote` function in `payment_gate` had an incorrect `#[allow(clippy::too_many_arguments)]` attribute removed (it doesn't take many args)
- `avg_balance` private input was committed via Poseidon hash but never used in `score_proxy` — now incorporated with a cap of 10,000 units; threshold scaling factor updated from 600 to 700; `selectThreshold()` and live score preview in `ProofGenerator` updated to match
- `vite.config.ts` was missing `Cross-Origin-Embedder-Policy: require-corp` header; without it browsers cannot expose `SharedArrayBuffer` and snarkjs falls back to single-threaded WASM (~3× slower)
- Dead `encodeBytes` closure in `ProofGenerator.handleGenerate` removed
- `ProofGenerator` local proof-encoding functions (`encodeG1Bytes`, `encodeG2Bytes`, `encodeScalarBytes`) removed; imports from SDK instead
- JSDoc on `generateReputationProof` documents the snarkjs output ordering vs on-chain input ordering asymmetry that was an undocumented footgun

---

Expand Down
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Nullius Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,29 @@ The ZK proof is not cosmetic — the Soroban contract **cannot be tricked**:
- The `meets_threshold` output is enforced both by the circuit and the verifier contract
- Stellar's native BN254 host functions (Protocol 25/26) make verification cheap

### Score formula

The reputation score is computed inside the ZK circuit (never on-chain):

| Component | Max contribution | Notes |
|-----------|-----------------|-------|
| Transaction count | 40 pts | capped at 50 txs |
| Clean transaction rate | 40 pts | (tx_count − disputes) contribution |
| Wallet age | 20 pts | capped at 12 months |
| Average balance | ~14 pts | capped at 10,000 units (XLM/1000) |

All arithmetic uses integer scaling (factor 700) to avoid division in ZK constraints.

## Deployed contracts (Stellar testnet)

> Updated after deployment via `npm run deploy:testnet`
> Updated after deployment via `npm run deploy:testnet`.
> Run `cat sdk/src/contract_ids.json` to see the latest addresses.

| Contract | Address |
|----------|---------|
| groth16_verifier | TBD |
| reputation_registry | TBD |
| payment_gate | TBD |
| groth16_verifier | See `sdk/src/contract_ids.json` |
| reputation_registry | See `sdk/src/contract_ids.json` |
| payment_gate | See `sdk/src/contract_ids.json` |

## Privacy guarantees

Expand Down
16 changes: 16 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 19 additions & 10 deletions circuits/reputation_score.circom
Original file line number Diff line number Diff line change
Expand Up @@ -82,28 +82,37 @@ template ReputationScore() {
age_lt.in[1] <== 12;
age_capped <== age_lt.out * months_active + (1 - age_lt.out) * 12;

// balance_score: capped at 10,000 units → contributes up to 20 points
// avg_balance is in stroops (1 XLM = 10,000,000 stroops). We normalise by
// dividing conceptually by 1,000 first (caller passes balance / 1000).
// Cap at 10_000 units (equivalent to 100 XLM) to prevent gaming.
signal bal_capped;
component bal_lt = LessThan(32);
bal_lt.in[0] <== avg_balance;
bal_lt.in[1] <== 10000;
bal_capped <== bal_lt.out * avg_balance + (1 - bal_lt.out) * 10000;

// Combined score proxy (avoids division):
// score_proxy = tx_capped * 12 * 40 + age_capped * 50 * 20
// + (tx_count - dispute_count) * 12 * 40
// threshold check done on same scale
// Scale everything by 600 (LCM of 50 and 12) to stay integer:
// tx contribution: tx_capped * 480 (max 50*480=24000)
// clean contribution:(tx_count-disputes)*480 (max 24000)
// age contribution: age_capped * 1000 (max 12000)
// total max = 60000 → 100 points scaled
// threshold_scaled = threshold * 600
// tx contribution: tx_capped * 480 (max 50*480 = 24000)
// clean contribution: clean_txs * 480 (max 50*480 = 24000)
// age contribution: age_capped * 1000 (max 12*1000 = 12000)
// balance contribution: bal_capped * 1 (max 10000*1 = 10000)
// total max = 70000 → threshold_scaled = threshold * 700
// threshold_scaled = threshold * 700

signal clean_txs;
clean_txs <== tx_count - dispute_count;

signal score_proxy;
score_proxy <== tx_capped * 480 + clean_txs * 480 + age_capped * 1000;
score_proxy <== tx_capped * 480 + clean_txs * 480 + age_capped * 1000 + bal_capped;

signal threshold_scaled;
threshold_scaled <== threshold * 600;
threshold_scaled <== threshold * 700;

// -------------------------------------------------------
// 3. Check score_proxy >= threshold_scaled
// score_proxy max = 70000 fits in 17 bits; 32 bits is safe.
// -------------------------------------------------------
component gte = GreaterEqThan(32);
gte.in[0] <== score_proxy;
Expand Down
Loading
Loading