Skip to main content

Access Proofs

An Access Proof is a Groth16 zero-knowledge proof that demonstrates knowledge of a commitment preimage and Merkle tree membership without consuming the commitment. It is the cryptographic primitive that enables persistent, repeatable data access in Specter. Unlike the Redemption Proof — which spends a nullifier and permanently consumes the commitment — an Access Proof authenticates and leaves the commitment intact.

This is the mechanism that separates authentication from consumption. A user can prove "I own this data" an unlimited number of times without ever destroying the proof of ownership.

Why Access Proofs Exist

The Specter data protocol supports two fundamentally different access patterns:

PatternMechanismExample
One-time accessRedemption Proof (spends nullifier)Gift card redemption, secret sharing, token transfer
Persistent accessAccess Proof (no nullifier)Credential verification, recurring authentication, Phantom Identity

Most real-world data operations are persistent. A diploma is verified on every job application. An API key authenticates on every request. A subscription key is checked on every access. Building these patterns on top of one-time redemption would require re-committing data after every access — an expensive, complex, and fragile approach.

Access Proofs solve this cleanly: the commitment stays in the tree, and the proof can be regenerated for every session.

The Access Proof Circuit

The Access Proof Circuit (accessProof.circom) is deliberately minimal. It has 4 public inputs and 5 private inputs — roughly half the complexity of the 8-input Redemption Circuit.

Public Inputs

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

Private Inputs

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

What the Circuit Proves

The circuit enforces three constraints:

  1. Commitment computation: The prover knows values (secret, nullifierSecret, dataHash, blinding) that hash to a commitment in the tree. The commitment is computed as Poseidon4(secret,nullifierSecret,dataHash,blinding)\text{Poseidon}_4(\text{secret}, \text{nullifierSecret}, \text{dataHash}, \text{blinding}).

  2. Merkle tree membership: The computed commitment is a leaf in the Merkle tree at the given root. The circuit walks 20 levels using the provided path elements and indices, recomputing the root and constraining it to match the public input.

  3. accessTag correctness: The accessTag is correctly derived from the prover's nullifierSecret and the session's sessionNonce. This binds the proof to a specific session without revealing the nullifierSecret.

What the Circuit Does NOT Prove

The Access Proof Circuit deliberately omits:

  • Nullifier computation: no nullifier is derived, no commitment is consumed.
  • Recipient binding: no address is bound to the proof (session nonce replaces this role).
  • Value fields: no token amount, no token ID, no change commitment.
  • Policy binding: no policy ID, no policy parameters.

These omissions are the entire point. The circuit is stripped down to pure authentication — "I know the preimage of a commitment in this tree" — with nothing that would make the operation one-time or value-bearing.

Proof Generation and Verification Flow

Anti-Replay: The accessTag Mechanism

The accessTag prevents proof replay without consuming the commitment. It is a session-scoped binding that ensures each proof can only be used once.

How It Works

  1. The verifier (or the protocol) provides a fresh sessionNonce for each access session.
  2. The prover computes: accessTag=Poseidon2(nullifierSecret,  sessionNonce)\text{accessTag} = \text{Poseidon}_2(\text{nullifierSecret},\; \text{sessionNonce})
  3. The prover includes both sessionNonce and accessTag as public inputs to the ZK proof.
  4. The circuit verifies that accessTag is correctly derived from the prover's nullifierSecret and the provided sessionNonce.
  5. The on-chain contract records the accessTag in a mapping.
  6. If the same accessTag is submitted again, the transaction reverts.

Why This Is Sufficient

PropertyGuarantee
UniquenessA new sessionNonce produces a new accessTag (Poseidon collision resistance).
BindingThe accessTag is bound to the prover's nullifierSecret (ZK circuit enforces this). An attacker cannot compute a valid accessTag without knowing the secret.
Non-replayabilityEach accessTag is recorded on-chain. Submitting the same proof twice fails.
Non-consumptionThe accessTag does not involve the commitment hash or leaf index. The commitment is untouched.

accessTag vs. Nullifier

NullifieraccessTag
DerivationPoseidon2(Poseidon2(nullifierSecret,commitment),leafIndex)\text{Poseidon}_2(\text{Poseidon}_2(\text{nullifierSecret}, \text{commitment}), \text{leafIndex})Poseidon2(nullifierSecret,sessionNonce)\text{Poseidon}_2(\text{nullifierSecret}, \text{sessionNonce})
Deterministic for same commitment?Yes (always the same)No (different per session)
Effect on commitmentPermanently consumedNo effect
Reusability of underlying dataNone (one-time)Unlimited
Storage growthBounded (one per commitment)Unbounded (one per session)

The trade-off is storage growth. Nullifiers are bounded — there is at most one nullifier per commitment, so the nullifier registry grows linearly with the number of commitments. Access tags are unbounded — every session adds a new entry. In practice, the PersistentKeyVault can implement pruning strategies (e.g., expiring old access tags after a sufficient time window) to manage storage growth.

Quantum-Resistant Layer

Access Proofs in the PersistentKeyVault include an additional quantum-resistant verification layer:

  1. The stored quantumCommitment is keccak256(quantumSecret)\text{keccak256}(\text{quantumSecret}).
  2. On each access, the caller provides the quantumSecret (preimage) and a newQuantumCommitment.
  3. The vault verifies: keccak256(quantumSecret)=?stored quantumCommitment\text{keccak256}(\text{quantumSecret}) \stackrel{?}{=} \text{stored quantumCommitment}
  4. If valid, the vault replaces the stored commitment with newQuantumCommitment.

This rotation means that even if the Groth16/BN254 proving system is compromised (e.g., by a quantum computer), an attacker must also provide a valid keccak256 preimage to access the vault. keccak256 preimage resistance is believed to hold against quantum adversaries (Grover's algorithm provides at most a quadratic speedup, reducing 256-bit security to 128-bit — still computationally infeasible).

Where Access Proofs Are Used

SystemHow Access Proofs Are Used
Phantom IdentityEach "connect" generates an Access Proof to authorize split-key recombination from the PersistentKeyVault.
Persistent credential verificationA credential holder proves ownership of a committed credential on each verification request.
Recurring API authenticationAn API key holder generates an Access Proof on each API call to prove key ownership.
Subscription accessA subscriber proves commitment ownership on each content access.
Any persistent data patternAny use case where data needs to be proven repeatedly without being consumed.

Access Proofs are the foundation that makes Specter a persistent data privacy protocol. Without them, every data operation would be one-time — commit, reveal, done. With them, committed data becomes a permanent, reusable, privately-accessible resource.