Skip to main content

Access Proof Circuit

The Access Proof Circuit (accessProof.circom, 93 LOC) is the core data access circuit — the circuit that makes Specter a data protocol, not just a token mixer. It proves that the prover knows a commitment preimage and that the commitment exists in the Merkle tree, without computing a nullifier and without spending anything.

This is pure authentication. The same proof can be generated indefinitely. The committed data is never consumed, never moved, never altered. The prover simply demonstrates: "I own this commitment."

Design Philosophy

The Redemption Circuit is designed for one-time operations: withdraw tokens, spend the nullifier, done. But most data access patterns are not one-time. A credential needs to be verified repeatedly. An API key needs to be authenticated on every request. An encryption key needs to be proven without being revealed, over and over.

The Access Proof Circuit strips away everything that makes the Redemption Circuit one-time:

FeatureRedemption CircuitAccess Proof Circuit
Nullifier computationYes (one-time spend)No (repeatable)
Amount fieldsYes (withdraw + change)No (no value)
Recipient bindingYes (prevents front-running)No (session-bound instead)
Policy bindingYes (policyId + params)No (pure data access)
Anti-replay mechanismNullifier (permanent)accessTag (session-scoped)
Can be used again?No (nullifier spent)Yes (indefinitely)

What remains is the minimum necessary for cryptographic proof of data ownership: commitment verification, Merkle membership, and a session-bound tag for anti-replay.

Public Inputs

The Access Proof Circuit has 4 public inputs:

#InputTypeDescription
0rootField elementThe Merkle tree root being proven against. Must match a known on-chain root.
1dataHashField elementHash of the sealed data. Binds the proof to specific data content.
2sessionNonceField elementA random nonce generated for this access session. Prevents proof reuse across sessions.
3accessTagField elementDerived tag: Poseidon2(nullifierSecret,  sessionNonce)\text{Poseidon}_2(\text{nullifierSecret},\; \text{sessionNonce}). Recorded on-chain for anti-replay.

Private Inputs

The circuit has 5 private inputs:

InputTypeDescription
secretField elementThe user's secret, part of the commitment preimage.
nullifierSecretField elementUsed to derive the accessTag (but NOT to compute a nullifier).
blindingField elementRandom blinding factor for the commitment.
pathElements[20]Field element[20]Sibling hashes along the Merkle proof path.
pathIndices[20]Bit[20]Left/right direction bits for each level of the tree.

Constraint Walkthrough

The circuit has three steps — significantly simpler than the seven-step Redemption Circuit.

Step 1: Compute Commitment from Preimage

The commitment is a 4-input Poseidon hash:

commitment=Poseidon4(secret,  nullifierSecret,  dataHash,  blinding)\text{commitment} = \text{Poseidon}_4(\text{secret},\; \text{nullifierSecret},\; \text{dataHash},\; \text{blinding})

Note the structural difference from the Redemption Circuit's 7-input commitment. The Access Proof commitment includes dataHash directly (instead of tokenId and amount) and omits policy fields. This makes the commitment data-agnostic — the dataHash can represent any piece of data: a credential, an image hash, an API key, an encryption key, or any other arbitrary value.

Step 2: Verify Merkle Tree Membership

Identical to the Redemption Circuit. The MerkleTreeChecker(20) sub-circuit walks 20 levels of the tree, verifying that the computed commitment is a leaf at the claimed root. The prover supplies the sibling hashes and direction bits; the circuit recomputes the root and constrains it to match the public input.

Step 3: Compute and Verify accessTag

The anti-replay mechanism. Instead of a nullifier (which would make the proof one-time), the circuit computes a session-bound tag:

accessTag=Poseidon2(nullifierSecret,  sessionNonce)\text{accessTag} = \text{Poseidon}_2(\text{nullifierSecret},\; \text{sessionNonce})

The circuit constrains this computed value to equal the public input accessTag.

Why this works for anti-replay: the sessionNonce is a fresh random value generated by the verifier (or the protocol) for each access session. The prover must include this nonce in their proof, binding the proof to a specific session. The on-chain contract records the accessTag to prevent the same proof from being replayed within the same session.

Why this does not consume the commitment: the accessTag is derived from nullifierSecret and sessionNonce, not from the commitment or leaf index. A new session with a new nonce produces a new accessTag. The commitment remains in the tree, unspent, available for future access proofs.

Key Difference: No Nullifier

This is the critical design decision. In the Redemption Circuit, the nullifier is:

nullifier=Poseidon2 ⁣(Poseidon2(nullifierSecret,  commitment),  leafIndex)\text{nullifier} = \text{Poseidon}_2\!\big(\text{Poseidon}_2(\text{nullifierSecret},\; \text{commitment}),\; \text{leafIndex}\big)

This value is deterministic — it will be the same every time for the same commitment. Once recorded on-chain, the commitment is permanently spent.

The Access Proof Circuit deliberately omits this computation. There is no leaf index reconstruction, no nullifier derivation, no nullifier check. The commitment cannot be "accidentally spent" by using the Access Proof Circuit. The two circuits operate on entirely separate planes: Redemption consumes; Access Proof authenticates.

Use Cases

The Access Proof Circuit enables Specter's data protocol capabilities:

Use CasedataHash ContainsAccess Pattern
Credential verificationHash of credential payloadVerify on each login, indefinitely
API key authenticationHash of API key materialAuthenticate on each request
Image provenanceHash of original imageProve ownership without revealing the image
Encryption key proofHash of encryption keyProve you hold the key without exposing it
Phantom IdentityHash of identity materialAuthenticate as a persistent pseudonymous identity
Sealed document accessHash of document contentProve document ownership for access control

Circuit Instantiation

component main {public [root, dataHash, sessionNonce, accessTag]}
= AccessProof(20);

The tree depth of 20 matches the Redemption Circuit, supporting ~1 million commitments. Both circuits operate against the same Merkle tree infrastructure, though the Open Ghost system maintains a separate OpenCommitmentTree for data-only commitments.

Relationship to Phantom Identity

Phantom Identity extends the Access Proof Circuit into a full identity system. A Phantom Identity is a persistent pseudonym backed by a commitment in the tree. The holder generates Access Proofs repeatedly to authenticate as that identity — proving they know the secrets without ever revealing them or consuming the commitment.

The accessTag mechanism provides anti-replay per session while allowing unlimited future sessions. This is the foundation that enables Specter to function as a persistent data privacy protocol, not just a one-time reveal system.