Trust Model Audit

Groth16 · BLS12-381 · Stellar Testnet

Full breakdown of what is private, what is publicly verifiable on-chain, and what is enforced off-chain. Judges and auditors should read this page alongside the circuit code.

9
On-chain guarantees
1
Off-chain enforced
2
Private (ZK hidden)
Privacy Boundary In One Screen

AidShield is not anonymous settlement. It is private eligibility with public accountability: the chain receives enough data to verify payment and block replay, while the credential witness and beneficiary identity stay off-chain.

Chain Sees
Merkle root
Nullifier after claim
Payout or vendor wallet
Amount and timing
Contract and verifier IDs
Issuer Sees
Wallet-to-slot issuance record
Credential hash and expiry
Issuer key ID
Delivery mode
HMAC-keyed ledger identifiers, not raw names
Beneficiary Keeps Local
Credential secret
Merkle witness/path
Passphrase-protected QR payload
Local proof generation witness
Name and identity documents
Property-by-Property Trust Breakdown
Claim secretprivate

Delivered inside the signed credential so the beneficiary can prove locally. During claim it is not sent on-chain or to the verifier.

Merkle rooton-chain

Stored in the disbursement contract and checked against the proof's public inputs on every claim.

Nullifier uniquenesson-chain

The contract persists every spent nullifier (Poseidon(secret, disbursement_id, claimant_address, 1)) and rejects duplicates.

Proof validity (Groth16)on-chain

Verified by the Groth16 verifier contract using native BLS12-381 pairing_check on Soroban. Cannot pass without a valid witness.

Wallet binding (circuit-level)on-chain

Phase 4: the Merkle leaf is Poseidon(secret, disbursement_id, claimant_address, expires_at, issuer_key_id). The wallet, expiry, and issuer are committed into the tree at campaign-generation time. A stolen secret cannot generate a valid proof for a different wallet or later expiry.

Credential issuanceoff-chain

The operator signs credentials with an Ed25519 key. Signature verification happens client-side before proof generation. The operator must vet eligibility off-chain.

Credential expiryon-chain

The expiry timestamp is a public circuit input bound into the Merkle leaf and checked against ledger time by the disbursement contract.

Issuer registryon-chain

The issuer_key_id is committed into each Merkle leaf. The contract accepts claims only when that issuer key is active and rejects claims after revocation.

Payout amounton-chain

Fixed in the disbursement contract at initialisation. Cannot be changed without a contract upgrade.

Escrow balanceon-chain

Held in XLM inside the contract. Released directly to the claimant's wallet upon a valid proof submission.

Beneficiary identityprivate

Names, IDs, aid-list membership, and Merkle witness stay off-chain. The claimant wallet is public settlement data and can be linked like any Stellar address.

Claim eventon-chain

Includes nullifier, amount, and claimant wallet. Voucher events also include vendor wallet. These are public accountability data, not anonymous payout data.

Attack Resistance
Double claim (replay)On-chain

Nullifier stored permanently after first claim. Contract rejects any reuse.

Forged proofOn-chain

Groth16 soundness — computationally infeasible to produce a valid proof without the witness. Verified via native BLS12-381 pairing.

Wrong Merkle rootOn-chain

Public input checked against on-chain root. Proof for a different root fails verification.

Secret inference from proofCircuit (ZK)

Secret is a private input. Groth16 zero-knowledge property prevents extraction from proof bytes.

Using another wallet's credentialOn-chain

Phase 4: leaf = Poseidon(secret, disbursement_id, claimant_address, expires_at, issuer_key_id). A different wallet generates a different leaf, so no Merkle proof exists for it. The nullifier remains wallet-bound.

Issuer impersonationOff-chain

ISSUER_PUBLIC_KEY is hardcoded in the claim frontend. Ed25519 signature verified before proof generation begins.

Revoked issuer credentialOn-chain

The issuer key id is a public proof input and must be active in the contract registry when the claim is submitted.

Unauthorized vendor payoutOn-chain

Voucher redemption checks the vendor allowlist before proof execution and payout transfer.

Privacy pool abuseOperator policy

AidShield is not an open mixer. Eligibility is campaign-scoped, issuer-bound, expiry-bound, wallet-bound, and funded from a controlled escrow.

Known Limitations (Hackathon Scope)
  • Credential expiry: Now bound into the ZK leaf and enforced against ledger time by the disbursement contract.
  • Issuer governance: The upgraded contract supports issuer add/revoke plus threshold governor co-signers for sensitive admin operations. Production should add per-issuer limits and a guided multi-party signing UX.
  • Trusted setup (Groth16): The proving key was generated with a single-contributor ceremony (demo only). Production requires a multi-party trusted setup to eliminate the toxic waste risk.
  • Issuance uniqueness: Production should configure Upstash Redis REST variables so credential issuance reserves campaign slot and wallet keys with SET NX. Without Redis, the app uses a locked local-file fallback for demos.
  • Browser proving: A compromised frontend could read a loaded credential witness. Use the official deployment, keep CSP enabled, rotate credentials after suspected compromise, and prefer short expiries.
  • Public settlement: Stellar settlement remains public: payout wallet or vendor, route, amount, timing, contract ids, root, and nullifier are visible by design.
Deployed Contracts
Disbursement contractCARYQO…5D3CZD
Groth16 BLS12-381 verifierCAD7QR…DEJ2KG
Issuer public keyGD3ZN2…OBZDEO
Disbursement ID176bc0…d25922
Merkle root (on-chain)337334…d6d5ad
ZK Proof Specifics
Proof systemGroth16 (snarkjs)
Elliptic curveBLS12-381
Circuit languagecircom 2.1
Hash functionPoseidon (BLS12-381 scalar field)
Private inputssecret, merkle_path (not revealed)
Public inputsdisbursement_id, merkle_root, nullifier, claimant_address, expires_at, issuer_key_id
Proof size384 bytes (G1 + G2 + G1 uncompressed)
On-chain checkNative bls.pairing_check on Soroban
Proving locationBrowser WASM (witness not sent on-chain)
Trusted setupGroth16 (requires ceremony for production)
VK hash (SHA-256)c243d1…d4d128