NOAH (Network for On-chain Authenticated Handshakes) is a zero-knowledge proof-based Know Your Customer (KYC) system that enables DeFi protocols to verify user eligibility without exposing personal information. Users can prove they meet compliance requirements (age, jurisdiction, accreditation status) while maintaining complete privacy.
- Overview
- Use Cases
- Zero-Knowledge Proof Mathematics
- ZK Circuit Implementation
- Architecture
- System Components
- Key Flows
- Protocol Guide: Setting Requirements
- User Guide: Accessing DeFi Protocols
- Issuer Guide: Managing Credentials
- Getting Started
- Development
- API Reference
- Security Considerations
- Roadmap & Future Enhancements
- License
- Resources
This system enables selective disclosure of KYC credentials using zero-knowledge proofs. Users can prove they meet specific requirements (e.g., "I am over 21 and from an allowed jurisdiction") without revealing their actual age, location, or other sensitive data.
- 🔒 Privacy-Preserving: Personal data never leaves the user's device
- ✅ Selective Disclosure: Users only prove what's required
- 🚀 On-Chain Verification: Fast, gas-efficient proof verification
- 🔐 Revocable Credentials: Issuers can revoke credentials when needed
- 🏛️ Multi-Protocol Support: Each DeFi protocol sets its own requirements
NOAH enables DeFi protocols to verify user eligibility for compliance (KYC/AML) without exposing personal data. Users prove they meet requirements (age, jurisdiction, accreditation) while keeping their actual data private.
Problem: Protocols need to verify user eligibility (age, location, accreditation) but want to protect user privacy.
Solution: Users generate ZK proofs showing they meet requirements without revealing exact values.
Example: A lending protocol requires users to be 18+ and from allowed jurisdictions. Users prove eligibility without sharing their exact age or location.
Use Case: Services requiring minimum age (e.g., 18+, 21+).
Example: A DeFi protocol restricts access to users 21+ in certain jurisdictions. Users prove they meet both without revealing exact age or location.
Use Case: Protocols that must restrict access by jurisdiction (e.g., US-only, EU-compliant).
Example: A protocol allows only users from specific countries. Users prove membership in the allowed set without revealing their exact jurisdiction.
Use Case: Protocols requiring accredited investor status for certain products.
Example: An investment platform requires accredited status. Users prove they are accredited without revealing other personal details.
Use Case: Users can reuse the same credential across multiple protocols.
Benefit: One KYC credential can be used across multiple DeFi protocols, each with different requirements.
Use Case: Organizations that need to demonstrate compliance without exposing user data.
Benefit: Maintains regulatory compliance while protecting user privacy.
- Want to access DeFi protocols while maintaining privacy
- Need to prove eligibility without exposing personal information
- Want to reuse credentials across multiple protocols
- Need to verify user eligibility for compliance
- Want to protect user privacy
- Need flexible, customizable requirements
- Want on-chain verification for transparency
- Organizations that issue and verify credentials
- Need to manage credential lifecycle (issue, revoke)
- Want to maintain trust and compliance
Requirement: Users must be 18+ and from allowed jurisdictions.
Flow: User generates a ZK proof showing they meet requirements → DEX verifies on-chain → Access granted.
Requirement: Users must be accredited investors for certain products.
Flow: User proves accredited status → Protocol verifies → Access to premium products.
Requirement: Protocol allows users from specific countries.
Flow: User proves jurisdiction membership → Protocol verifies → Access granted without revealing exact location.
- Privacy: Personal data never leaves the user's device
- Compliance: Protocols can verify eligibility on-chain
- Flexibility: Each protocol sets its own requirements
- Efficiency: On-chain verification is fast and gas-efficient
- Security: Credentials are revocable and tamper-proof
- Reusability: One credential works across multiple protocols
NOAH bridges privacy and compliance in DeFi: users maintain privacy while protocols meet regulatory requirements. It enables selective disclosure—users prove what's needed, nothing more.
This is especially valuable in DeFi, where privacy and compliance are both important.
This section provides a comprehensive mathematical foundation for understanding the zero-knowledge proof system used in this protocol. The implementation is based on the Groth16 proof system, a pairing-based zk-SNARK construction.
The Groth16 proof system is a pairing-based zero-knowledge succinct non-interactive argument of knowledge (zk-SNARK) that enables efficient proof generation and verification. It is particularly well-suited for on-chain verification due to its small proof size and fast verification time.
Groth16 relies on bilinear pairings, which are mathematical functions that map pairs of points from two elliptic curve groups into a third group. The key property of pairings is bilinearity:
e(a·P, Q) = e(P, a·Q) = e(P, Q)^a
where e is the pairing function, P and Q are points on elliptic curves, and a is a scalar.
This property allows us to verify complex arithmetic relationships without revealing the underlying values, making it ideal for zero-knowledge proofs.
The protocol uses the BN254 (Barreto-Naehrig) elliptic curve, which provides efficient pairing operations. BN254 defines two elliptic curve groups:
- G1: Points on the curve
y² = x³ + 3over the base field Fp - G2: Points on a twisted curve over the extension field Fp2
BN254 Curve Parameters:
- Base field modulus:
P = 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47 - Scalar field modulus:
R = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001 - Embedding degree:
k = 12(provides 128-bit security level)
G1 Group:
- Defined over the base field Fp
- Points are represented as
(x, y)coordinates wherex, y ∈ Fp - Group order equals the scalar field modulus R
- Used for proof components A and C, and public input commitments
G2 Group:
- Defined over the extension field Fp2 = Fp[i]/(i² + 1)
- Points are represented as
(x₀ + x₁·i, y₀ + y₁·i)wherex₀, x₁, y₀, y₁ ∈ Fp - Used for proof component B and verification key elements
Target Group GT:
- The pairing maps G1 × G2 → GT
- GT is a multiplicative subgroup of Fp12
- Used for the final pairing check in verification
The protocol operates over three distinct fields:
1. Base Field Fp:
- Prime modulus:
P = 21888242871839275222246405745257275088696311157297823662689037894645226208583 - All elliptic curve coordinates (G1 points) are elements of Fp
- Arithmetic operations: addition, multiplication, inversion, square root (all modulo P)
2. Scalar Field Fr:
- Prime modulus:
R = 21888242871839275222246405745257275088548364400416034343698204186575808495617 - Used for all witness values and public inputs in the circuit
- All private and public inputs must be reduced modulo R
- The circuit constraints operate over this field
3. Extension Field Fp2:
- Constructed as
Fp2 = Fp[i]/(i² + 1) - Elements are represented as
a₀ + a₁·iwherea₀, a₁ ∈ Fp - Used for G2 point coordinates
- Arithmetic:
(a₀ + a₁·i) + (b₀ + b₁·i) = (a₀ + b₀) + (a₁ + b₁)·i - Multiplication:
(a₀ + a₁·i) · (b₀ + b₁·i) = (a₀·b₀ - a₁·b₁) + (a₀·b₁ + a₁·b₀)·i
Field Relationship:
Fp (base field) ⊂ Fp2 (extension field) ⊂ Fp12 (target field)
Fr (scalar field) - independent, used for circuit arithmetic
The core of Groth16 verification is checking the following pairing equation:
e(A, B) = e(α, β) · e(C, δ) · ∏ᵢ (e(PUBᵢ, γ))^xᵢ
Where:
A ∈ G1, B ∈ G2, C ∈ G1: Proof components (computed by the prover)α ∈ G1, β ∈ G2, γ ∈ G2, δ ∈ G2: Verification key elements (from trusted setup)PUBᵢ ∈ G1: Public input commitment points (from verification key)xᵢ ∈ Fr: Public input valuese: Bilinear pairing function
Verification Process:
-
Compute public input linear combination:
L_pub = CONSTANT + ∑ᵢ (xᵢ · PUBᵢ)where
CONSTANTis the constant term from the verification key. -
Check pairing equation:
e(A, B) = e(α, -β) · e(C, -δ) · e(L_pub, -γ)In practice, we use negated forms (
-β,-δ,-γ) to optimize the on-chain computation, as the verification key stores these pre-computed negated values. -
On-chain implementation (from
src/ZKVerifier.sol):- Uses Ethereum's pairing precompile at address
0x08 - Verifies four pairings:
e(A, B),e(C, -δ),e(α, -β),e(L_pub, -γ) - Returns success if the product equals 1 in GT
- Uses Ethereum's pairing precompile at address
The verification key (VK) contains the following elements from the trusted setup:
- α ∈ G1: Alpha point (single point)
- β ∈ G2: Beta point (negated and stored as
BETA_NEG) - γ ∈ G2: Gamma point (negated and stored as
GAMMA_NEG) - δ ∈ G2: Delta point (negated and stored as
DELTA_NEG) - CONSTANT ∈ G1: Constant term for public input linear combination
- PUBᵢ ∈ G1: Public input commitment points (one per public input)
For this circuit with 14 public inputs, the verification key contains:
- 1 constant point
- 14 public input points (PUB_0 through PUB_13)
Trusted Setup:
- The current implementation uses a single-party trusted setup via
gnark'sgroth16.Setup() - For production deployments, a multi-party ceremony (PPOT - Powers of Tau) is recommended
- The trusted setup generates proving key (PK) and verification key (VK) that are cryptographically linked
- If the setup is compromised, false proofs can be generated, but existing proofs remain valid
The NOAH circuit is compiled into a Rank-1 Constraint System (R1CS), which is a standard format for representing arithmetic circuits in zero-knowledge proof systems.
An R1CS constraint has the form:
(A · s) ⊙ (B · s) = C · s
Where:
A, B, C: Vectors of coefficients (one per constraint)s: Witness vector containing all variables (private inputs, public inputs, intermediate values)⊙: Element-wise multiplication (Hadamard product)·: Dot product (scalar multiplication)
Constraint Satisfaction:
For a valid witness s, every constraint must satisfy:
(Aᵢ · s) · (Bᵢ · s) = Cᵢ · s (for all constraints i)
The NOAH circuit (defined in circuit/zkkyc.go) implements the following constraints:
1. Age Verification:
actualAge >= minAge
Implemented as: ageValid = Cmp(actualAge, minAge) which returns 1 if actualAge >= minAge, 0 otherwise.
2. Jurisdiction Verification:
actualJurisdiction ∈ {allowedJurisdictions[0], ..., allowedJurisdictions[9]}
Implemented by checking membership in the allowed set:
- For each
i:matchᵢ = (allowedJurisdictions[i] ≠ 0) ∧ (actualJurisdiction = allowedJurisdictions[i]) jurisdictionValid = (match₀ + match₁ + ... + match₉) > 0
3. Hash Verification:
credentialHash = credentialHashPublic
Implemented as: hashValid = IsZero(credentialHash - credentialHashPublic)
4. Accreditation Check:
(requireAccredited = 0) ∨ (actualAccredited = requireAccredited)
Implemented as: accreditationValid = (requireAccredited = 0) + ((requireAccredited ≠ 0) · (actualAccredited = requireAccredited))
5. Final Validity:
isValid = ageValid · jurisdictionValid · hashValid · accreditationValid
All constraints must be satisfied (i.e., isValid = 1) for the proof to be valid.
The witness vector s contains:
- Private inputs:
actualAge,actualJurisdiction,actualAccredited,credentialHash - Public inputs:
minAge,allowedJurisdictions[0..9],requireAccredited,credentialHashPublic,isValid - Intermediate variables: All values computed during constraint evaluation (e.g.,
ageValid,jurisdictionValid, etc.)
Witness Creation Process:
- Assign values to private and public inputs
- Evaluate all circuit constraints to compute intermediate variables
- Construct the full witness vector
s - Verify that all R1CS constraints are satisfied:
(A · s) ⊙ (B · s) = C · s
Once a valid witness is generated:
-
Proving Key (PK) Usage:
- The proving key contains structured reference string (SRS) elements
- Used to compute proof components A, B, C through polynomial evaluations
-
Proof Components:
- A ∈ G1: Commitment to private inputs and randomness
- B ∈ G2: Commitment to private inputs (different group for security)
- C ∈ G1: Correctness term ensuring A and B are consistent
-
Proof Size:
- A: 64 bytes (2 × 32 bytes for G1 point)
- B: 128 bytes (4 × 32 bytes for G2 point)
- C: 64 bytes (2 × 32 bytes for G1 point)
- Total: 256 bytes (uncompressed) or 128 bytes (compressed)
Due to the constraints of working across multiple languages and systems, the protocol uses a 60-bit truncation of credential hashes to ensure compatibility between JavaScript, Go, and Solidity.
Problem:
- Ethereum addresses and hashes are 256 bits (32 bytes)
- Go's
int64type can only represent values from-2⁶³to2⁶³ - 1(signed 64-bit integer) - JavaScript's
Number.MAX_SAFE_INTEGER = 2⁵³ - 1(53 bits for safe integer arithmetic) - Solidity's
uint256can handle full 256-bit values
Solution: Extract the last 60 bits (15 hex characters) from the full 256-bit hash:
truncatedHash = fullHash & 0xFFFFFFFFFFFFFFF
Where 0xFFFFFFFFFFFFFFF is a 60-bit mask (15 hex characters = 60 bits).
Why 60 bits?
- Fits safely in Go's
int64(signed, so we use 60 bits to avoid sign issues) - Fits in JavaScript's safe integer range (53 bits, but we use 60 for consistency)
- Provides sufficient entropy:
2⁶⁰ ≈ 1.15 × 10¹⁸possible values - Collision probability remains negligible for practical purposes
JavaScript (from backend/src/utils/proof-generator.js):
const fullHash = BigInt(credentialHash);
const mask = BigInt('0xFFFFFFFFFFFFFFF'); // 60 bits
const truncatedHash = fullHash & mask;Go (circuit expects int64):
// Hash is already truncated when passed to circuit
credentialHash: int64(truncatedHashValue)Solidity (from src/ProtocolAccessControl.sol):
uint256 fullHash = uint256(credentialHash);
uint256 truncatedHash = fullHash & 0xFFFFFFFFFFFFFFF; // Mask to get last 60 bits
require(publicSignals[12] == truncatedHash, "Credential hash mismatch");Critical Considerations:
- Always extract from original hash: The truncation must be performed on the original 256-bit hash value, not on an already-truncated intermediate value
- Use BigInt for large values: JavaScript must use
BigIntto avoid precision loss when working with values exceedingNumber.MAX_SAFE_INTEGER - Consistent masking: All three languages use the same mask
0xFFFFFFFFFFFFFFFto ensure identical truncation - Comparison in contract: The Solidity contract extracts the truncated portion from the full hash and compares it with the public signal
Hash Verification Flow:
- User computes full credential hash:
keccak256(credential_data)→ 256 bits - Truncate to 60 bits:
hash & 0xFFFFFFFFFFFFFFF→ 60 bits - Use truncated hash in circuit (as
int64) - Contract receives full hash and public signal with truncated hash
- Contract extracts truncated portion:
fullHash & 0xFFFFFFFFFFFFFFF - Contract compares:
extractedTruncated == publicSignals[12]
This approach ensures that:
- The circuit can work with
int64values - The contract can verify against the full hash
- All languages maintain precision throughout the process
graph TB
subgraph OffChain["Off-Chain Layer"]
User[User Wallet]
Issuer[KYC Issuer]
Frontend[React Frontend]
Gateway[API Gateway]
UserService[User Service]
IssuerService[Issuer Service]
ProtocolService[Protocol Service]
ProofService[Proof Service]
Circuit[Go Circuit]
ProofGen[Proof Generator]
end
subgraph OnChain["On-Chain Layer"]
Registry[CredentialRegistry]
AccessControl[ProtocolAccessControl]
Verifier[ZKVerifier]
end
User --> Frontend
Frontend --> Gateway
Gateway --> UserService
Gateway --> IssuerService
Gateway --> ProtocolService
Gateway --> ProofService
Issuer --> IssuerService
IssuerService --> Registry
User --> ProofService
ProofService --> Circuit
Circuit --> ProofGen
ProofGen --> ProofService
ProofService --> User
User --> AccessControl
AccessControl --> Registry
AccessControl --> Verifier
AccessControl --> User
ProtocolService --> AccessControl
UserService --> AccessControl
graph LR
subgraph Contracts["Smart Contracts"]
Registry[CredentialRegistry]
AccessControl[ProtocolAccessControl]
Verifier[ZKVerifier]
end
subgraph RegistryState["Registry State"]
Credentials[credentials mapping]
Issuers[credentialIssuers mapping]
Revoked[revokedCredentials mapping]
Trusted[trustedIssuers mapping]
end
subgraph AccessState["Access Control State"]
Requirements[protocolRequirements mapping]
Access[hasAccess mapping]
UserCreds[userCredentials mapping]
end
Registry --> Credentials
Registry --> Issuers
Registry --> Revoked
Registry --> Trusted
AccessControl --> Requirements
AccessControl --> Access
AccessControl --> UserCreds
AccessControl --> Registry
AccessControl --> Verifier
Registry -.-> AccessControl
AccessControl -.-> External[External Listeners]
graph TB
subgraph FrontendLayer["Frontend Layer"]
UserDash[User Dashboard]
IssuerDash[Issuer Dashboard]
ProtocolDash[Protocol Dashboard]
end
subgraph GatewayLayer["API Gateway Layer"]
Gateway[API Gateway]
end
subgraph BackendServices["Backend Services"]
UserSvc[User Service]
IssuerSvc[Issuer Service]
ProtocolSvc[Protocol Service]
ProofSvc[Proof Service]
end
subgraph ProofGen["Proof Generation"]
Circuit[Go Circuit]
WitnessGen[Witness Generator]
Prover[Groth16 Prover]
ProvingKey[Proving Key]
end
subgraph DataLayer["Data Layer"]
DB[(Database)]
EventListener[Event Listener]
end
subgraph Blockchain["Blockchain Interaction"]
Web3[Web3 Provider]
Contracts[Contract Clients]
end
UserDash --> Gateway
IssuerDash --> Gateway
ProtocolDash --> Gateway
Gateway --> UserSvc
Gateway --> IssuerSvc
Gateway --> ProtocolSvc
Gateway --> ProofSvc
ProofSvc --> Circuit
Circuit --> WitnessGen
WitnessGen --> Prover
Prover --> ProvingKey
UserSvc --> DB
IssuerSvc --> DB
ProtocolSvc --> DB
ProofSvc --> DB
UserSvc --> Web3
IssuerSvc --> Web3
ProtocolSvc --> Web3
Web3 --> Contracts
Contracts --> Registry[CredentialRegistry]
Contracts --> AccessControl[ProtocolAccessControl]
EventListener --> Web3
EventListener --> DB
sequenceDiagram
participant Issuer
participant IssuerSvc as Issuer Service
participant User
participant Registry as CredentialRegistry
participant DB as Database
Issuer->>User: 1. Perform KYC (Off-Chain)
Note over Issuer,User: Age, Jurisdiction,<br/>Accreditation Status
User->>User: 2. Compute Credential Hash<br/>keccak256(credential_data)
Issuer->>IssuerSvc: 3. POST /credential/register<br/>{hash, userAddress}
IssuerSvc->>Registry: 4. registerCredential(hash, user)
Registry->>Registry: 5. Validate Issuer<br/>Check trustedIssuers
Registry->>Registry: 6. Store Credential<br/>credentials[hash] = true<br/>credentialIssuers[hash] = issuer
Registry-->>IssuerSvc: 7. CredentialIssued Event
IssuerSvc->>DB: 8. Store Credential Record
IssuerSvc-->>Issuer: 9. Success Response
sequenceDiagram
participant User
participant Frontend
participant ProofSvc as Proof Service
participant Circuit as Go Circuit
participant AccessControl as ProtocolAccessControl
participant Registry as CredentialRegistry
participant Verifier as ZKVerifier
User->>Frontend: 1. Enter Protocol Address
Frontend->>AccessControl: 2. getRequirements(protocol)
AccessControl-->>Frontend: 3. Requirements<br/>{minAge, jurisdictions, requireAccredited}
User->>Frontend: 4. Request Proof Generation
Frontend->>ProofSvc: 5. POST /proof/generate<br/>{credential, requirements}
ProofSvc->>Circuit: 6. Create Witness<br/>{actualAge, actualJurisdiction,<br/>actualAccredited, credentialHash}
Circuit->>Circuit: 7. Verify Constraints<br/>- Age >= minAge<br/>- Jurisdiction in allowed<br/>- Hash matches<br/>- Accreditation valid
Circuit->>ProofSvc: 8. Proof (A, B, C)<br/>Public Signals [14]
ProofSvc->>ProofSvc: 9. Format for On-Chain<br/>Truncate hash (60 bits)
ProofSvc-->>Frontend: 10. Return Proof + Signals
User->>Frontend: 11. Submit Proof
Frontend->>AccessControl: 12. verifyAndGrantAccess<br/>(proof, signals, hash, user)
AccessControl->>Registry: 13. isCredentialValid(hash)
Registry-->>AccessControl: 14. true/false
AccessControl->>AccessControl: 15. Reconstruct 14-element<br/>Public Inputs Array
AccessControl->>Verifier: 16. verifyProof(A, B, C, publicInputs)
Verifier->>Verifier: 17. Groth16 Pairing Check<br/>e(A, B) = e(α, β) · e(C, δ) · e(L, γ)
Verifier-->>AccessControl: 18. Proof Valid/Invalid
AccessControl->>AccessControl: 19. Validate Requirements Match<br/>- minAge matches<br/>- requireAccredited matches<br/>- hash matches (truncated)
AccessControl->>AccessControl: 20. Grant Access<br/>hasAccess[protocol][user] = true
AccessControl-->>Frontend: 21. AccessGranted Event
Frontend-->>User: 22. Access Confirmed
sequenceDiagram
participant Protocol
participant ProtocolSvc as Protocol Service
participant AccessControl as ProtocolAccessControl
participant User
participant UserSvc as User Service
Protocol->>ProtocolSvc: 1. Set Requirements
ProtocolSvc->>AccessControl: 2. setRequirements<br/>(minAge, jurisdictions, requireAccredited)
AccessControl->>AccessControl: 3. Store Requirements<br/>protocolRequirements[protocol]
AccessControl-->>ProtocolSvc: 4. RequirementsSet Event
Note over User,AccessControl: User generates proof and submits...
User->>UserSvc: 5. Check Access Status
UserSvc->>AccessControl: 6. checkAccess(user)
AccessControl->>AccessControl: 7. Query hasAccess[protocol][user]
AccessControl-->>UserSvc: 8. true/false
UserSvc-->>User: 9. Access Status
alt Revoke Access
Protocol->>AccessControl: 10. revokeAccess(user)
AccessControl->>AccessControl: 11. hasAccess[protocol][user] = false
AccessControl-->>Protocol: 12. AccessRevoked Event
end
stateDiagram-v2
[*] --> NotIssued: Initial State
NotIssued --> Issued: Issuer registers credential<br/>registerCredential(hash, user)
Issued --> Active: Credential validated<br/>isCredentialValid() = true
Active --> Revoked: Issuer/Owner revokes<br/>revokeCredential(hash)
Active --> AccessGranted: User proves eligibility<br/>verifyAndGrantAccess()
AccessGranted --> AccessRevoked: Protocol revokes access<br/>revokeAccess(user)
AccessGranted --> Revoked: Credential revoked<br/>(cascading effect)
Revoked --> [*]: Credential invalid<br/>All access denied
AccessRevoked --> [*]: User must re-verify<br/>with valid credential
note right of Issued
Credential hash stored
Issuer address recorded
CredentialIssued event emitted
end note
note right of Active
Credential exists
Not revoked
Can be used for proof generation
end note
note right of Revoked
Credential marked as revoked
Cannot be used for new proofs
Existing access may be revoked
end note
note right of AccessGranted
User has access to protocol
Proof verified successfully
AccessGranted event emitted
end note
On-chain registry that manages credential hashes and trusted issuers.
Key Functions:
registerCredential(bytes32 hash, address user): Register a credential hash (issuer-only)revokeCredential(bytes32 hash): Revoke a credential (issuer or owner)isCredentialValid(bytes32 hash): Check if credential exists and is not revokedaddIssuer(address issuer, string name): Add trusted issuer (owner-only)removeIssuer(address issuer): Remove issuer (owner-only)
State:
credentials: Maps credential hash → existscredentialIssuers: Maps credential hash → issuer addressrevokedCredentials: Maps credential hash → revoked statustrustedIssuers: Maps issuer address → trusted status
Manages protocol-specific requirements and access control.
Key Functions:
setRequirements(uint256 minAge, uint256[] jurisdictions, bool requireAccredited): Set protocol requirementsverifyAndGrantAccess(...): Verify ZK proof and grant accesscheckAccess(address user): Check if user has accessrevokeAccess(address user): Revoke user's access
Requirements Structure:
struct Requirements {
uint256 minAge; // Minimum age required
uint256[] allowedJurisdictions; // Allowed jurisdiction hashes (max 10)
bool requireAccredited; // Require accredited investor status
bool isSet; // Whether requirements are set
}On-chain Groth16 proof verifier generated from the Go circuit.
Key Function:
verifyProof(uint[2] a, uint[2][2] b, uint[2] c, uint[14] publicSignals): Verify ZK proof
Public Signals Order:
[0] = minAge
[1-10] = allowedJurisdictions (10 elements, padded with 0)
[11] = requireAccredited (0 or 1)
[12] = credentialHashPublic
[13] = isValid (expected to be 1)
Go circuit definition using gnark that defines the ZK proof constraints.
Private Inputs (Hidden):
ActualAge: User's actual ageActualJurisdiction: User's actual jurisdictionActualAccredited: User's accreditation statusCredentialHash: Credential hash (private)
Public Inputs (Revealed):
MinAge: Minimum age requirementAllowedJurisdictions: Array of allowed jurisdictionsRequireAccredited: Whether accreditation is requiredCredentialHashPublic: Credential hash (public, for verification)
Circuit Logic:
- Age Check:
actualAge >= minAge - Jurisdiction Check:
actualJurisdiction in allowedJurisdictions - Hash Check:
credentialHash == credentialHashPublic - Accreditation Check: If required, must match; if not required, always valid
Output:
IsValid: 1 if all checks pass, 0 otherwise
┌──────────┐ ┌──────────┐ ┌──────────────────┐
│ Issuer │ │ User │ │CredentialRegistry│
└────┬─────┘ └────┬─────┘ └────────┬─────────┘
│ │ │
│ 1. Perform KYC │ │
│───────────────────>│ │
│ (Off-Chain) │ │
│ │ │
│ 2. Issue Credential│ │
│ (Age, Juris., │ │
│ Accredited) │ │
│───────────────────>│ │
│ │ │
│ 3. Compute Hash │ │
│ hash = keccak256│ │
│ (credential) │ │
│ │ │
│ 4. Register Hash │ │
│────────────────────────────────────────────>│
│ registerCredential(hash, user) │
│ │ │
│ │ 5. CredentialIssued │
│ │<───────────────────────│
│ │ Event │
Steps:
- Issuer performs KYC verification off-chain
- Issuer creates credential with user's data
- User computes credential hash:
keccak256(credential_data) - Issuer calls
CredentialRegistry.registerCredential(hash, user) - Registry emits
CredentialIssuedevent
┌──────────────┐ ┌──────────────────────┐
│ Protocol │ │ProtocolAccessControl │
└──────┬───────┘ └──────────┬───────────┘
│ │
│ 1. setRequirements() │
│───────────────────────────>│
│ minAge: 21 │
│ jurisdictions: [US, EU] │
│ requireAccredited: true │
│ │
│ 2. RequirementsSet Event │
│<───────────────────────────│
│ │
Steps:
- Protocol calls
ProtocolAccessControl.setRequirements(...) - Requirements are stored for that protocol
RequirementsSetevent is emitted
┌──────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ User │ │Proof Generator│ │ProtocolAccessCtrl│ │ ZKVerifier │
└────┬─────┘ └──────┬───────┘ └────────┬─────────┘ └──────┬───────┘
│ │ │ │
│ 1. Get Requirements │ │ │
│──────────────────────────────────────────────>│ │
│ │ │ │
│ 2. Requirements │ │ │
│<────────────────────────────────────────────────│ │
│ │ │ │
│ 3. Generate Proof │ │ │
│─────────────────────>│ │ │
│ Private: age, │ │ │
│ jurisdiction, etc. │ │ │
│ │ │ │
│ 4. Proof + Signals │ │ │
│<─────────────────────│ │ │
│ │ │ │
│ 5. verifyAndGrantAccess│ │ │
│────────────────────────────────────────────────>│ │
│ (proof, signals, │ │ │
│ credentialHash) │ │ │
│ │ │ │
│ │ │ 6. Verify Credential │
│ │ │─────────────────────────>│
│ │ │ CredentialRegistry │
│ │ │ │
│ │ │ 7. Verify Proof │
│ │ │─────────────────────────>│
│ │ │ verifyProof(...) │
│ │ │ │
│ │ │ 8. Proof Valid │
│ │ │<─────────────────────────│
│ │ │ │
│ │ │ 9. Check Requirements │
│ │ │ Match │
│ │ │ │
│ 10. Access Granted │ │ │
│<────────────────────────────────────────────────│ │
│ AccessGranted Event│ │ │
Detailed Steps:
-
User queries protocol requirements
- User calls
getRequirements(protocol)to get requirements
- User calls
-
User generates ZK proof (Off-Chain)
# User runs proof generation tool go run cmd/prove/main.go input.json- Input includes private data (actual age, jurisdiction, etc.)
- Circuit verifies constraints without revealing private data
- Output: proof (a, b, c) and public signals
-
User submits proof to protocol
- User calls
ProtocolAccessControl.verifyAndGrantAccess(...) - Parameters:
a, b, c: Proof componentspublicSignals: Public signals array (13 elements)credentialHash: Credential hash to verifyuser: User address
- User calls
-
Protocol verifies credential
- Checks
CredentialRegistry.isCredentialValid(credentialHash) - Ensures credential exists and is not revoked
- Checks
-
Protocol verifies ZK proof
- Reconstructs full 14-element public inputs array
- Calls
ZKVerifier.verifyProof(...) - Verifies Groth16 proof using pairing checks
-
Protocol validates requirements match
- Checks
publicSignals[0] == req.minAge - Checks
publicSignals[11] == (req.requireAccredited ? 1 : 0) - Checks
publicSignals[12] == uint256(credentialHash) - Jurisdiction check is implicit in proof validity
- Checks
-
Access granted
- Sets
hasAccess[protocol][user] = true - Stores
userCredentials[protocol][user] = credentialHash - Emits
AccessGrantedevent
- Sets
┌──────────┐ ┌──────────────────┐ ┌──────────────────────┐
│ Issuer │ │CredentialRegistry│ │ProtocolAccessControl │
└────┬─────┘ └────────┬─────────┘ └──────────┬───────────┘
│ │ │
│ 1. revokeCredential() │ │
│──────────────────────>│ │
│ (credentialHash) │ │
│ │ │
│ 2. Mark as Revoked │ │
│ revokedCredentials │ │
│ [hash] = true │ │
│ │ │
│ 3. CredentialRevoked │ │
│ Event │ │
│<──────────────────────│ │
│ │ │
│ │ 4. Future verifications │
│ │ will fail │
│ │──────────────────────────────>│
│ │ isCredentialValid() │
│ │ returns false │
│ │<─────────────────────────────│
Steps:
- Issuer (or owner) calls
CredentialRegistry.revokeCredential(hash) - Registry marks credential as revoked
CredentialRevokedevent is emitted- Future access attempts with this credential will fail
The Issuer Dashboard enables KYC issuers to register, manage, and revoke credentials on-chain. This guide walks through the complete workflow for credential lifecycle management.
As a trusted KYC issuer, you can:
- Register credentials: On-board users by registering their credential hashes on-chain
- View issued credentials: Track all credentials you've issued
- Check credential status: Verify if a credential is valid or revoked
- Revoke credentials: Immediately invalidate credentials when needed (e.g., compliance violations, account closure)
Before using the Issuer Dashboard:
- Trusted Issuer Status: Your wallet address must be registered as a trusted issuer by the protocol owner
- Wallet Connection: Connect your MetaMask wallet with the issuer address
- Network Access: Ensure you're connected to the correct network (local, testnet, or mainnet)
- Open the Issuer Dashboard in your browser
- Click "Connect Wallet" and select MetaMask
- Approve the connection request
- Verify that the connected address matches your registered issuer address
Note: If you see "Not trusted issuer" errors, contact the protocol administrator to add your address as a trusted issuer.
Upon connecting, the dashboard automatically loads all credentials you've issued:
- Credential Hash: The unique 32-byte hash identifier (click to copy)
- User Address: The Ethereum address of the credential owner
- Status: Active (valid) or revoked
- Created Date: Timestamp when the credential was registered
Features:
- Click any credential hash to copy it to clipboard
- Use the "Copy" button for quick hash copying
- View credential details in a clean, organized list
To register a new credential for a user:
-
Compute Credential Hash (Off-Chain):
The credential hash is computed from user data using Keccak256:
// Credential data format const credentialData = `user:${userAddress},age:${age},jurisdiction:${jurisdictionHash},accredited:${accreditedValue},timestamp:${timestamp}`; // Hash computation const jurisdictionHash = ethers.keccak256(ethers.toUtf8Bytes(jurisdiction)); const credentialHash = ethers.keccak256(ethers.toUtf8Bytes(credentialData));
Components:
userAddress: User's Ethereum address (0x...)age: User's age (integer)jurisdiction: Jurisdiction string (e.g., "US", "EU") - converted to hashaccredited: 1 if accredited investor, 0 otherwisetimestamp: Unix timestamp of credential creation
Example:
// User data { userAddress: "0x3a2439dcaad194ae3f7f6ef3f1f15ea526c1dd3a", age: 25, jurisdiction: "US", accredited: 1 } // Results in credential hash: // 0x000000000000000000000000000000000000000000000000000000024cb016ea
-
Enter Credential Hash:
- Paste the computed credential hash into the "Credential Hash" field
- Format:
0xfollowed by 64 hexadecimal characters - Example:
0x000000000000000000000000000000000000000000000000000000024cb016ea - The field validates format in real-time
-
Enter User Address:
- Paste the user's Ethereum address
- Format:
0xfollowed by 40 hexadecimal characters - Example:
0xd5881aa749eefd3cb08d10f051ac776d664d0663 - The field validates format in real-time
-
Register Credential:
- Click "Register Credential" button
- Approve the MetaMask transaction
- Wait for transaction confirmation
- Success message displays transaction hash
On-Chain Process:
- Smart contract validates you're a trusted issuer
- Checks credential doesn't already exist
- Checks credential wasn't previously revoked
- Registers credential hash → user address mapping
- Emits
CredentialIssuedevent - Optionally saves to off-chain database (if configured)
To verify if a credential is valid:
- Enter the credential hash in the "Check Credential Status" section
- Click "Check Status"
- View result:
- Green Alert: "Credential is valid" - credential exists and is not revoked
- Red Alert: "Credential is invalid or revoked" - credential doesn't exist or was revoked
Use Cases:
- Verify credential before user attempts access
- Troubleshoot access issues
- Audit credential status
To revoke a credential (e.g., compliance violation, account closure):
- Enter the credential hash in the "Revoke Credential" section
- Click "Revoke Credential"
- Confirm the revocation in the popup dialog
- Approve the MetaMask transaction
- Wait for transaction confirmation
- Success message displays transaction hash
Important Notes:
- Revocation is immediate and permanent (cannot be undone)
- Revoked credentials cannot be used for future access grants
- Only the issuer who created the credential (or protocol owner) can revoke it
- Revocation emits
CredentialRevokedevent
When to Revoke:
- User violates compliance requirements
- Account closure or suspension
- Credential data is compromised
- Regulatory requirements change
flowchart TD
Start([Start: Access Issuer Dashboard]) --> ConnectWallet[Connect MetaMask Wallet]
ConnectWallet --> CheckIssuer{Is Address<br/>Trusted Issuer?}
CheckIssuer -->|No| Error1[Error: Not Trusted Issuer<br/>Contact Administrator]
CheckIssuer -->|Yes| LoadCredentials[Load Issued Credentials]
LoadCredentials --> Dashboard[Issuer Dashboard Ready]
Dashboard --> ViewCreds[View Issued Credentials]
Dashboard --> RegisterNew[Register New Credential]
Dashboard --> CheckStatus[Check Credential Status]
Dashboard --> RevokeCred[Revoke Credential]
ViewCreds --> DisplayList[Display Credential List<br/>Hash, User, Status, Date]
DisplayList --> CopyHash[Click to Copy Hash]
CopyHash --> Dashboard
RegisterNew --> ComputeHash[Compute Credential Hash<br/>Off-Chain]
ComputeHash --> EnterHash[Enter Credential Hash<br/>32-byte hex]
EnterHash --> EnterUser[Enter User Address<br/>Ethereum address]
EnterUser --> ValidateInput{Validate<br/>Format?}
ValidateInput -->|Invalid| Error2[Error: Invalid Format]
ValidateInput -->|Valid| SubmitRegister[Click Register Credential]
SubmitRegister --> ApproveTx1[Approve MetaMask Transaction]
ApproveTx1 --> WaitTx1[Wait for Confirmation]
WaitTx1 --> OnChainReg[On-Chain Registration<br/>CredentialRegistry.registerCredential]
OnChainReg --> CheckExists{Credential<br/>Exists?}
CheckExists -->|Yes| Error3[Error: Credential Already Exists]
CheckExists -->|No| RegisterSuccess[Success: Credential Registered<br/>Event Emitted]
RegisterSuccess --> RefreshList[Refresh Credential List]
RefreshList --> Dashboard
Error2 --> RegisterNew
Error3 --> RegisterNew
CheckStatus --> EnterCheckHash[Enter Credential Hash]
EnterCheckHash --> QueryStatus[Query isCredentialValid]
QueryStatus --> DisplayStatus{Valid?}
DisplayStatus -->|Yes| StatusValid[Display: Valid]
DisplayStatus -->|No| StatusInvalid[Display: Invalid/Revoked]
StatusValid --> Dashboard
StatusInvalid --> Dashboard
RevokeCred --> EnterRevokeHash[Enter Credential Hash]
EnterRevokeHash --> ConfirmRevoke{Confirm<br/>Revocation?}
ConfirmRevoke -->|Cancel| Dashboard
ConfirmRevoke -->|Yes| SubmitRevoke[Click Revoke Credential]
SubmitRevoke --> ApproveTx2[Approve MetaMask Transaction]
ApproveTx2 --> WaitTx2[Wait for Confirmation]
WaitTx2 --> OnChainRevoke[On-Chain Revocation<br/>CredentialRegistry.revokeCredential]
OnChainRevoke --> CheckAuth{Authorized<br/>to Revoke?}
CheckAuth -->|No| Error4[Error: Not Authorized]
CheckAuth -->|Yes| RevokeSuccess[Success: Credential Revoked<br/>Event Emitted]
RevokeSuccess --> RefreshList2[Refresh Credential List]
RefreshList2 --> Dashboard
Error4 --> RevokeCred
Error1 --> End([End])
style Start fill:#e1f5ff
style End fill:#ffe1f5
style Dashboard fill:#e8f5e9
style RegisterSuccess fill:#c8e6c9
style RevokeSuccess fill:#ffcdd2
style Error1 fill:#ffcdd2
style Error2 fill:#ffcdd2
style Error3 fill:#ffcdd2
style Error4 fill:#ffcdd2
The credential hash is a critical component that links off-chain KYC data to on-chain registration. Here's how it's computed:
// Step 1: Convert jurisdiction string to hash
jurisdictionHash = keccak256(utf8Bytes(jurisdiction))
// Step 2: Create credential data string
credentialData = `user:${userAddress},age:${age},jurisdiction:${jurisdictionHash},accredited:${accreditedValue},timestamp:${timestamp}`
// Step 3: Hash the credential data
credentialHash = keccak256(utf8Bytes(credentialData))See generate-credential-hash.js for a complete implementation:
function generateCredentialHash(userData) {
const { userAddress, age, jurisdiction, accredited } = userData;
// Convert jurisdiction string to hash
const jurisdictionHash = ethers.keccak256(ethers.toUtf8Bytes(jurisdiction));
// Create credential data string
const timestamp = Date.now();
const accreditedValue = accredited ? 1 : 0;
const credentialData = `user:${userAddress},age:${age},jurisdiction:${jurisdictionHash},accredited:${accreditedValue},timestamp:${timestamp}`;
// Hash the credential data
const credentialHash = ethers.keccak256(ethers.toUtf8Bytes(credentialData));
return { credentialHash, jurisdictionHash, credentialData, timestamp };
}- Deterministic: Same input data always produces the same hash
- Unique: Different credentials should have different hashes (use unique timestamps)
- Privacy: Only the hash is stored on-chain; actual data remains off-chain
- Verification: Users must use the same hash when generating ZK proofs
- ✅ After completing full KYC verification
- ✅ When user meets all compliance requirements
- ✅ After verifying identity documents
- ✅ When user explicitly requests credential issuance
- ✅ User violates compliance requirements
- ✅ Account closure or suspension
- ✅ Credential data is compromised or incorrect
- ✅ Regulatory requirements change
- ✅ User requests credential revocation
- Private Key Management: Store issuer private keys securely (hardware wallet recommended)
- Access Control: Limit dashboard access to authorized personnel only
- Audit Trail: Maintain off-chain logs of all credential operations
- Validation: Always validate credential hash format before registration
- Double-Check: Verify user address before registering credentials
Common errors and solutions:
| Error | Cause | Solution |
|---|---|---|
| "Not trusted issuer" | Address not registered as issuer | Contact protocol administrator |
| "Credential already exists" | Hash already registered | Use different credential hash or check existing credentials |
| "Invalid format" | Hash/address format incorrect | Ensure 0x prefix and correct length (64 chars for hash, 40 for address) |
| "Not authorized to revoke" | Not the issuer who created credential | Only original issuer or owner can revoke |
The Issuer Dashboard uses the following backend endpoints:
-
Register Credential:
POST /credential/register- Body:
{ credentialHash, userAddress, age?, jurisdiction?, accredited? } - Returns:
{ success, transactionHash, credentialHash, userAddress }
- Body:
-
Revoke Credential:
POST /credential/revoke- Body:
{ credentialHash } - Returns:
{ success, transactionHash, credentialHash }
- Body:
-
Check Credential:
GET /credential/check/:hash- Returns:
{ credentialHash, isValid }
- Returns:
-
Get Issuer Credentials:
GET /credentials/:issuer- Returns:
Array<{ credential_hash, user_address, status, created_at }>
- Returns:
See backend/src/issuer/server.js for implementation details.
This guide walks through the complete Protocol Dashboard flow for DeFi protocols to set and manage their KYC requirements.
The Protocol Dashboard enables DeFi protocols to:
- Set minimum age requirements
- Specify allowed jurisdictions (up to 10)
- Require accredited investor status
- View current requirements
- Monitor access grants
- Open the Protocol Dashboard in the frontend application
- Connect your MetaMask wallet (or compatible Web3 wallet)
- Ensure you're connected with the protocol's wallet address
- The protocol address is automatically detected from your connected wallet
- Alternatively, the default
ProtocolAccessControlcontract address is used
UI Display:
- Connected wallet address is shown at the top
- Protocol address is displayed below the connection status
Upon connecting, the dashboard automatically fetches and displays current requirements:
- Minimum Age: The current minimum age requirement
- Require Accredited: Whether accredited investor status is required (Yes/No)
- Jurisdictions Count: Number of allowed jurisdictions
API Call:
GET /api/v1/protocol/requirements/{protocolAddress}Response:
{
"protocol": "0x...",
"minAge": "21",
"allowedJurisdictions": ["1234567890", "1111111111"],
"requireAccredited": true,
"isSet": true
}If no requirements are set, isSet will be false and the current requirements card will not be displayed.
-
Enter Minimum Age
- Input a numeric value in the "Minimum Age" field
- This represents the minimum age users must be to access your protocol
- Example:
21for age-restricted protocols
-
Enter Allowed Jurisdictions
- Input jurisdiction codes as a comma-separated list
- Supported formats:
- Jurisdiction codes:
US, UK, CA, DE, FR(automatically converted to hashes) - Hash numbers:
1234567890, 1111111111(used directly)
- Jurisdiction codes:
- Maximum 10 jurisdictions allowed
- Examples:
US, UK, CA→ Converted to jurisdiction hashes1234567890, 1111111111→ Used as-is (if valid hashes)
-
Set Accredited Investor Requirement
- Check the "Require Accredited Investor Status" checkbox if your protocol requires accredited investor status
- Leave unchecked if accreditation is not required
Jurisdiction Handling:
- String jurisdiction codes (e.g., "US", "UK") are automatically converted to hash values using
keccak256 - The conversion happens client-side before submission
- Large hash values are converted to hex format for backend compatibility
- The backend accepts both numeric and hex string formats
- Click the "Set Requirements" button
- Confirm the transaction in MetaMask
- Wait for transaction confirmation
Backend Processing:
POST /api/v1/protocol/requirements/set
{
"protocolAddress": "0x...",
"minAge": 21,
"allowedJurisdictions": [1234567890, 1111111111],
"requireAccredited": true
}On-Chain Transaction:
- Calls
ProtocolAccessControl.setRequirements(...) - Validates jurisdiction count (max 10)
- Stores requirements in
protocolRequirements[protocolAddress] - Emits
RequirementsSetevent
Success Response:
{
"success": true,
"transactionHash": "0x...",
"protocolAddress": "0x...",
"requirements": {
"minAge": 21,
"allowedJurisdictions": [1234567890, 1111111111],
"requireAccredited": true
}
}After successful submission:
- A success message displays with the transaction hash
- The current requirements card automatically refreshes
- New requirements are immediately visible
Error Handling:
- Validation Errors: Displayed if input validation fails
- Missing minimum age
- Invalid jurisdiction format
- Too many jurisdictions (>10)
- Transaction Errors: Displayed if on-chain transaction fails
- Insufficient gas
- Contract revert
- Network issues
flowchart TD
Start([Protocol Dashboard]) --> ConnectWallet{Wallet Connected?}
ConnectWallet -->|No| ShowConnectMessage[Show: Connect MetaMask Wallet]
ConnectWallet -->|Yes| GetRequirements[Fetch Current Requirements]
GetRequirements --> DisplayCurrent[Display Current Requirements]
DisplayCurrent --> ShowForm[Show Requirements Form]
ShowForm --> UserInput[User Inputs Requirements]
UserInput --> EnterMinAge[Enter Minimum Age]
EnterMinAge --> EnterJurisdictions[Enter Jurisdictions<br/>Comma-separated]
EnterJurisdictions --> ParseJurisdictions{Parse Jurisdictions}
ParseJurisdictions -->|String Codes| ConvertToHash[Convert to Hashes<br/>keccak256]
ParseJurisdictions -->|Hash Numbers| UseDirectly[Use Hash Values]
ConvertToHash --> ValidateInputs[Validate Inputs]
UseDirectly --> ValidateInputs
ValidateInputs --> CheckMinAge{Min Age Valid?}
CheckMinAge -->|No| ShowError1[Error: Missing Min Age]
CheckMinAge -->|Yes| CheckJurisdictions{Jurisdictions Valid?}
CheckJurisdictions -->|No| ShowError2[Error: Invalid Jurisdictions]
CheckJurisdictions -->|Yes| CheckCount{Count <= 10?}
CheckCount -->|No| ShowError3[Error: Too Many Jurisdictions]
CheckCount -->|Yes| PrepareRequest[Prepare API Request]
PrepareRequest --> SubmitTransaction[Submit Transaction]
SubmitTransaction --> WaitConfirmation[Wait for Confirmation]
WaitConfirmation --> CheckResult{Transaction Success?}
CheckResult -->|No| ShowTxError[Show Transaction Error]
CheckResult -->|Yes| UpdateUI[Update UI with Success]
UpdateUI --> RefreshRequirements[Refresh Requirements Display]
RefreshRequirements --> End([Requirements Set Successfully])
ShowError1 --> ShowForm
ShowError2 --> ShowForm
ShowError3 --> ShowForm
ShowTxError --> ShowForm
ShowConnectMessage --> End
style Start fill:#e1f5ff
style End fill:#c8e6c9
style ShowError1 fill:#ffcdd2
style ShowError2 fill:#ffcdd2
style ShowError3 fill:#ffcdd2
style ShowTxError fill:#ffcdd2
style UpdateUI fill:#c8e6c9
The system enforces the following constraints:
-
Minimum Age
- Must be a positive integer
- No maximum limit (protocol-specific)
- Required field
-
Allowed Jurisdictions
- Maximum 10 jurisdictions per protocol
- Can be empty (no jurisdiction restriction)
- Each jurisdiction must be:
- A valid jurisdiction code string (e.g., "US", "UK"), OR
- A valid hash number (uint256)
-
Accredited Investor Requirement
- Boolean value (true/false)
- Optional (defaults to false)
Contract: src/ProtocolAccessControl.sol
Function Signature:
function setRequirements(
uint256 minAge,
uint256[] memory allowedJurisdictions,
bool requireAccredited
) externalStorage:
mapping(address => Requirements) public protocolRequirements;
struct Requirements {
uint256 minAge;
uint256[] allowedJurisdictions;
bool requireAccredited;
bool isSet;
}Events:
event RequirementsSet(
address indexed protocol,
uint256 minAge,
uint256[] allowedJurisdictions,
bool requireAccredited
);-
Jurisdiction Selection
- Use standard jurisdiction codes (ISO 3166-1 alpha-2) when possible
- Test jurisdiction conversion before production deployment
- Keep jurisdiction list manageable (fewer jurisdictions = lower gas costs)
-
Age Requirements
- Set age requirements based on regulatory requirements
- Consider different requirements for different protocol features
-
Accredited Investor Status
- Only require accreditation if legally necessary
- Document why accreditation is required for compliance
-
Monitoring
- Monitor
RequirementsSetevents for requirement changes - Track access grants to understand user eligibility
- Review requirements periodically for regulatory compliance
- Monitor
Issue: "Invalid protocol address"
- Solution: Ensure your wallet is connected and the address is valid (42 characters, starts with 0x)
Issue: "Too many jurisdictions"
- Solution: Reduce the number of jurisdictions to 10 or fewer
Issue: "Transaction failed"
- Solution:
- Check you have sufficient ETH for gas
- Verify the contract is deployed and accessible
- Check network connectivity
Issue: "Jurisdiction conversion error"
- Solution:
- Use valid jurisdiction codes (e.g., "US", "UK", "CA")
- Or use numeric hash values directly
- Check console logs for conversion details
This section provides a comprehensive guide for users to access DeFi protocols using the NOAH system. The User Dashboard enables you to prove compliance with protocol requirements while maintaining complete privacy of your personal information.
The User Dashboard is a web interface that allows you to:
- View your registered credentials
- Check protocol requirements
- Generate zero-knowledge proofs
- Verify proofs and gain access to DeFi protocols
- Check your current access status
Action:
- Open the User Dashboard in your web browser
- Click "Connect Wallet" to connect your MetaMask wallet
- Approve the connection request in MetaMask
What Happens:
- The dashboard detects your connected wallet address
- Your credentials are automatically fetched from the backend API
- The "My Credentials" section displays all credentials registered for your address
UI Components:
- Wallet Connection Status: Shows your connected wallet address (e.g.,
0x1234...5678) - My Credentials Card: Lists all your credentials with:
- Credential hash (clickable to select)
- Status indicator (active/revoked)
- Issuer address
- Copy button for credential hash
Expected Result:
- Wallet connected successfully
- Credentials list displayed (or message if no credentials found)
Error Handling:
- If wallet connection fails, an error message is displayed
- If no credentials are found, a message prompts you to contact an issuer
Action:
- In the "Check Protocol Requirements" section, enter the protocol's contract address
- The address must be a valid Ethereum address (42 characters, starting with
0x)
What Happens:
- The dashboard validates the address format
- Once a valid address is entered, the system automatically queries protocol requirements
- Requirements are fetched from the
ProtocolAccessControlcontract
UI Components:
- Protocol Address Input Field: Text field with validation
- Check Requirements Button: Enabled when address is valid
Expected Result:
- Protocol requirements are displayed:
- Minimum age required
- Whether accredited investor status is required
- Number of allowed jurisdictions
Error Handling:
- Invalid address format shows validation error
- If protocol has no requirements set, an appropriate message is displayed
Action: Requirements are automatically displayed after entering a valid protocol address.
What You'll See:
- Min Age: The minimum age requirement (e.g., 21)
- Require Accredited: Whether accredited investor status is required (Yes/No)
- Allowed Jurisdictions: The number of allowed jurisdictions
Understanding Requirements:
- These requirements define what you need to prove to gain access
- Your credential must satisfy all requirements:
- Your age must be >= minimum age
- Your jurisdiction must be in the allowed list
- If accredited status is required, your credential must indicate accredited status
Action:
- Select a credential from the "My Credentials" list (click on it), OR
- Manually enter your credential hash in the "Generate ZK Proof" section
- Ensure the protocol address is entered and requirements are loaded
- Click "Generate Proof" button
What Happens:
-
Credential Validation:
- System checks if credential exists in the database
- If not found, attempts to fetch from backend API
- Validates credential hash format (64 hex characters after
0x)
-
Manual Entry Fallback:
- If credential exists on-chain but data is missing from database, a manual entry form appears
- You must enter:
- Age (number)
- Jurisdiction (code like "US", "UK", or hash number)
- Accredited status (1 for Yes, 0 for No)
-
Proof Generation:
- Backend generates a Groth16 zero-knowledge proof
- Proof demonstrates you meet requirements without revealing:
- Your actual age
- Your actual jurisdiction
- Your actual accredited status
- Only the requirements and credential hash are revealed
UI Components:
- Credential Hash Input: Text field for manual entry
- Copy Hash Button: Copies hash to clipboard
- Verify Hash Button: Checks if credential is valid on-chain
- Manual Entry Form (shown when needed):
- Age input field
- Jurisdiction input field
- Accredited status input field
- Generate Proof Button: Initiates proof generation
Expected Result:
- Proof generated successfully
- Success message displayed
- "Verify Proof & Grant Access" button appears
Error Handling:
- Credential Not Found: Error message with instructions to contact issuer
- Credential Invalid/Revoked: Error message indicating credential status
- Missing Data: Manual entry form appears automatically
- Invalid Format: Validation errors for hash format or jurisdiction code
- Proof Generation Failure: Error message with details
Technical Details:
- Proof generation uses the Go circuit (
circuit/zkkyc.go) - Private inputs (hidden): actual age, jurisdiction, accredited status, credential hash
- Public inputs (revealed): minimum age, allowed jurisdictions, require accredited flag, credential hash (for verification)
- Proof consists of three components:
a,b,c(Groth16 format)
Action:
- After proof generation succeeds, click "Verify Proof & Grant Access" button
- Approve the transaction in MetaMask when prompted
- Wait for transaction confirmation
What Happens:
-
On-Chain Verification:
- Transaction is sent to
ProtocolAccessControl.verifyAndGrantAccess() - Contract performs multiple checks:
- Verifies credential exists and is not revoked in
CredentialRegistry - Verifies the ZK proof using
ZKVerifier.verifyProof() - Validates that proof's public signals match protocol requirements
- Checks that credential hash in proof matches the provided hash
- Verifies credential exists and is not revoked in
- Transaction is sent to
-
Access Granting:
- If all checks pass, access is granted
- Your address is marked as having access to the protocol
AccessGrantedevent is emitted on-chain
UI Components:
- Verify Proof Button: Appears after successful proof generation
- Loading Indicator: Shows during transaction processing
- Success/Error Messages: Display transaction result
Expected Result:
- Transaction confirmed on-chain
- Success message with transaction hash
- Access status updated to "You have access to this protocol"
Error Handling:
- Transaction Rejected: User rejected transaction in MetaMask
- Proof Verification Failed: Proof doesn't verify (wrong requirements, invalid proof, etc.)
- Credential Invalid: Credential doesn't exist or is revoked
- Requirements Mismatch: Proof's public signals don't match protocol requirements
- Network Error: Connection issues or RPC errors
Gas Costs:
- Proof verification typically costs ~200,000-300,000 gas
- Actual cost depends on network conditions
Action:
- Enter a protocol address in the "Access Status" section
- Access status is automatically checked
What Happens:
- System queries
ProtocolAccessControl.checkAccess(user, protocol) - Returns whether you currently have access
UI Components:
- Access Status Card: Displays current access status
- Status Indicator: Green (has access) or Yellow (no access)
Expected Result:
- Clear indication of access status
- "You have access to this protocol" (green) or "You do not have access" (yellow)
flowchart TD
Start([User Opens Dashboard]) --> ConnectWallet{Wallet<br/>Connected?}
ConnectWallet -->|No| ShowConnect[Show Connect Wallet Prompt]
ShowConnect --> WaitConnect[Wait for User to Connect]
WaitConnect --> ConnectWallet
ConnectWallet -->|Yes| LoadCredentials[Load User Credentials]
LoadCredentials --> DisplayCredentials{Credentials<br/>Found?}
DisplayCredentials -->|No| ShowNoCreds[Show: No Credentials Found<br/>Contact Issuer]
DisplayCredentials -->|Yes| ShowCredsList[Display Credentials List]
ShowCredsList --> EnterProtocol[User Enters Protocol Address]
EnterProtocol --> ValidateAddress{Address<br/>Valid?}
ValidateAddress -->|No| ShowAddressError[Show Address Format Error]
ShowAddressError --> EnterProtocol
ValidateAddress -->|Yes| FetchRequirements[Fetch Protocol Requirements]
FetchRequirements --> DisplayRequirements[Display Requirements:<br/>Min Age, Jurisdictions,<br/>Accredited Status]
DisplayRequirements --> SelectCredential[User Selects Credential<br/>or Enters Hash]
SelectCredential --> ValidateHash{Credential Hash<br/>Valid?}
ValidateHash -->|No| ShowHashError[Show Hash Format Error]
ShowHashError --> SelectCredential
ValidateHash -->|Yes| CheckCredential[Check Credential Exists]
CheckCredential --> CredExists{Credential<br/>Exists?}
CredExists -->|No| ShowCredError[Show: Credential Not Found<br/>Contact Issuer]
CredExists -->|Yes| CheckDataComplete{Data<br/>Complete?}
CheckDataComplete -->|No| ShowManualEntry[Show Manual Entry Form:<br/>Age, Jurisdiction, Accredited]
ShowManualEntry --> UserFillsData[User Fills Manual Data]
UserFillsData --> ValidateManualData{Manual Data<br/>Valid?}
ValidateManualData -->|No| ShowManualError[Show Validation Error]
ShowManualError --> UserFillsData
ValidateManualData -->|Yes| GenerateProof
CheckDataComplete -->|Yes| GenerateProof[Generate ZK Proof<br/>Backend API Call]
GenerateProof --> ProofSuccess{Proof<br/>Generated?}
ProofSuccess -->|No| ShowProofError[Show Proof Generation Error]
ShowProofError --> SelectCredential
ProofSuccess -->|Yes| DisplayProofSuccess[Show: Proof Generated Successfully<br/>Display Verify Button]
DisplayProofSuccess --> UserClicksVerify[User Clicks<br/>Verify & Grant Access]
UserClicksVerify --> MetaMaskPrompt[Show MetaMask Transaction Prompt]
MetaMaskPrompt --> UserApproves{User<br/>Approves?}
UserApproves -->|No| ShowRejected[Show: Transaction Rejected]
UserApproves -->|Yes| SendTransaction[Send Transaction to Blockchain]
SendTransaction --> WaitConfirm[Wait for Transaction Confirmation]
WaitConfirm --> VerifyOnChain[On-Chain Verification:<br/>1. Check Credential Valid<br/>2. Verify ZK Proof<br/>3. Check Requirements Match]
VerifyOnChain --> VerificationSuccess{Verification<br/>Successful?}
VerificationSuccess -->|No| ShowVerifyError[Show Verification Error:<br/>Proof Invalid, Credential Revoked,<br/>or Requirements Mismatch]
ShowVerifyError --> SelectCredential
VerificationSuccess -->|Yes| GrantAccess[Grant Access On-Chain<br/>Emit AccessGranted Event]
GrantAccess --> ShowSuccess[Show Success Message<br/>with Transaction Hash]
ShowSuccess --> CheckAccessStatus[User Can Check Access Status]
CheckAccessStatus --> End([Access Granted!])
style Start fill:#e1f5ff
style End fill:#c8e6c9
style ShowConnect fill:#fff3cd
style ShowNoCreds fill:#fff3cd
style ShowCredError fill:#ffcdd2
style ShowProofError fill:#ffcdd2
style ShowVerifyError fill:#ffcdd2
style ShowSuccess fill:#c8e6c9
style GrantAccess fill:#c8e6c9
Cause: Wallet not connected
Solution: Click "Connect Wallet" button and approve the connection in MetaMask
Cause: No credentials have been registered for your wallet address
Solution: Contact an issuer to register a credential for your address using the Issuer Dashboard
Cause: Credential hash doesn't match required format
Solution: Ensure hash is 64 hex characters after 0x (e.g., 0x1234...abcd)
Cause: Credential hash doesn't exist in the CredentialRegistry contract
Solution: Verify the hash is correct, or contact the issuer to register it
Cause: Credential exists on-chain but data is missing from database
Solution: Use the manual entry form to enter your credential data (age, jurisdiction, accredited status)
Cause: Credential exists but missing required fields
Solution: Fill in the manual entry form with the missing data
Cause: Backend proof generation error (circuit constraints not satisfied, invalid inputs, etc.)
Solution:
- Verify your credential data is correct
- Ensure you meet the protocol requirements
- Check that jurisdiction code is valid (if using string format)
Cause: User rejected transaction in MetaMask
Solution: Approve the transaction when prompted, or check MetaMask settings
Cause: Proof doesn't verify on-chain (invalid proof, wrong requirements, etc.)
Solution:
- Regenerate the proof with correct requirements
- Ensure credential is not revoked
- Verify credential hash matches
Cause: Proof's public signals don't match protocol requirements
Solution: Regenerate proof after ensuring protocol requirements are correctly loaded
-
Keep Your Credential Hash Safe: Store your credential hash securely. You'll need it to generate proofs.
-
Verify Credential Status: Before generating proofs, use the "Verify Hash" button to ensure your credential is still valid and not revoked.
-
Check Requirements First: Always check protocol requirements before generating a proof to ensure you meet them.
-
Manual Entry Accuracy: When using manual entry, ensure all data is accurate. Incorrect data will result in proof generation failure.
-
Transaction Confirmation: Wait for transaction confirmation before assuming access is granted. Check the access status after verification.
-
Multiple Credentials: If you have multiple credentials, select the one that best matches the protocol requirements.
-
Network Considerations: Be aware of gas costs and network congestion when verifying proofs on-chain.
- Frontend Component:
frontend/src/pages/UserDashboard.jsx - API Endpoints:
GET /user/credentials/:address- Get user credentialsGET /protocol/:address/requirements- Get protocol requirementsPOST /proof/generate- Generate ZK proofPOST /protocol/verify-access- Verify proof and grant accessGET /user/access/:protocolAddress/:userAddress- Check access status
- Smart Contracts:
ProtocolAccessControl.verifyAndGrantAccess()- Main verification functionCredentialRegistry.isCredentialValid()- Check credential validityZKVerifier.verifyProof()- Verify Groth16 proof
This section provides a comprehensive guide for KYC issuers to manage credentials using the NOAH system. The Issuer Dashboard enables you to register, view, and revoke credentials for users while maintaining proper credential lifecycle management.
The Issuer Dashboard is a web interface that allows trusted KYC issuers to:
- View all credentials issued by your address
- Register new credential hashes on-chain
- Revoke credentials when needed
- Check credential validity status
- Manage credential data in the database
Action:
- Open the Issuer Dashboard in your web browser
- Click "Connect Wallet" to connect your MetaMask wallet
- Approve the connection request in MetaMask
What Happens:
- The dashboard detects your connected wallet address
- The system verifies that your address is registered as a trusted issuer in the
CredentialRegistrycontract - Your issued credentials are automatically fetched from the backend API
UI Components:
- Wallet Connection Status: Shows your connected wallet address (e.g.,
0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb) - Connection Prompt: If wallet is not connected, displays "Please connect your MetaMask wallet to continue"
Expected Result:
- Wallet connected successfully
- Issuer address displayed at the top of the dashboard
Error Handling:
- If wallet connection fails, an error message is displayed
- If your address is not registered as a trusted issuer, credential registration will fail with "Not trusted issuer" error
Prerequisites:
- Your address must be added as a trusted issuer by the
CredentialRegistryowner - This is done via
CredentialRegistry.addIssuer(issuerAddress, "Issuer Name")
Action: Upon connecting your wallet, the dashboard automatically loads and displays all credentials you have issued.
What You'll See:
- My Issued Credentials Card: Lists all credentials registered by your issuer address
- Each credential displays:
- Credential Hash: Full 32-byte hash (clickable to copy)
- Status: Active or revoked status indicator
- User Address: The address of the credential holder (truncated display)
- Created Date: When the credential was registered
- Copy Button: Quick copy functionality for the credential hash
UI Components:
- Credentials List: Scrollable list of all your credentials
- Status Chips: Color-coded status indicators (green for active, gray for revoked)
- Empty State: If no credentials found, displays "No credentials found. Register a credential to get started."
API Call:
GET /api/v1/issuer/credentials/{issuerAddress}Response:
[
{
"credential_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"user_address": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"issuer_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"age": 25,
"jurisdiction": "1234567890",
"accredited": 1,
"created_at": "2024-01-01T00:00:00.000Z"
}
]Expected Result:
- List of all your issued credentials displayed
- Each credential is clickable to copy its hash
- Status indicators show current credential state
Action:
- In the "Register Credential" section, enter the credential hash
- Enter the user address (the address of the credential holder)
- Optionally, you can include credential data (age, jurisdiction, accredited status) for database storage
- Click "Register Credential" button
What Happens:
-
Input Validation:
- Credential hash must be a valid 32-byte hash (0x followed by 64 hex characters)
- User address must be a valid Ethereum address (0x followed by 40 hex characters)
- Format validation happens client-side before submission
-
On-Chain Registration:
- Backend calls
CredentialRegistry.registerCredential(hash, user)on-chain - Contract validates that you are a trusted issuer
- Contract checks that credential doesn't already exist
- Contract checks that credential wasn't previously revoked
- If valid, credential is registered:
credentials[hash] = truecredentialIssuers[hash] = issuerAddressCredentialIssuedevent is emitted
- Backend calls
-
Database Storage (Optional):
- If credential data (age, jurisdiction, accredited) is provided, it's stored in the database
- This enables faster proof generation for users
- If data is not provided, credential still works but users may need manual entry
UI Components:
- Credential Hash Input Field: Text field with format validation
- Placeholder:
0x000000000000000000000000000000000000000000000000000000024cb016ea - Helper text: "Must be a 32-byte hash (0x followed by 64 hex characters)"
- Error state: Red border if format is invalid
- Placeholder:
- User Address Input Field: Text field with address validation
- Placeholder:
0xd5881aa749eefd3cb08d10f051ac776d664d0663 - Helper text: "Must be a valid Ethereum address (0x followed by 40 hex characters)"
- Error state: Red border if format is invalid
- Placeholder:
- Register Button:
- Disabled if inputs are invalid or transaction is pending
- Shows loading spinner during transaction
- Text changes to "Registering..." during processing
API Call:
POST /api/v1/issuer/credential/register
{
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"age": 25, // Optional
"jurisdiction": "1234567890", // Optional
"accredited": 1 // Optional
}Expected Result:
- Success message displayed with transaction hash
- Credential hash and user address fields cleared
- Credentials list automatically refreshes to show the new credential
- New credential appears in "My Issued Credentials" list
Error Handling:
- Validation Errors:
- "Credential hash must be a valid 32-byte hash" - Invalid hash format
- "User address must be a valid Ethereum address" - Invalid address format
- "Please provide both credential hash and user address" - Missing required fields
- On-Chain Errors:
- "Credential already exists" - Hash is already registered
- "Not trusted issuer" - Your address is not registered as a trusted issuer
- "Credential was revoked" - Attempting to re-register a revoked credential
- Transaction Errors:
- Network errors, insufficient gas, contract revert messages
Gas Costs:
- Credential registration typically costs ~50,000-100,000 gas
- Actual cost depends on network conditions
Action:
- In the "Revoke Credential" section, enter the credential hash to revoke
- Click "Revoke Credential" button
- Confirm the revocation in the browser confirmation dialog
What Happens:
-
Confirmation Dialog:
- Browser shows: "Are you sure you want to revoke credential {hash}?"
- User must confirm before proceeding
-
On-Chain Revocation:
- Backend calls
CredentialRegistry.revokeCredential(hash)on-chain - Contract validates:
- Credential exists
- You are the issuer who registered it (or the contract owner)
- If valid, credential is revoked:
revokedCredentials[hash] = trueCredentialRevokedevent is emitted
- Backend calls
-
Impact:
- Revoked credentials cannot be used for new proof generation
- Existing access grants may be affected (protocols should check credential validity)
- Credential status changes to "revoked" in the UI
UI Components:
- Revoke Credential Section: Separate card for revocation
- Credential Hash Input: Same format as registration
- Revoke Button:
- Red/error color variant to indicate destructive action
- Disabled if hash is empty or transaction is pending
- Shows loading spinner during transaction
- Text changes to "Revoking..." during processing
API Call:
POST /api/v1/issuer/credential/revoke
{
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}Expected Result:
- Success message displayed with transaction hash
- Credentials list automatically refreshes
- Revoked credential status changes to "revoked" (if visible in list)
Error Handling:
- Validation Errors:
- "Please provide credential hash" - Missing hash
- "Please connect your wallet first" - Wallet not connected
- On-Chain Errors:
- "Credential does not exist" - Hash is not registered
- "Not authorized to revoke" - You are not the issuer or owner
- Transaction Errors: Network errors, insufficient gas, etc.
When to Revoke:
- User's KYC status changes (e.g., jurisdiction change, age requirement no longer met)
- Credential was issued in error
- User requests revocation
- Regulatory compliance requirements
- Security concerns (compromised credential)
Action:
- In the "Check Credential Status" section, enter a credential hash
- Click "Check Status" button
What Happens:
- System queries
CredentialRegistry.isCredentialValid(hash)on-chain - Returns
trueif credential exists and is not revoked,falseotherwise
UI Components:
- Check Credential Status Card: Separate section for status checking
- Credential Hash Input: Text field for hash entry
- Check Status Button: Outlined variant, disabled if hash is empty
- Status Alert:
- Green success alert: "Credential is valid"
- Red error alert: "Credential is invalid or revoked"
Expected Result:
- Clear indication of credential validity
- Status displayed immediately after checking
Use Cases:
- Verify a credential before registering (check if already exists)
- Verify a credential's current status
- Troubleshoot credential issues
- Audit credential validity
Understanding how credential hashes are computed is essential for issuers to properly register credentials.
Credentials are hashed using Keccak256 (Ethereum's standard hashing function). The hash is computed from credential data that includes:
- User Address: The Ethereum address of the credential holder
- Age: User's age (numeric value)
- Jurisdiction: User's jurisdiction (converted to hash if string)
- Accredited Status: Whether user is an accredited investor (0 or 1)
- Timestamp: When the credential was created
JavaScript Implementation:
import { ethers } from 'ethers';
function generateCredentialHash(userData) {
const { userAddress, age, jurisdiction, accredited } = userData;
// Convert jurisdiction string to hash (if string)
const jurisdictionHash = ethers.keccak256(ethers.toUtf8Bytes(jurisdiction));
// Create credential data string
const timestamp = Date.now();
const accreditedValue = accredited ? 1 : 0;
const credentialData = `user:${userAddress},age:${age},jurisdiction:${jurisdictionHash},accredited:${accreditedValue},timestamp:${timestamp}`;
// Hash the credential data
const credentialHash = ethers.keccak256(ethers.toUtf8Bytes(credentialData));
return credentialHash;
}Hash Format:
- Full Hash: 32 bytes (64 hex characters after
0x) - Example:
0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef - Truncation: For circuit compatibility, the last 60 bits (15 hex chars) are used in proofs
- Deterministic Hashing: Same input data always produces the same hash
- Hash Uniqueness: Different credentials should have different hashes
- Hash Format: Must be exactly 32 bytes (64 hex characters)
- Case Sensitivity: Hash is case-insensitive (but typically lowercase)
- Pre-image Resistance: Cannot reverse hash to get original data
When registering a credential:
- The hash must be computed before registration
- The hash is stored on-chain as
bytes32 - The hash serves as the unique identifier for the credential
- Users will need this hash to generate proofs
flowchart TD
Start([Issuer Opens Dashboard]) --> ConnectWallet{Wallet<br/>Connected?}
ConnectWallet -->|No| ShowConnect[Show: Connect MetaMask Wallet]
ShowConnect --> WaitConnect[Wait for User to Connect]
WaitConnect --> ConnectWallet
ConnectWallet -->|Yes| CheckIssuer{Is Trusted<br/>Issuer?}
CheckIssuer -->|No| ShowNotIssuer[Show: Not a Trusted Issuer<br/>Contact Administrator]
CheckIssuer -->|Yes| LoadCredentials[Load Issued Credentials]
LoadCredentials --> DisplayCredentials[Display Credentials List]
DisplayCredentials --> UserAction{User Action}
UserAction -->|Register| RegisterFlow[Register Credential Flow]
UserAction -->|Revoke| RevokeFlow[Revoke Credential Flow]
UserAction -->|Check Status| CheckFlow[Check Status Flow]
UserAction -->|View List| DisplayCredentials
RegisterFlow --> EnterHash[Enter Credential Hash]
EnterHash --> EnterUser[Enter User Address]
EnterUser --> ValidateInputs{Validate<br/>Inputs}
ValidateInputs -->|Invalid Hash| ShowHashError[Show: Invalid Hash Format<br/>Must be 0x + 64 hex chars]
ValidateInputs -->|Invalid Address| ShowAddressError[Show: Invalid Address Format<br/>Must be 0x + 40 hex chars]
ValidateInputs -->|Valid| SubmitRegister[Submit Registration Request]
ShowHashError --> EnterHash
ShowAddressError --> EnterUser
SubmitRegister --> OnChainRegister[On-Chain Registration:<br/>CredentialRegistry.registerCredential]
OnChainRegister --> CheckOnChain{On-Chain<br/>Checks}
CheckOnChain -->|Already Exists| ShowExistsError[Show: Credential Already Exists]
CheckOnChain -->|Was Revoked| ShowRevokedError[Show: Credential Was Revoked]
CheckOnChain -->|Not Issuer| ShowNotIssuerError[Show: Not Trusted Issuer]
CheckOnChain -->|Success| StoreCredential[Store Credential:<br/>credentials mapping = true<br/>credentialIssuers mapping = issuer]
StoreCredential --> EmitEvent[Emit CredentialIssued Event]
EmitEvent --> SaveToDB{Save to<br/>Database?}
SaveToDB -->|Yes| StoreData[Store Credential Data<br/>age, jurisdiction, accredited]
SaveToDB -->|No| RefreshList
StoreData --> RefreshList[Refresh Credentials List]
RefreshList --> ShowSuccess[Show Success Message<br/>with Transaction Hash]
ShowSuccess --> DisplayCredentials
ShowExistsError --> EnterHash
ShowRevokedError --> EnterHash
ShowNotIssuerError --> End([End])
RevokeFlow --> EnterRevokeHash[Enter Credential Hash to Revoke]
EnterRevokeHash --> ConfirmRevoke{Confirm<br/>Revocation?}
ConfirmRevoke -->|No| DisplayCredentials
ConfirmRevoke -->|Yes| SubmitRevoke[Submit Revocation Request]
SubmitRevoke --> OnChainRevoke[On-Chain Revocation:<br/>CredentialRegistry.revokeCredential]
OnChainRevoke --> CheckRevokeAuth{Authorization<br/>Check}
CheckRevokeAuth -->|Not Authorized| ShowRevokeAuthError[Show: Not Authorized to Revoke]
CheckRevokeAuth -->|Credential Not Found| ShowNotFoundError[Show: Credential Does Not Exist]
CheckRevokeAuth -->|Success| MarkRevoked[Mark as Revoked:<br/>revokedCredentials mapping = true]
MarkRevoked --> EmitRevokeEvent[Emit CredentialRevoked Event]
EmitRevokeEvent --> RefreshListRevoke[Refresh Credentials List]
RefreshListRevoke --> ShowRevokeSuccess[Show Revocation Success<br/>with Transaction Hash]
ShowRevokeSuccess --> DisplayCredentials
ShowRevokeAuthError --> EnterRevokeHash
ShowNotFoundError --> EnterRevokeHash
CheckFlow --> EnterCheckHash[Enter Credential Hash]
EnterCheckHash --> QueryStatus[Query On-Chain Status:<br/>isCredentialValid]
QueryStatus --> DisplayStatus{Status<br/>Result}
DisplayStatus -->|Valid| ShowValid[Show: Credential is Valid]
DisplayStatus -->|Invalid| ShowInvalid[Show: Credential is Invalid or Revoked]
ShowValid --> DisplayCredentials
ShowInvalid --> DisplayCredentials
style Start fill:#e1f5ff
style End fill:#c8e6c9
style ShowConnect fill:#fff3cd
style ShowNotIssuer fill:#ffcdd2
style ShowHashError fill:#ffcdd2
style ShowAddressError fill:#ffcdd2
style ShowExistsError fill:#ffcdd2
style ShowRevokedError fill:#ffcdd2
style ShowNotIssuerError fill:#ffcdd2
style ShowRevokeAuthError fill:#ffcdd2
style ShowNotFoundError fill:#ffcdd2
style ShowSuccess fill:#c8e6c9
style ShowRevokeSuccess fill:#c8e6c9
style StoreCredential fill:#c8e6c9
style MarkRevoked fill:#fff3cd
style ShowValid fill:#c8e6c9
style ShowInvalid fill:#ffcdd2
-
Credential Hash Management
- Compute hashes deterministically using consistent data format
- Store credential hashes securely (they're needed for revocation)
- Verify hash format before registration (32 bytes, 64 hex characters)
-
When to Issue Credentials
- After successful KYC verification
- When user meets all compliance requirements
- After verifying user identity and eligibility
- Before users need to access DeFi protocols
-
When to Revoke Credentials
- User's KYC status changes (jurisdiction, age, accreditation)
- Credential issued in error
- User requests revocation
- Regulatory compliance requires revocation
- Security concerns (compromised credential)
-
Credential Data Storage
- Include age, jurisdiction, and accredited status when registering
- This enables faster proof generation for users
- Data is stored off-chain in database (not on-chain for privacy)
-
Security Considerations
- Only register credentials after proper KYC verification
- Verify user addresses before registration
- Keep issuer private keys secure
- Monitor for suspicious credential patterns
-
Error Handling
- Always validate inputs before submission
- Check credential status before registration
- Handle transaction failures gracefully
- Provide clear error messages to users
-
Monitoring
- Track all issued credentials
- Monitor credential revocation events
- Audit credential status regularly
- Review access patterns for anomalies
Cause: Wallet not connected
Solution: Click "Connect Wallet" button and approve the connection in MetaMask
Cause: Your address is not registered as a trusted issuer in CredentialRegistry
Solution: Contact the protocol administrator to add your address as a trusted issuer using CredentialRegistry.addIssuer()
Cause: Invalid hash format
Solution: Ensure hash is exactly 64 hex characters after 0x (e.g., 0x1234...abcd)
Cause: Invalid address format
Solution: Ensure address is exactly 40 hex characters after 0x (e.g., 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb)
Cause: Hash is already registered on-chain
Solution:
- Verify the hash is correct
- If re-registering is needed, use a different hash (different credential data)
- Check if credential was previously revoked
Cause: Attempting to register a previously revoked credential
Solution:
- Credentials cannot be re-registered after revocation
- Issue a new credential with a new hash if needed
Cause: You are not the issuer who registered the credential
Solution:
- Only the original issuer (or contract owner) can revoke
- Verify you registered this credential
- Contact the contract owner if revocation is needed
Cause: Hash is not registered on-chain
Solution:
- Verify the hash is correct
- Check if credential was ever registered
- Ensure you're checking the correct network
- Frontend Component:
frontend/src/pages/IssuerDashboard.jsx - API Endpoints:
GET /api/v1/issuer/credentials/:issuer- Get issuer credentialsPOST /api/v1/issuer/credential/register- Register credentialPOST /api/v1/issuer/credential/revoke- Revoke credentialGET /api/v1/issuer/credential/check/:hash- Check credential validity
- Smart Contracts:
CredentialRegistry.registerCredential()- Register credential on-chainCredentialRegistry.revokeCredential()- Revoke credential on-chainCredentialRegistry.isCredentialValid()- Check credential validityCredentialRegistry.addIssuer()- Add trusted issuer (owner-only)
- Events:
CredentialIssued(user, credentialHash, issuer, timestamp)- Emitted when credential is registeredCredentialRevoked(credentialHash, issuer, timestamp)- Emitted when credential is revoked
- Go 1.21+ (for circuit development)
- Foundry (for Solidity development)
- Node.js (for Hardhat, if using)
# Clone the repository
git clone <repository-url>
cd Pyp
# Install Go dependencies
go mod download
# Install Foundry (if not installed)
curl -L https://foundry.paradigm.xyz | bash
foundryup- Generate the ZK Verifier Contract
# Generate proving key and verifier contract
go run cmd/generate-verifier/main.goThis will:
- Compile the Go circuit to R1CS
- Generate Groth16 proving and verification keys
- Generate the Solidity verifier contract
- Save files to
build/directory
- Build Solidity Contracts
forge build- Run Tests
# Run all tests
forge test
# Run specific test file
forge test --match-contract CredentialRegistryTest
forge test --match-contract ProtocolAccessControlTest
# Run with gas reporting
forge test --gas-report- Create input file (
build/test-input.json):
{
"actualAge": 25,
"actualJurisdiction": 1234567890,
"actualAccredited": 1,
"credentialHash": 9876543210,
"minAge": 21,
"allowedJurisdictions": [1234567890, 1111111111, 0, 0, 0, 0, 0, 0, 0, 0],
"requireAccredited": 1,
"credentialHashPublic": 9876543210
}- Generate proof:
go run cmd/prove/main.go build/test-input.json- Proof saved to
build/proof.json
The NOAH SDK is published as an npm package and can be easily installed in your project.
Install the SDK using npm, yarn, or pnpm:
# Using npm
npm install noah-protocol-sdk ethers
# Using yarn
yarn add noah-protocol-sdk ethers
# Using pnpm
pnpm add noah-protocol-sdk ethersThe SDK requires the following peer dependencies:
ethers(^6.0.0) - Required for blockchain interactionsreact(^18.0.0 || ^19.0.0) - Optional, only needed for React hooksreact-dom(^18.0.0 || ^19.0.0) - Optional, only needed for React hooks@tanstack/react-query(^5.0.0) - Optional, only needed for React hooks
For DeFi Protocols:
import { ProtocolClient } from 'noah-protocol-sdk';
import { ethers } from 'ethers';
// Connect wallet
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// Initialize protocol client
const protocol = new ProtocolClient(signer);
// Set KYC requirements
await protocol.setRequirements({
minAge: 21,
jurisdictions: ['US', 'UK', 'CA'],
requireAccredited: true
});For End Users:
import { UserClient } from 'noah-protocol-sdk';
import { ethers } from 'ethers';
// Connect wallet
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// Initialize user client
// API base URL is configured via environment variables (see .env.example)
const user = new UserClient(signer, {
apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000/api/v1'
});
// Generate proof and verify access
const proof = await user.generateProof({
credential: {
credentialHash: '0x...',
age: 25,
jurisdiction: 'US',
accredited: 1
},
requirements: {
protocolAddress: '0x...',
minAge: 21,
allowedJurisdictions: ['US', 'UK'],
requireAccredited: true
}
});
await user.verifyAndGrantAccess({
proof: proof.proof,
publicSignals: proof.publicSignals,
credentialHash: proof.credentialHash,
protocolAddress: '0x...'
});- Package Name:
noah-protocol-sdk - Current Version:
0.1.2 - License: MIT
- Repository: GitHub Repository
- npm Registry: npmjs.com/package/noah-protocol-sdk
The SDK is written in TypeScript and includes full type definitions. No additional @types package is required.
import { ProtocolClient, UserClient, IssuerClient } from 'noah-protocol-sdk';
import type { Requirements, ProofResult, TransactionResult } from 'noah-protocol-sdk';If you're using React, you can use the provided hooks for easier integration:
import { useProtocol, useUser, useCredentials } from 'noah-protocol-sdk';
function MyComponent() {
const { requirements, setRequirements, hasAccess } = useProtocol(signer);
const { generateProof, verifyAndGrantAccess } = useUser(signer);
// Use the hooks in your component
}Note: React hooks require react, react-dom, and @tanstack/react-query to be installed.
The SDK can be configured with custom contract addresses and API endpoints:
const protocol = new ProtocolClient(signer, {
protocolAccessControlAddress: '0xCustomAddress...',
provider: customProvider
});
const user = new UserClient(signer, {
apiBaseUrl: 'https://api.noah.xyz/api/v1',
contractAddresses: {
protocolAccessControl: '0x...',
credentialRegistry: '0x...',
zkVerifier: '0x...'
}
});For production deployments, configure the API base URL via environment variables. Each example project includes a .env.example file that you can copy and customize:
# Copy the example file
cp .env.example .env
# Edit .env with your production valuesExample .env.example files:
- Next.js Example:
examples/nextjs-example/.env.example - React Example:
examples/react-example/.env.example - Frontend:
frontend/.env.example
Environment Variable Names:
- Next.js:
NEXT_PUBLIC_API_BASE_URL - Vite/React:
VITE_API_BASE_URL
Example values:
# For production (Render deployment)
NEXT_PUBLIC_API_BASE_URL=https://noah-abw7.onrender.com
VITE_API_BASE_URL=https://noah-abw7.onrender.com
# For local development
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api/v1
VITE_API_BASE_URL=http://localhost:3000/api/v1For complete SDK documentation, API reference, and advanced usage examples, see:
- SDK README:
packages/noah-sdk/README.md - SDK Examples:
examples/README.md
For developers looking to integrate NOAH into their applications, we provide comprehensive example projects demonstrating both end-to-end user flows and DeFi protocol integration.
📚 View SDK Examples Documentation →
The examples include:
- React Example: Client-side integration with React hooks
- Next.js Example: Full-stack TypeScript application
Each example demonstrates:
- ✅ Wallet connection and management
- ✅ Protocol requirement setting and verification
- ✅ User proof generation and access verification
- ✅ Issuer credential management
- ✅ Complete integration patterns
See the SDK Examples README for:
- Step-by-step setup instructions
- Complete use case examples for end users and DeFi protocols
- API reference and integration guides
- Troubleshooting tips
.
├── circuit/ # Go circuit definitions
│ ├── zkkyc.go # Main NOAH circuit
│ └── zkkyc_test.go # Circuit tests
├── cmd/
│ ├── generate-verifier/ # Verifier generation tool
│ │ └── main.go
│ └── prove/ # Proof generation tool
│ └── main.go
├── src/ # Solidity contracts
│ ├── CredentialRegistry.sol
│ ├── ProtocolAccessControl.sol
│ ├── ZKVerifier.sol # Generated verifier
│ └── IZKVerifier.sol # Verifier interface
├── test/ # Foundry tests
│ ├── CredentialRegistry.t.sol
│ └── ProtocolAccessControl.t.sol
├── build/ # Build artifacts
│ ├── proving_key.pk # Proving key
│ ├── circuit.ccs # Compiled circuit
│ └── proof.json # Generated proofs
└── script/ # Deployment scripts
└── Deploy.s.sol
The test suite includes comprehensive edge case coverage:
- CredentialRegistry: 25 tests covering duplicates, revocations, access control, etc.
- ProtocolAccessControl: 23 tests covering proof verification, requirement mismatches, etc.
Run tests with verbose output:
forge test -vvv- Deploy CredentialRegistry:
CredentialRegistry registry = new CredentialRegistry();- Deploy ZKVerifier:
ZKVerifier verifier = new ZKVerifier();- Deploy ProtocolAccessControl:
ProtocolAccessControl accessControl = new ProtocolAccessControl(
address(verifier),
address(registry)
);- Setup Issuers:
registry.addIssuer(issuerAddress, "Issuer Name");See script/Deploy.s.sol for deployment script example.
This section documents all backend API endpoints with request/response formats. All endpoints are accessible through the API Gateway at port 3000, which routes requests to the appropriate microservices.
- Gateway:
http://localhost:3000 - Direct Service Access (for development):
- User Service:
http://localhost:3002 - Issuer Service:
http://localhost:3001 - Protocol Service:
http://localhost:3003 - Proof Service:
http://localhost:3004
- User Service:
All endpoints are versioned under /api/v1/. The gateway automatically routes requests to the appropriate service.
All endpoints return JSON responses. Error responses follow this format:
{
"success": false,
"error": {
"message": "Error message",
"details": "Additional error details (optional)"
}
}Check the health status of the gateway and all dependent services.
Endpoint: GET /api/v1/health or GET /health
Response:
{
"status": "ok",
"timestamp": "2024-01-01T00:00:00.000Z",
"services": {
"blockchain": {
"status": "ok",
"blockNumber": 12345678
},
"database": {
"status": "ok"
},
"redis": {
"status": "ok"
},
"contracts": {
"status": "ok"
}
}
}Status Codes:
200: All services healthy503: One or more services degraded
Retrieve the KYC requirements for a specific protocol.
Endpoint: GET /api/v1/user/protocol/:address/requirements
Path Parameters:
address(string, required): Ethereum address of the protocol (must be valid 0x-prefixed hex address)
Response:
{
"protocol": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"minAge": "21",
"allowedJurisdictions": ["1234567890", "1111111111"],
"requireAccredited": true
}Status Codes:
200: Success400: Invalid address format500: Service error
Example:
curl http://localhost:3000/api/v1/user/protocol/0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb/requirementsCheck if a user has been granted access to a protocol.
Endpoint: GET /api/v1/user/access/:protocol/:user
Path Parameters:
protocol(string, required): Ethereum address of the protocoluser(string, required): Ethereum address of the user
Response:
{
"protocol": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"user": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"hasAccess": true
}Status Codes:
200: Success400: Invalid address format500: Service error
Example:
curl http://localhost:3000/api/v1/user/access/0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb/0x8ba1f109551bD432803012645Hac136c22C9c8dRetrieve all credentials associated with a user address.
Endpoint: GET /api/v1/user/credentials/:user
Path Parameters:
user(string, required): Ethereum address of the user
Response:
[
{
"credential_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"user_address": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"issuer_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"age": 25,
"jurisdiction": "1234567890",
"accredited": 1,
"created_at": "2024-01-01T00:00:00.000Z"
}
]Status Codes:
200: Success (returns empty array if no credentials found)400: Invalid address format500: Service error
Example:
curl http://localhost:3000/api/v1/user/credentials/0x8ba1f109551bD432803012645Hac136c22C9c8dRetrieve credential data by its hash. First checks the database, then falls back to on-chain verification.
Endpoint: GET /api/v1/user/credential/:hash
Path Parameters:
hash(string, required): 32-byte credential hash (0x-prefixed hex string, 64 hex characters)
Response (Found in Database):
{
"credential_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"user_address": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"issuer_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"age": 25,
"jurisdiction": "1234567890",
"accredited": 1,
"created_at": "2024-01-01T00:00:00.000Z"
}Response (Found On-Chain but Not in Database):
{
"error": {
"message": "Credential found on-chain but data not available in database",
"details": "The credential hash is registered on-chain, but the credential data (age, jurisdiction, accredited) is not available. Please contact the issuer to ensure the credential is properly registered in the system.",
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"issuerAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"existsOnChain": true
}
}Response (Not Found):
{
"error": {
"message": "Credential not found",
"details": "The credential hash is not registered on-chain. Please ensure the credential has been registered by an issuer.",
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"existsOnChain": false
}
}Status Codes:
200: Success (credential found in database)404: Credential not found or not in database400: Invalid hash format500: Service error
Example:
curl http://localhost:3000/api/v1/user/credential/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdefRegister a new credential hash on-chain. Optionally includes credential data (age, jurisdiction, accredited) for database storage.
Endpoint: POST /api/v1/issuer/credential/register
Request Body:
{
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"age": 25,
"jurisdiction": "1234567890",
"accredited": 1
}Request Fields:
credentialHash(string, required): 32-byte credential hash (0x-prefixed hex string)userAddress(string, required): Ethereum address of the credential holderage(integer, optional): User's age (0-150)jurisdiction(string|number, optional): Jurisdiction identifier (number or string)accredited(integer, optional): Accredited investor status (0 or 1)
Response:
{
"success": true,
"transactionHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d"
}Status Codes:
200: Success400: Validation error (invalid hash, address, or field values)500: Service error or transaction failure
Rate Limiting: Strict rate limiting applied (fewer requests per minute)
Example:
curl -X POST http://localhost:3000/api/v1/issuer/credential/register \
-H "Content-Type: application/json" \
-d '{
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"age": 25,
"jurisdiction": "1234567890",
"accredited": 1
}'Revoke a credential on-chain. Once revoked, the credential cannot be used for access grants.
Endpoint: POST /api/v1/issuer/credential/revoke
Request Body:
{
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}Request Fields:
credentialHash(string, required): 32-byte credential hash to revoke
Response:
{
"success": true,
"transactionHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}Status Codes:
200: Success400: Validation error (invalid hash format)500: Service error or transaction failure
Rate Limiting: Strict rate limiting applied
Example:
curl -X POST http://localhost:3000/api/v1/issuer/credential/revoke \
-H "Content-Type: application/json" \
-d '{
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}'Check if a credential hash is valid (exists and not revoked) on-chain.
Endpoint: GET /api/v1/issuer/credential/check/:hash
Path Parameters:
hash(string, required): 32-byte credential hash
Response:
{
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"isValid": true
}Status Codes:
200: Success400: Invalid hash format500: Service error
Example:
curl http://localhost:3000/api/v1/issuer/credential/check/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdefRetrieve all credentials issued by a specific issuer address.
Endpoint: GET /api/v1/issuer/credentials/:issuer
Path Parameters:
issuer(string, required): Ethereum address of the issuer
Response:
[
{
"credential_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"user_address": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"issuer_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"age": 25,
"jurisdiction": "1234567890",
"accredited": 1,
"created_at": "2024-01-01T00:00:00.000Z"
}
]Status Codes:
200: Success (returns empty array if no credentials found or database unavailable)400: Invalid address format500: Service error
Example:
curl http://localhost:3000/api/v1/issuer/credentials/0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbSet or update the KYC requirements for a protocol. Requires protocol private key for authentication.
Endpoint: POST /api/v1/protocol/requirements/set
Request Body:
{
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"minAge": 21,
"allowedJurisdictions": [1234567890, 1111111111],
"requireAccredited": true,
"privateKey": "0x..."
}Request Fields:
protocolAddress(string, required): Ethereum address of the protocolminAge(integer, required): Minimum age requirement (0-150)allowedJurisdictions(array, required): Array of allowed jurisdiction identifiers (max 10). Each element can be:- Integer number
- Numeric string (for large hash values)
- Hex string (0x-prefixed)
requireAccredited(boolean, required): Whether accredited investor status is requiredprivateKey(string, optional): Protocol private key (if not provided, usesPROTOCOL_PRIVATE_KEYenv variable)
Response:
{
"success": true,
"transactionHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"requirements": {
"minAge": 21,
"allowedJurisdictions": [1234567890, 1111111111],
"requireAccredited": true
}
}Status Codes:
200: Success400: Validation error (invalid address, age out of range, too many jurisdictions, etc.)500: Service error or transaction failure
Rate Limiting: Strict rate limiting applied
Example:
curl -X POST http://localhost:3000/api/v1/protocol/requirements/set \
-H "Content-Type: application/json" \
-d '{
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"minAge": 21,
"allowedJurisdictions": ["1234567890", "1111111111"],
"requireAccredited": true
}'Retrieve the current KYC requirements for a protocol.
Endpoint: GET /api/v1/protocol/requirements/:protocolAddress
Path Parameters:
protocolAddress(string, required): Ethereum address of the protocol
Response:
{
"protocol": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"minAge": "21",
"allowedJurisdictions": ["1234567890", "1111111111"],
"requireAccredited": true,
"isSet": true
}Status Codes:
200: Success400: Invalid address format500: Service error
Example:
curl http://localhost:3000/api/v1/protocol/requirements/0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbVerify a zero-knowledge proof and grant access to a user if the proof is valid. This endpoint performs on-chain verification.
Endpoint: POST /api/v1/protocol/access/verify
Request Body:
{
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"proof": {
"a": ["0x...", "0x..."],
"b": [["0x...", "0x..."], ["0x...", "0x..."]],
"c": ["0x...", "0x..."]
},
"publicSignals": [
"21",
"1234567890",
"1111111111",
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"1",
"9876543210",
"1"
],
"privateKey": "0x..."
}Request Fields:
protocolAddress(string, required): Ethereum address of the protocoluserAddress(string, required): Ethereum address of the user requesting accesscredentialHash(string, required): 32-byte credential hashproof(object, required): Groth16 proof components:a(array): G1 point [x, y] (2 elements)b(array): G2 point [[x0, x1], [y0, y1]] (2x2 array)c(array): G1 point [x, y] (2 elements)
publicSignals(array, required): Array of 13 public signals (as strings or numbers):[0]: minAge[1-10]: allowedJurisdictions (10 elements, padded with 0)[11]: requireAccredited (0 or 1)[12]: credentialHashPublic (truncated 60-bit hash)- Note: The 14th signal (isValid) is reconstructed on-chain
privateKey(string, optional): Protocol private key (if not provided, usesPROTOCOL_PRIVATE_KEYenv variable)
Response:
{
"success": true,
"transactionHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"message": "Access granted successfully"
}Status Codes:
200: Success (proof verified and access granted)400: Validation error (missing fields, invalid format)500: Service error or transaction failure (proof invalid, credential revoked, etc.)
Rate Limiting: Strict rate limiting applied
Example:
curl -X POST http://localhost:3000/api/v1/protocol/access/verify \
-H "Content-Type: application/json" \
-d '{
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"proof": { ... },
"publicSignals": [ ... ]
}'Revoke a user's access to a protocol. Requires protocol private key.
Endpoint: POST /api/v1/protocol/access/revoke
Request Body:
{
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"privateKey": "0x..."
}Request Fields:
protocolAddress(string, required): Ethereum address of the protocoluserAddress(string, required): Ethereum address of the user to revoke access forprivateKey(string, optional): Protocol private key (if not provided, usesPROTOCOL_PRIVATE_KEYenv variable)
Response:
{
"success": true,
"transactionHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d"
}Status Codes:
200: Success400: Validation error (invalid addresses)500: Service error or transaction failure
Rate Limiting: Strict rate limiting applied
Example:
curl -X POST http://localhost:3000/api/v1/protocol/access/revoke \
-H "Content-Type: application/json" \
-d '{
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d"
}'Check if a user has access to a protocol (read-only, no authentication required).
Endpoint: GET /api/v1/protocol/access/:protocolAddress/:userAddress
Path Parameters:
protocolAddress(string, required): Ethereum address of the protocoluserAddress(string, required): Ethereum address of the user
Response:
{
"protocol": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"user": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"hasAccess": true
}Status Codes:
200: Success400: Invalid address format500: Service error
Example:
curl http://localhost:3000/api/v1/protocol/access/0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb/0x8ba1f109551bD432803012645Hac136c22C9c8dGenerate a zero-knowledge proof for a credential against protocol requirements. Requires authentication token.
Endpoint: POST /api/v1/proof/generate
Authentication: Bearer token required (see Authentication section)
Request Body:
{
"credential": {
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"age": 25,
"jurisdiction": "1234567890",
"accredited": 1,
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d"
},
"requirements": {
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"minAge": 21,
"allowedJurisdictions": [1234567890, 1111111111],
"requireAccredited": true
}
}Request Fields:
credential(object, required): Credential datacredentialHash(string, required): 32-byte credential hashage(integer, required): User's actual age (0-150)jurisdiction(string|number, required): User's actual jurisdiction identifieraccredited(integer, required): User's accredited status (0 or 1)userAddress(string, optional): User's Ethereum address
requirements(object, required): Protocol requirementsprotocolAddress(string, required): Protocol Ethereum addressminAge(integer, required): Minimum age requirement (0-150)allowedJurisdictions(array, required): Array of allowed jurisdiction identifiersrequireAccredited(boolean, optional): Whether accreditation is required (default: false)
Response:
{
"success": true,
"proof": {
"a": ["0x...", "0x..."],
"b": [["0x...", "0x..."], ["0x...", "0x..."]],
"c": ["0x...", "0x..."],
"publicSignals": [
"21",
"1234567890",
"1111111111",
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"1",
"9876543210",
"1"
]
},
"publicSignals": [
"21",
"1234567890",
"1111111111",
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"1",
"9876543210",
"1"
],
"publicInputs": [
"21",
"1234567890",
"1111111111",
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"1",
"9876543210",
"1"
],
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}Response Fields:
proof: Formatted proof object ready for on-chain submissiona,b,c: Groth16 proof componentspublicSignals: Array of 14 public signals (includes isValid as last element)
publicSignals: Same asproof.publicSignals(for convenience)publicInputs: Original public inputs from proof generation (for backward compatibility)credentialHash: The credential hash used in proof generation
Status Codes:
200: Success400: Validation error (invalid credential data, requirements mismatch, etc.)401: Unauthorized (missing or invalid authentication token)500: Service error or proof generation failure
Rate Limiting: Strict rate limiting applied
Example:
curl -X POST http://localhost:3000/api/v1/proof/generate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"credential": {
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"age": 25,
"jurisdiction": "1234567890",
"accredited": 1
},
"requirements": {
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"minAge": 21,
"allowedJurisdictions": [1234567890, 1111111111],
"requireAccredited": true
}
}'Retrieve all generated proofs for a specific credential hash. Requires authentication token.
Endpoint: GET /api/v1/proof/credential/:hash
Authentication: Bearer token required
Path Parameters:
hash(string, required): 32-byte credential hash
Response:
{
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"proofs": [
{
"id": 1,
"credentialHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"userAddress": "0x8ba1f109551bD432803012645Hac136c22C9c8d",
"protocolAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"proofData": { ... },
"publicSignals": [ ... ],
"status": "generated",
"created_at": "2024-01-01T00:00:00.000Z"
}
]
}Status Codes:
200: Success400: Invalid hash format401: Unauthorized (missing or invalid authentication token)500: Service error
Example:
curl http://localhost:3000/api/v1/proof/credential/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef \
-H "Authorization: Bearer YOUR_TOKEN"Common HTTP status codes returned by the API:
- 200 OK: Request successful
- 400 Bad Request: Invalid request format, validation error, or missing required fields
- 401 Unauthorized: Missing or invalid authentication token (for protected endpoints)
- 404 Not Found: Resource not found (credential, user, etc.)
- 429 Too Many Requests: Rate limit exceeded
- 500 Internal Server Error: Server error or transaction failure
- 503 Service Unavailable: Service or dependency unavailable
The API implements rate limiting to prevent abuse:
- Standard Endpoints: Moderate rate limit (e.g., 100 requests per minute)
- Strict Endpoints: Lower rate limit (e.g., 10 requests per minute)
- Credential registration
- Credential revocation
- Setting requirements
- Proof verification
- Proof generation
Rate limit headers are included in responses:
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Remaining requests in current windowX-RateLimit-Reset: Time when rate limit resets
Some endpoints require authentication:
- Proof Generation (
POST /api/v1/proof/generate): Requires Bearer token - Get Proofs (
GET /api/v1/proof/credential/:hash): Requires Bearer token
Authentication is handled via JWT tokens. Include the token in the Authorization header:
Authorization: Bearer YOUR_JWT_TOKEN
- Format:
0xfollowed by 40 hexadecimal characters - Example:
0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb - Validation: Must match regex
/^0x[a-fA-F0-9]{40}$/
- Format:
0xfollowed by 64 hexadecimal characters (32 bytes) - Example:
0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef - Validation: Must match regex
/^0x[a-fA-F0-9]{64}$/
- Can be:
- Integer numbers:
1234567890 - Numeric strings:
"1234567890"(for large values) - Hex strings:
"0x1234567890abcdef..."
- Integer numbers:
- G1 Points (
a,c): Array of 2 BigInt values[x, y] - G2 Points (
b): Array of 2 arrays, each with 2 BigInt values[[x0, x1], [y0, y1]] - All values are serialized as hex strings or decimal strings in JSON
- Array of 13-14 string values representing:
[0]: minAge (string)[1-10]: allowedJurisdictions (10 strings, padded with "0")[11]: requireAccredited ("0" or "1")[12]: credentialHashPublic (truncated 60-bit hash as string)[13]: isValid ("0" or "1") - included in proof generation response, reconstructed on-chain
- Trusted Setup: The current setup uses a single-party trusted setup. For production, consider a multi-party ceremony (PPOT).
- Credential Revocation: Issuers can revoke credentials, which immediately invalidates access.
- Zero Address Checks: Some functions don't check for zero addresses - add these in production.
- Jurisdiction Limits: Maximum 10 jurisdictions per protocol to prevent gas issues.
- Proof Verification: Always verify proofs on-chain; never trust off-chain verification alone.
NOAH is continuously evolving to provide better privacy, performance, and user experience. This section outlines planned technical improvements and product enhancements.
Current State: NOAH uses a single-party trusted setup for the Groth16 proving and verification keys.
Future Enhancement: Implement a Powers of Tau (PPOT) ceremony with multiple participants to ensure trustless setup.
Benefits:
- Eliminates the need to trust a single party
- Increases security and decentralization
- Follows industry best practices (similar to Zcash, Tornado Cash)
- Enables community participation in setup
Implementation Considerations:
- Use existing PPOT ceremony tools (e.g., Perpetual Powers of Tau)
- Coordinate multi-party ceremony with key stakeholders
- Document ceremony process and participants
- Publish ceremony transcript for verification
Current State: The circuit has constraints for age verification, jurisdiction checks, hash verification, and accreditation status.
Future Enhancements:
- Reduce Constraint Count: Optimize arithmetic operations to minimize circuit size
- Lookup Tables: Use lookup tables for jurisdiction membership checks instead of individual comparisons
- Custom Gates: Implement custom gates for common operations (e.g., hash truncation)
- Constraint Reordering: Optimize constraint ordering for better proving performance
Expected Impact:
- 20-30% reduction in proof generation time
- Lower gas costs for verification
- Faster user experience
Current State: NOAH uses Groth16 zk-SNARKs.
Future Options:
PLONK:
- Universal trusted setup (reusable across circuits)
- Smaller verification keys
- Better for circuit updates
STARKs:
- No trusted setup required
- Quantum-resistant
- Larger proof sizes but faster verification
Implementation Strategy:
- Evaluate proof size vs. verification cost trade-offs
- Consider hybrid approach (Groth16 for current use cases, PLONK for new features)
- Monitor industry developments in proof systems
Current State: Each proof is verified individually on-chain.
Future Enhancement: Implement batch verification for multiple proofs in a single transaction.
Benefits:
- Reduced gas costs per verification (amortized)
- Better scalability for protocols with high verification volume
- Enables bulk access grants
Implementation Approach:
- Aggregate multiple proofs into a single batch proof
- Verify batch proof on-chain
- Maintain individual proof validity tracking
Current State: Proofs verify single credential attributes.
Future Enhancement: Support recursive proofs that can verify multiple credentials or combine proofs from different sources.
Use Cases:
- Multi-credential verification (e.g., "I have KYC credential AND accreditation credential")
- Cross-protocol proof portability
- Proof of proofs (verifying that a proof was verified elsewhere)
Technical Requirements:
- Circuit modifications to accept proof inputs
- Recursive verification key generation
- Careful gas cost analysis
Current State: Credentials are immutable once issued; updates require new credentials.
Future Enhancement: Enable privacy-preserving credential updates without revealing what changed.
Implementation Ideas:
- Use zero-knowledge proofs to show credential update validity
- Maintain credential version history
- Allow selective update disclosure (e.g., "age updated" without revealing old/new age)
Enhancement: Develop native mobile applications (iOS and Android) for NOAH.
Features:
- Mobile wallet integration (WalletConnect)
- QR code scanning for protocol addresses
- Push notifications for credential status
- Offline proof generation preparation
Benefits:
- Broader user accessibility
- Better user experience on mobile devices
- Increased adoption in mobile-first markets
Enhancement: Create a browser extension for seamless DeFi protocol integration.
Features:
- Automatic protocol detection on DeFi websites
- One-click proof generation and submission
- Credential management in extension popup
- Background proof generation
Use Cases:
- Users visiting DeFi protocols automatically see NOAH integration
- Seamless access without leaving the protocol website
- Reduced friction in user onboarding
Current State: NOAH is deployed on Mantle Sepolia testnet.
Future Chains:
- Ethereum Mainnet: Primary deployment for production
- Polygon: Lower gas costs, high throughput
- Arbitrum: Fast finality, low costs
- Optimism: EVM compatibility, low fees
- Base: Coinbase-backed, growing ecosystem
- zkSync Era: Native zk-rollup, efficient verification
Implementation Strategy:
- Deploy verifier contracts on each chain
- Cross-chain credential portability (via bridges or message passing)
- Unified frontend supporting multiple chains
- Chain-specific gas optimization
Enhancement: Add expiration timestamps to credentials with renewal mechanisms.
Features:
- Expiration date in credential hash computation
- Automatic expiration checks in circuit
- Renewal workflow for issuers
- User notifications before expiration
Benefits:
- Ensures credentials remain current
- Compliance with regulatory requirements
- Better credential lifecycle management
Enhancement: Allow users to delegate proof generation to trusted services.
Use Cases:
- Users with limited computational resources
- Enterprise users with dedicated proof generation services
- Mobile users delegating to cloud services
Security Considerations:
- Ensure delegation doesn't compromise privacy
- Use encryption for credential data transmission
- Implement access controls for delegated services
Enhancement: Provide protocols with analytics on access grants and user demographics (privacy-preserving).
Metrics:
- Total access grants (without revealing user identities)
- Jurisdiction distribution (aggregated)
- Age range statistics (aggregated)
- Access grant trends over time
Privacy Guarantees:
- All analytics are aggregated
- No individual user data exposed
- Differential privacy techniques where applicable
Enhancement: Develop and maintain SDKs for easy protocol integration.
Supported Languages:
- JavaScript/TypeScript: For web3 dApps
- Solidity: Smart contract integration helpers
- Python: For backend services
- Go: For high-performance services
SDK Features:
- Proof verification helpers
- Requirements management
- Access control integration
- Error handling and retry logic
- TypeScript type definitions
Documentation:
- Integration guides
- Code examples
- Best practices
- API reference
Current State: Credentials support age, jurisdiction, and accreditation status.
Future Attributes:
- Income Level: Privacy-preserving income verification
- Residency Status: Proof of residency without revealing address
- Professional Licenses: Verify professional credentials
- Credit Score Ranges: Creditworthiness without exact score
- Custom Attributes: Protocol-specific requirements
Implementation Approach:
- Extensible circuit design
- Backward compatibility with existing credentials
- Migration path for existing credentials
- Clear documentation for new attributes
Phase 1 (Q1 2025):
- Multi-party trusted setup ceremony
- Circuit optimizations
- Mobile app MVP
Phase 2 (Q2 2025):
- Browser extension
- Multi-chain deployment (Ethereum, Polygon)
- SDK development
Phase 3 (Q3 2025):
- Batch verification
- Credential expiration
- Analytics dashboard
Phase 4 (Q4 2025):
- Recursive proofs research
- Additional credential attributes
- Delegated proof generation
We welcome contributions! Areas where help is especially needed:
- Circuit Optimization: Help reduce constraint count and improve proving performance
- Multi-Chain Deployment: Deploy and test on different chains
- Documentation: Improve guides and add examples
- Testing: Expand test coverage and add integration tests
- Security Audits: Review code and suggest improvements
See our Contributing Guidelines (to be added) for more information.
MIT