# Anti-Sybil Mechanisms

Preventing Sybil attacks, where a single entity creates multiple fake identities to manipulate a system or claim disproportionate rewards, is a critical requirement for secure agent ecosystems. If an AI agent cannot trust that the identities it interacts with are unique humans, the entire economic model collapses: airdrops get farmed, governance votes get manipulated, and reward pools get drained.

zkMe implements robust, privacy-preserving anti-Sybil mechanisms directly within its zero-knowledge circuits, ensuring uniqueness without compromising user anonymity.

## Nullifier Generation and Uniqueness

To enforce a strict 1:1 binding between a human and an action, and to prevent zero-knowledge proof replay attacks, the zkMe Credential System utilizes cryptographic nullifiers.

A nullifier is a deterministic, one-way hash generated inside the zero-knowledge circuit. It serves as a unique, anonymous identifier for a specific user in a specific context. The same user interacting with the same session will always produce the same nullifier, but no one can reverse-engineer the user’s identity from the nullifier value.

### The Nullifier Flow

```mermaid
sequenceDiagram
    participant V as Verifier / AI Agent
    participant H as Holder (SSI Wallet)
    participant C as ZK Circuit
    participant SC as Smart Contract
    participant R as Nullifier Registry

    V->>H: Request proof with nullifierSessionID
    Note over V: e.g. "Airdrop-2026-Q1"
    H->>C: Generate proof (identity + credential + sessionID)
    C->>C: Hash(userKey, credential, verifierID, sessionID)
    C-->>H: Proof + nullifier (public signal)
    H->>SC: Submit proof on-chain
    SC->>SC: Verify: if sessionID != 0, nullifier must != 0
    SC->>R: Check: usedNullifiers[nullifier]?
    alt Nullifier is new
        R-->>SC: false (not used)
        SC->>R: Mark usedNullifiers[nullifier] = true
        SC-->>V: Verification success
    else Nullifier already used
        R-->>SC: true (duplicate)
        SC-->>V: Reject (duplicate identity)
    end
```

The flow operates through five distinct stages:

1. **Session Binding:** When an AI agent or a decentralized application (Verifier) requests a proof from a user, they provide a unique `nullifierSessionID`. This ID defines the scope of the uniqueness check (e.g., “Airdrop Claim 2026” or “Governance Vote #42”). The session ID is set by the backend when configuring the ZKP request via `setZkpRequest`.
2. **Circuit Computation:** The zero-knowledge circuit processes the request using the `NULLIFY` operator (Operator Code 17). The circuit takes the user’s core identity data (their private key or derived ID) and hashes it together with the `nullifierSessionID`, the credential data, and the verifier’s identity.
3. **Deterministic Output:** The circuit outputs this hash as a public signal (`nullifier`). This value is mathematically unique to the specific combination of the user’s underlying identity, the specific credential being used, the Verifier requesting the proof, and the `nullifierSessionID`. The same inputs will always produce the same output, but different inputs (even slightly different) will produce completely different outputs.
4. **On-Chain Validation:** The smart contract performs an initial integrity check. If `nullifierSessionID != 0`, the contract mandates that the resulting `nullifier` must also be non-zero. This prevents a circuit from bypassing the uniqueness mechanism by outputting a zero nullifier:

   ```solidity
   function _checkNullify(uint256 nullifier, uint256 nullifierSessionID) internal pure {
       require(nullifierSessionID == 0 || nullifier != 0, "Invalid nullify pub signal");
   }
   ```
5. **Registry Enforcement:** The business logic contract (or the AI agent’s backend) maintains a registry of used nullifiers. By checking the submitted nullifier against this registry, the system can instantly detect and reject duplicate attempts, ensuring “one person, one vote” or “one person, one claim” without ever learning the user’s actual identity:

   ```solidity
   mapping(uint256 => bool) public usedNullifiers;

   function claimReward(uint256 nullifier, bytes calldata proof) external {
       require(!usedNullifiers[nullifier], "Already claimed");
       // ... verify ZK proof ...
       usedNullifiers[nullifier] = true;
       // ... distribute reward ...
   }
   ```

{% hint style="info" %}
**Important:** The core verification circuit outputs the nullifier as a public signal and validates the relationship between the nullifier and the session ID, but it does not store the uniqueness registry itself. The registry must be maintained by the business logic layer (either on-chain in a mapping or off-chain in a database). This separation of concerns allows different applications to define their own uniqueness scopes without modifying the circuit.
{% endhint %}

***

## Unified Authentication (BJJ and ETH Identity)

To generate a valid zero-knowledge proof and a valid nullifier, the system must authenticate the user, proving that the person generating the proof actually controls the identity bound to the credential. Historically, ZK identity systems required users to manage specialized BabyJubJub (BJJ) cryptographic keys, which are optimized for efficient verification inside zero-knowledge circuits but created significant friction for onboarding standard Web3 users who already have Ethereum wallets.

The latest circuit architecture introduces a unified authentication mechanism that supports both key types through a single circuit, significantly simplifying the user experience for agent-driven applications.

### How It Works

The system uses a flag called `isBJJAuthEnabled` to select the authentication path:

| Authentication Mode | Flag Value              | Authentication Method                                      | Identity Derivation                                                                                          | Use Case                                                                                                                   |
| ------------------- | ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| BJJ                 | `isBJJAuthEnabled == 1` | BabyJubJub EdDSA signature                                 | Identity derived from BJJ key pair; verified against the Global Identity State Tree (GIST) root              | High-security scenarios requiring dedicated identity keys, such as institutional custody or multi-party computation setups |
| ETH                 | `isBJJAuthEnabled == 0` | Standard Ethereum wallet signature (e.g., MetaMask, Rabby) | Identity mathematically derived from the sender’s Ethereum address via `GenesisUtils.calcIdFromEthAddress()` | Standard user onboarding, consumer-facing dApps, and AI agent interactions where minimizing friction is the priority       |

The contract-level logic that enforces this dual-path authentication is straightforward:

```solidity
if (pubSignals.isBJJAuthEnabled == 1) {
    // BJJ path: verify against Global Identity State Tree
    _checkGistRoot(pubSignals.userID, pubSignals.gistRoot, state);
} else {
    // ETH path: derive identity directly from Ethereum address
    _checkAuth(pubSignals.userID, sender);
    // Internally verifies: userID == calcIdFromEthAddress(idType, ethIdentityOwner)
}
```

### Why This Matters for the Agent Economy

The ETH authentication path eliminates the need for users to manage separate, specialized cryptographic keys just to interact with privacy-preserving agents. The frontend SDK automatically detects the user’s connected wallet type and sets the `isBJJAuthEnabled` flag accordingly, making the entire process transparent to the end user.

This is particularly important for AI agent onboarding flows. When an agent needs to verify a user’s credential, the user simply signs with their existing Ethereum wallet. There is no additional key generation step, no seed phrase to back up, and no specialized wallet software to install. This allows seamless onboarding of millions of existing EVM users into the zkMe ecosystem while maintaining strict cryptographic proof of ownership and enabling robust anti-Sybil protections.

The BJJ path remains available for advanced use cases where dedicated identity keys provide additional security guarantees, such as institutional wallets that separate signing authority from identity authority, or hardware security module (HSM) integrations where the BJJ key is stored in tamper-resistant hardware.

***

## SIG/MTP Unified Circuit

Further optimizing the verification process, the circuit architecture combines two fundamentally different proof verification methods, Signature-based (SIG) and Merkle Tree Proof-based (MTP), into a single unified circuit.

### Background: Two Proof Types

When an Issuer creates a credential for a Holder, the credential’s validity can be established in two ways:

<table><thead><tr><th width="112.91015625">Proof Type</th><th width="246.970703125">Identifier</th><th>How It Works</th><th>Strengths</th></tr></thead><tbody><tr><td><strong>BJJ Signature</strong></td><td><code>BJJSignature2021</code></td><td>The Issuer signs the credential with their BabyJubJub private key. The ZK circuit verifies this signature inside the proof.</td><td>Immediate availability (no on-chain transaction needed after issuance). Lower latency for the Holder.</td></tr><tr><td><strong>Merkle Tree Proof</strong></td><td><code>Iden3SparseMerkleTreeProof</code></td><td>The credential is added as a leaf to the Issuer’s on-chain Sparse Merkle Tree. The ZK circuit verifies a Merkle inclusion proof.</td><td>More gas-efficient for on-chain verification. Stronger auditability because the credential’s existence is anchored in the Issuer’s published state root.</td></tr></tbody></table>

In previous architectures, these two proof types required separate circuits and separate validator contracts. A developer building an AI agent had to implement two different verification paths and manage the routing logic between them.

### Unified Approach

The current architecture merges both paths into a single circuit. The system uses a `proofType` field as a public input to select the appropriate verification path at proof generation time:

```solidity
// On-chain validation ensures the proof type matches the request
_checkProofType(credAtomicQuery.proofType, pubSignals.proofType);
```

The SDK automatically selects the appropriate proof type based on what is available for the credential. If the Verifier does not explicitly specify a `proofType` (by setting it to `0`), the SDK will automatically utilize the available proof, **prioritizing MTP** when both are available. This prioritization reflects the fact that MTP proofs are generally more gas-efficient for on-chain verification scenarios, because the Merkle root is already anchored on-chain and does not require the verifier contract to perform an in-circuit signature verification.

### Developer Impact

This unification simplifies integration for developers building AI agents in two concrete ways. First, a single validator contract handles both proof types, so the agent’s backend does not need conditional routing logic. Second, existing verifiable credentials do not need to be re-issued when switching proof types; only the zero-knowledge proof needs to be regenerated against the unified circuit. The credential data itself remains unchanged.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.zk.me/hub/how-built/credential-sys/anti-sybil-mech.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
