Our work
We developed the credential, policy and proof-verification components for zkproof.space. The work covered issuer and revocation registries, wallet binding, local proof generation and the contract checks that connect eligibility to a specific transaction.
A lending pool may need to restrict access by jurisdiction, identity assurance or wallet-screening status. Publishing those inputs on a blockchain would leave a permanent record of information that users supplied for a much narrower purpose. Keeping the decision behind a private API creates a different dependency. Every protected action then relies on an online service whose policy calculation the contract cannot inspect. zkproof.space addresses this conflict by separating identity verification from the proof an EVM application needs to accept a transaction.
The architecture lets a holder prove that a specific policy is satisfied while keeping the underlying credential and provider-specific evidence outside public state. The relying contract receives a proof, a policy commitment, accepted data roots and the context of the intended action. Its decision depends on those explicit inputs. The central engineering challenge was making that decision verifiable without exposing the evidence. The second was keeping an otherwise valid proof from outliving a credential revocation, a dataset update or the transaction it was prepared for.
A reusable credential needed a stable circuit interface
Identity providers do not produce interchangeable records. Their schemas, signatures, status fields and update rules differ. Putting every provider's original payload into a proof circuit would tie circuit complexity and releases to changes outside the platform's control. zkproof.space introduces a derived credential between the upstream verification record and the proving system. It carries a bounded set of signed facts that the circuit can evaluate consistently.
The Base Compliance Credential includes assurance level, subject type, jurisdiction class, claim masks, risk flags and validity timestamps. Categorical values use bounded numeric domains rather than raw strings. An opaque source commitment connects the credential to its upstream context without placing the original case record inside the proof interface. The issuer signs a canonical commitment to these fields using an EdDSA-style signature suited to the circuit environment. EVM account signatures remain a separate key domain.
This normalization makes provider integration a credential-plane responsibility. A change in an upstream payload does not automatically require the circuit to understand a new external schema. The trade-off is an explicit trust relationship with the credential issuer, which is responsible for projecting the upstream result accurately. The circuit verifies the signed facts and their relationship to the policy. It cannot establish that the original identity check was truthful merely because the signature is valid.
Issuer acceptance is also represented as state. The circuit proves that the signing key belongs to the accepted issuerSetRoot, so the particular issuer need not become a public credential field. Policy owners can change the trusted issuer set through a new root. This separates issuer administration from the circuit's signature-verification logic and avoids hard-coding one provider key into every policy.
Wallet binding stays separate from identity verification
A credential's lifetime and a wallet's lifetime are different. A holder may add a wallet or lose access to one while the underlying identity verification remains valid. Combining both into one permanent credential would make wallet rotation unnecessarily disruptive. zkproof.space keeps a Base Compliance Credential and a Wallet Binding Attestation as separate signed objects. Their shared holder commitment connects them inside the circuit.
At enrollment, the SDK generates a high-entropy holder secret and derives a Poseidon commitment from it. The base credential contains the commitment, while the proof establishes knowledge of the secret. A wallet binding is issued after an account-control challenge and names the wallet, chain namespace, nonce and validity interval. The circuit requires its holder commitment to match the one in the base credential. A credential from one holder therefore cannot simply be paired with another holder's wallet attestation.
EOA binding uses typed structured data covering the account, chain, commitment, nonce, expiry and intended purpose. Contract-account binding follows ERC-1271 semantics. This provides separate places to manage identity validity and wallet authority, including shorter validity windows for wallet-related evidence. It also defines the limit of the claim. The construction binds a credential to an authorised account, but it does not make deliberate sharing of all secrets and account access physically impossible.
Policies had to change without a circuit per customer
Different protocols need different eligibility rules. One may require a minimum assurance level and an allowed jurisdiction, while another also requires recent wallet-risk screening. Compiling a separate circuit for every combination would multiply setup ceremonies, verifier deployments and release artifacts. We implemented ComplianceGateV1 with a bounded predicate language shared across policies. A new policy selects supported conditions and thresholds within that language.
The Policy Compiler converts the administrative definition into a deterministic field vector. That vector includes claim masks, age limits, screening requirements and replay settings. The circuit checks its hash against the public policyCommitment. The EVM Policy Registry stores the commitment expected by the integration. This connects a proof to the exact configured rules without publishing every private policy field as a separate public signal.
The bounded language is a deliberate constraint. It supports comparisons, bitmask checks, membership checks and freshness conditions rather than arbitrary executable policy code. A materially different predicate requires a circuit release. That creates a clear distinction between changing policy parameters and changing what the proving system is capable of expressing. It also gives reviewers a finite set of operations to examine.
A valid proof can still refer to obsolete evidence
Credential expiry alone cannot handle a verification that must be revoked early. Likewise, a proof generated against yesterday's sanctions dataset may be cryptographically correct while referring to state the protocol no longer accepts. zkproof.space makes the relevant roots and epoch explicit public inputs. The Compliance Gate checks them against its registries before accepting the proof for the protected action.
Credential revocation uses a sparse Merkle tree keyed by a digest of the issuer domain and credential identifier. A non-revoked credential corresponds to the default zero leaf. The circuit proves that leaf's relationship to the accepted revocationRoot without revealing the credential identifier. Sanctions-address checks use a separate sparse tree and a key derived from the dataset namespace, chain namespace and normalized address. Domain separation prevents roots for different purposes from being substituted for each other.
The resulting sanctions statement is precise. The address is absent from the committed dataset represented by the accepted root. It is not a universal statement about the wallet's history or its owner's conduct. Proprietary risk analysis has a separate signed-attestation path, including provider membership and freshness checks. Keeping these mechanisms distinct avoids treating exact address non-membership as equivalent to a broader screening judgment.
Time needs two layers of validation because a circuit cannot read the current block timestamp. Inside the proof, credential and attestation ages are checked against referenceTime, with proof validity bounded by their expiration limits. On-chain, the gate compares that reference and validUntil with block.timestamp and the configured time bounds. A prover cannot make an expired credential acceptable simply by choosing a convenient historical time. The contract supplies the time anchor that the circuit itself lacks.
| Control | Circuit | Gate |
|---|---|---|
| Policy | Predicate checks | Commitment match |
| Issuer | Key membership | Accepted root |
| Revocation | Zero leaf | Root freshness |
| Time | Relative age | Block time |
| Action | Public digest | Context match |
| Replay | Nullifier derivation | Consumption check |
Freshness creates an availability trade-off. A holder with valid credentials, snapshots and proving artifacts can continue during a temporary control-plane outage while the accepted state remains valid. Once a root exceeds the policy's maximum age, a fail-closed gate stops accepting it. Cached data therefore supports bounded continuity, not indefinite operation without updates. The architecture makes that stopping condition part of the policy instead of silently extending stale evidence.
Eligibility had to belong to one intended action
A proof submitted to a public mempool can be copied. If it establishes only generic eligibility, another caller may try to reuse it in a different context. zkproof.space binds the proof to an action digest containing the policy-defined security context. The gate reconstructs that digest from the actual call and requires it to match the proof's public input. Changing a protected parameter changes the expected digest.
The context includes the chain, gate, relying contract, subject, function and relevant call parameters, together with nonce and deadline information. The integration decides which parameters are security-relevant, such as recipient, token or amount. The architecture preserves a full 256-bit digest as two bounded limbs when representing it in the proof field. This avoids losing information through a naive reduction modulo the SNARK scalar field.
In direct-call mode, the gate checks the subject expected from the call context. A relayed action requires separate authorisation from the subject, verified through EOA signature recovery or ERC-1271 as appropriate. Validation matches the proof, signed action and nonce to the same subject and operation. Where one-time consumption is required, a domain-separated nullifier records use within the intended scope. Protocol-specific domains also avoid making the nullifier itself a shared tracking identifier across unrelated applications.
For atomic enforcement, the gate check and protected business action occur in the same transaction context. We placed eligibility validation in the protected execution path. A frontend pre-check does not substitute for that validation. Session-based approval has a separate lifecycle for its state, expiry and revocation.
Local proving keeps the witness on the holder's side
The default proving path runs locally through browser WASM, Node.js or an integrator-controlled runtime. Private credential fields and witness values stay within that runtime. Only the completed proof and designated public signals pass to the protocol. A remote prover is a separate deployment choice because it receives the private witness unless additional protection is introduced. It cannot inherit the same privacy claim simply by producing the same proof format.
Privacy also depends on how the prover obtains its supporting material. An address-specific request for a Merkle path can reveal which wallet is preparing a proof to the dataset service. We used snapshot distribution for public datasets so paths can be resolved locally. Immutable snapshots and proving artifacts are distributed through object storage and a CDN, with digest checks before use. The SDK resolves a latest-version alias to a specific immutable artifact before proving begins.
The privacy boundary continues into operational storage and logs. Control-plane records use opaque references, policy versions, epochs and artifact identifiers rather than requiring raw identity documents. Logs exclude full credentials, holder secrets, witnesses and private screening vectors. This allows operators to investigate a failed circuit version or stale root without collecting the evidence the proof system was designed to keep private. Ordinary EVM callers and transaction context remain visible, so the protected information is the eligibility evidence rather than the transaction as a whole.
Root and circuit releases are security decisions
We used Groth16 over BN254 for succinct proofs and compatibility with EVM pairing precompiles. That choice brings a circuit-specific trusted setup and makes release integrity part of the system's security model. The SDK resolves a signed manifest and checks artifact digests against the selected circuit release. A circuit identifier cannot silently be redirected to a different verification key.
The release process connects the source, proving artifacts and deployed verifier through explicit checks.
- Circuit releases pin source and compiler versions.
- The release record includes the constraint digest and setup artifacts.
- The build produces the proving key, verification key and Solidity verifier.
- Release checks use deterministic test vectors and published artifact digests.
- The immutable verifier is registered against its circuit identifier.
Dataset publication has a similar separation of responsibilities. Ingestion preserves source checksums and parser versions, normalization is isolated from tree construction and immutable snapshots record their provenance. The builder and root publisher use separate authority boundaries. An independent rebuild can compare the resulting root before publication approval, reducing reliance on a single data-processing path. On multiple EVM networks, publication and activation remain chain-local, so a global dataset epoch does not imply simultaneous finality everywhere.
These mechanisms make failures distinguishable. Operators can monitor root age, publication lag, artifact mismatches and verification failures by circuit version without interpreting every rejected action as an identity problem. Policy changes, dataset updates and circuit upgrades also retain separate lifecycles. The practical result is an architecture in which the protocol can identify which rules and evidence commitments governed a decision, while the personal evidence stays outside public execution.
zkproof.space makes eligibility verifiable by binding private evidence to a specific policy, accepted state and intended action. Its engineering value lies in keeping those bindings explicit through revocation, policy changes and execution, so a valid proof means the right conditions were satisfied for the transaction the protocol is actually processing.
See our architecture in practice.
DEVLAB · ARCHITECTURE EXAMPLE
Agent
Commerce
A look inside the software architecture behind Agent Commerce.
View architecture