Share
## https://sploitus.com/exploit?id=AECD405E-97C0-50FA-BD41-7673DAB158A7
# Bounty #295: Why `ownPublicKey()` Can't Be Trusted for Access Control

**A Comprehensive Tutorial on ZK Circuit Access Control Vulnerabilities in Midnight Compact Smart Contracts**

---

## Table of Contents

1. [Executive Summary](#executive-summary)
2. [The Vulnerability: A One-Sentence Explanation](#the-vulnerability-a-one-sentence-explanation)
3. [Prerequisite Knowledge](#prerequisite-knowledge)
4. [How ZK Circuits Handle Identity](#how-zk-circuits-handle-identity)
5. [The Four-Step Attack](#the-four-step-attack)
6. [Technical Root Cause Analysis](#technical-root-cause-analysis)
7. [The Secure Alternative: Witness + persistentHash](#the-secure-alternative-witness--persistenthash)
8. [Code Walkthrough: Vulnerable Contract](#code-walkthrough-vulnerable-contract)
9. [Code Walkthrough: Secure Contract](#code-walkthrough-secure-contract)
10. [Security Development Checklist](#security-development-checklist)
11. [Running the Proof of Concept](#running-the-proof-of-concept)
12. [Frequently Asked Questions](#frequently-asked-questions)
13. [Further Reading](#further-reading)

---

## Executive Summary

This tutorial examines **Bounty #295**, a critical security vulnerability in Midnight blockchain Compact smart contracts involving the misuse of `ownPublicKey()` for access control. The core finding is stark and simple: **`ownPublicKey()` in Zero-Knowledge circuits compiles to an unconstrained private input (witness) that the prover can arbitrarily set.** Any access control decision based on this value is completely bypassable.

We will walk through the technical details of why this happens, demonstrate a full four-step attack with proof-of-concept code, and then present the correct design pattern using witness secret keys combined with `persistentHash` commitments. By the end of this tutorial, you will understand the fundamental principle of ZK access control: **prove what you know, not who you claim to be.**

---

## The Vulnerability: A One-Sentence Explanation

> `ownPublicKey()` in a ZK circuit is an **unconstrained witness** β€” the prover can provide **any value** for it, and the circuit has **no mechanism** to verify that the prover possesses the corresponding private key.

This means that an attacker who observes a single legitimate transaction can extract the administrator's public key (which is public information on the blockchain) and then reuse it in their own proof to impersonate the administrator. The circuit will accept the attack trivially because it only checks byte equality between two values, never proving ownership.

---

## Prerequisite Knowledge

Before diving into the vulnerability, ensure you understand these concepts:

- **Zero-Knowledge Proofs (ZKPs)**: A cryptographic method where one party (the prover) can prove to another party (the verifier) that a statement is true without revealing any information beyond the validity of the statement itself.
- **Witness**: In a ZK circuit, a witness is a private input known only to the prover. The circuit can impose constraints on witness values, but the prover initially chooses them.
- **Compact Language**: Midnight's smart contract language that compiles to ZK circuits. Contracts define circuits that execute within zero-knowledge proofs.
- **`kernel.self()`**: A Compact built-in that returns the contract's own address/identity. In the ledger execution context, this refers to the actual deployed contract. **In the ZK circuit context, this is a witness value.**
- **`persistentHash`**: A Compact standard library function that computes a SHA-256 hash bound to the specific contract deployment, suitable for on-chain commitments.
- **Commitment Scheme**: A cryptographic primitive where one party commits to a value (by publishing a hash) and later reveals the value. The hash binds the committer without revealing the value.

---

## How ZK Circuits Handle Identity

To understand the vulnerability, we must first understand how ZK circuits process identity information.

### The ZK Circuit Model

A ZK circuit $C$ takes two types of inputs:

1. **Public inputs ($x$)**: Values visible to both prover and verifier. These are constrained by the circuit's public parameters.
2. **Private inputs / witnesses ($w$)**: Values known only to the prover. The prover chooses these values, and the circuit imposes constraints on them.

The prover's goal is to convince the verifier that:
$$ \exists w: C(x, w) = \text{true} $$

The critical insight is that **the prover chooses $w$ freely, subject only to the constraints in $C$**. If the circuit does not properly constrain $w$ to correspond to some real-world property (like ownership of a private key), then the prover can choose any $w$ that satisfies the circuit.

### What Happens with `ownPublicKey()`

When a Compact contract uses `kernel.self()` (the compiled form of `ownPublicKey()`) inside a circuit, the compiler treats it as a witness value. The circuit is compiled with an input slot for "the contract's own public key," and this slot is **unconstrained** β€” nothing in the circuit logic ties it to the actual on-chain identity of the deployed contract.

In pseudocode, the circuit looks like this:

```
circuit vulnerableWithdraw(
    private_input claimedPubKey: Bytes,   // ← kernel.self() β†’ unconstrained
    private_input storedAdminPubKey: Bytes,
    private_input amount: Uint
) β†’ Uint:
    assert(claimedPubKey == storedAdminPubKey)
    return amount
```

The prover can set `claimedPubKey` to any 32-byte value. If they set it to equal `storedAdminPubKey` (which is public), the assertion passes. The circuit has **no constraint** linking `claimedPubKey` to the actual transaction submitter's identity.

---

## The Four-Step Attack

The attack against a contract using `ownPublicKey()` for access control consists of four straightforward steps. No special privileges, no secret knowledge, and no advanced cryptographicη ΄θ§£ techniques are required.

### Step 1: Observe a Legitimate Transaction

The attacker monitors the blockchain for any transaction submitted by the legitimate administrator to the target contract. From this transaction, they extract the administrator's public key. On a public blockchain, all transaction data is publicly visible. The public key may appear as:
- A parameter to the circuit call
- An emitted event from a previous transaction
- On-chain state that was initialized during deployment

**What the attacker gains**: A 32-byte (or 33-byte) public key value. This is public information available to anyone who can read the blockchain.

### Step 2: Construct a Malicious Witness

The attacker takes the extracted public key and constructs their own proof locally. In the ZK circuit, they set the `claimedPubKey` witness (the value corresponding to `kernel.self()`) to be exactly the administrator's public key.

**What the attacker does**: No cryptography, no private key operation, no hacking β€” they simply copy-paste the public key bytes into their proof.

### Step 3: Submit the Malicious Transaction

The attacker submits their transaction to the network. The circuit verifier checks the proof:

```
assert(claimedPubKey == storedAdminPubKey)
```

Both values are the administrator's public key, so the assertion passes. The circuit has no way to know that the prover is not the legitimate administrator because it never checked ownership of the corresponding private key.

**What the attacker gains**: Full access to any functionality protected by this access control check.

### Step 4: Execute Unauthorized Operations

With the access control check bypassed, the attacker can now execute any operation that was supposed to be restricted to the administrator. In our vulnerable vault contract, this means withdrawing all funds.

**What the attacker gains**: All assets and privileges protected by the broken access control.

### Summary Table

| Step | Action | Knowledge Required | Difficulty |
|------|--------|-------------------|------------|
| 1 | Observe a legitimate transaction | Blockchain access | Trivial |
| 2 | Copy the admin's public key | Public key value | Trivial |
| 3 | Submit a proof with the copied key | Ability to submit a transaction | Trivial |
| 4 | Execute unauthorized operations | None | Trivial |

---

## Technical Root Cause Analysis

### Why This Vulnerability Exists

The vulnerability stems from a fundamental misconception about how ZK circuits handle identity. Developers familiar with traditional blockchain systems (like Ethereum) are accustomed to `msg.sender` being a **trusted, verifiable identity** provided by the execution environment. In Ethereum, `msg.sender` is set by the protocol based on the ECDSA signature verification β€” the system verifies that the transaction was signed by the private key corresponding to the claimed address.

In ZK-based systems like Midnight, the situation is fundamentally different:

1. **The prover controls the circuit inputs**: In a ZK proof, the prover generates the proof locally. They choose all witness values. There is no "execution environment" that injects trusted identity information.

2. **Identity must be proven, not stated**: For a ZK circuit to verify identity, it must include constraints that prove knowledge of a secret (like a private key or a pre-image of a hash). Simply stating "I am address X" is meaningless because the prover can always state any address.

3. **`kernel.self()` is a placeholder, not a constraint**: When the Compact compiler compiles `kernel.self()` in a circuit, it creates an input slot for the contract's identity. But unlike `msg.sender` in Ethereum, there are no protocol-level constraints linking this value to an actual on-chain identity. The compiler **cannot** enforce that the prover is who they claim to be because that would require a signature verification or similar cryptographic constraint, which is not automatically added.

### The Mathematical Perspective

Let us formalize the vulnerable circuit constraint:

$$ C_{\text{vuln}}(x, w) := (w_{\text{pk}} == x_{\text{stored}}) $$

Where:
- $w_{\text{pk}}$ is the claimed public key (witness, prover-chosen)
- $x_{\text{stored}}$ is the stored admin public key (public, known to attacker)

The prover needs to find a witness $w$ such that $C_{\text{vuln}}(x, w) = \text{true}$. Since $C_{\text{vuln}}$ only checks equality, and $x_{\text{stored}}$ is public, the prover simply sets $w_{\text{pk}} = x_{\text{stored}}$. This requires zero knowledge β€” the attacker does not even need to know the private key corresponding to $x_{\text{stored}}$.

Now contrast with the secure circuit constraint:

$$ C_{\text{secure}}(x, w) := (\text{SHA256}(w_{\text{secret}}) == x_{\text{commitment}}) $$

Where:
- $w_{\text{secret}}$ is the secret key (witness, prover-chosen)
- $x_{\text{commitment}}$ is the on-chain SHA-256 commitment (public)

The prover needs to find a witness $w$ such that $C_{\text{secure}}(x, w) = \text{true}$. Due to the pre-image resistance of SHA-256, the prover cannot compute $w_{\text{secret}}$ from $x_{\text{commitment}}$. The only way to satisfy the constraint is to **know** the original secret key that was used to create the commitment. This is a genuine knowledge proof.

### Common Misconceptions

**Misconception 1: "The blockchain verifies the transaction signature, so the prover's identity is already authenticated."**

The blockchain verifies that the transaction was signed by the submitter's key, but this is a separate concern from the circuit's internal logic. The circuit does not automatically know which key signed the transaction. `kernel.self()` is not connected to the transaction signature in the circuit constraints.

**Misconception 2: "But kernel.self() returns the correct value when I test it locally."**

`kernel.self()` can return the correct value in ledger simulation contexts because the simulator injects the actual contract address. However, in the standalone ZK circuit (which is what matters for security), the prover controls all witness values. The circuit constraints, not the simulator behavior, determine the actual security properties.

**Misconception 3: "The public key is secret β€” only the admin knows it."**

Public keys are, by definition, **public**. They are designed to be shared. On a blockchain, they are visible in every transaction. Security cannot rely on the secrecy of public keys.

---

## The Secure Alternative: Witness + persistentHash

The correct approach to access control in Midnight Compact contracts uses a combination of **witness secret keys** and **persistentHash commitments**.

### Design Principles

1. **Store commitments, not secrets**: The contract's on-chain state stores only `persistentHash(secretKey)` β€” a 32-byte SHA-256 hash. This commitment binds the contract to a specific secret without revealing it.

2. **Prove knowledge, not identity**: The prover passes the secret key as a witness to the circuit. The circuit verifies `persistentHash(secretKey) == storedCommitment`. This proves the prover **knows** the secret, which is the correct use of ZK proofs.

3. **Hash binding**: SHA-256's pre-image resistance ensures that seeing the commitment reveals nothing about the secret. Even with unlimited computational resources, finding a pre-image is infeasible.

### Security Guarantees

The secure pattern provides:

- **Soundness**: A malicious prover who does not know the secret cannot produce a valid proof. The only way to satisfy `persistentHash(secret) == commitment` is to know a pre-image of `commitment`.
- **Zero-knowledge**: The proof reveals that the prover knows a pre-image of the commitment, but reveals nothing about the pre-image itself.
- **Non-repudiation**: Once a proof is accepted, the prover cannot deny knowledge of the secret (though in practice, the secret could be shared).
- **Forward secrecy**: Even if the commitment is revealed (it is on-chain by design), the secret remains hidden.

### Comparison: Vulnerable vs. Secure

| Aspect | Vulnerable (`ownPublicKey()`) | Secure (witness + `persistentHash`) |
|--------|------------------------------|-------------------------------------|
| Input type | Unconstrained private input | Constrained witness |
| On-chain data | Raw public key (secrecy fails) | SHA-256 hash (no leakage) |
| Verification | Byte equality (trivial bypass) | Hash verification (cryptographic) |
| Prover proves | "I claim this identity" | "I know this secret" |
| Attack difficulty | Zero (public key is public) | Infeasible (SHA-256 pre-image) |
| ZK-appropriate | No β€” identity cannot be assumed | Yes β€” knowledge is provable |

---

## Code Walkthrough: Vulnerable Contract

The vulnerable contract (`contracts/vulnerable-auth.compact`) simulates a protected vault where the administrator authenticates using an `ownPublicKey()` equivalent pattern.

```compact
export circuit vulnerableWithdraw(
    claimedPubKey: Bytes,    // ← Simulates ownPublicKey() β€” UNCONSTRAINED
    storedAdminPubKey: Bytes, // ← On-chain admin public key
    amount: Uint
): Uint {
    // Only checks byte equality β€” NEVER verifies ownership
    assert(claimedPubKey == storedAdminPubKey);
    return disclose(amount);
}
```

**Why this is vulnerable**: The `claimedPubKey` parameter represents what `kernel.self()` would return in a ZK circuit β€” a value the prover supplies without constraint. The circuit only checks `claimedPubKey == storedAdminPubKey`. Since both values are known to the attacker (the admin's public key is on-chain), this check is trivially bypassed.

**What an attacker does**: The attacker observes the admin's public key from any legitimate transaction, then submits their own transaction with that same public key as `claimedPubKey`. The equality check passes, and the attacker gains admin privileges.

---

## Code Walkthrough: Secure Contract

The secure contract (`contracts/secure-auth.compact`) implements the correct pattern using witness secret keys and `persistentHash` commitments.

```compact
export circuit initialize(secretKey: Bytes): Bytes {
    const commitment: Bytes = persistentHash(secretKey);
    return disclose(commitment);
}

export circuit secureWithdraw(
    secretKey: Bytes,          // ← Constrained witness
    storedCommitment: Bytes,   // ← On-chain hash commitment
    amount: Uint
): Uint {
    const computedCommitment: Bytes = persistentHash(secretKey);
    assert(computedCommitment == storedCommitment);
    return disclose(amount);
}
```

**Why this is secure**: The `secretKey` parameter is a genuine constrained witness. The circuit verifies `persistentHash(secretKey) == storedCommitment`. Due to SHA-256's pre-image resistance, the only way to satisfy this constraint is to know the original secret key. The on-chain state reveals only a hash, which does not help an attacker.

**What an attacker faces**: The attacker sees `storedCommitment = SHA-256(adminSecret)` on-chain. To forge a proof, they would need to find a value `x` such that `SHA-256(x) == storedCommitment`. This is a pre-image attack on SHA-256, which is computationally infeasible with current technology.

---

## Security Development Checklist

When writing Midnight Compact contracts, use this checklist to avoid access control vulnerabilities:

### Access Control Design

- [ ] **NEVER** use `kernel.self()`, `ownPublicKey()`, or any unconstrained identity value for access control decisions.
- [ ] **ALWAYS** use witness parameters that the prover must KNOW (secrets, pre-images) combined with on-chain commitments.
- [ ] **VERIFY** that every access control constraint actually binds the witness to required knowledge, not just to equality with another value.

### Circuit Input Analysis

- [ ] For every private input (witness), ask: "What prevents the prover from setting this to an arbitrary value?"
- [ ] If the answer is "nothing" or "the circuit checks equality with another unconstrained value," the design is broken.
- [ ] If the answer involves cryptographic primitives (hash verification, signature verification, Merkle proof verification), the design is likely sound.

### On-Chain State

- [ ] Store only **commitments** (hashes) of sensitive values, never raw secrets or raw public keys used for access control.
- [ ] Use `persistentHash()` for commitment creation β€” it binds the hash to the specific contract deployment, preventing replay across contracts.
- [ ] Ensure that on-chain state does not leak information that would help an attacker bypass constraints.

### Testing and Verification

- [ ] Test your circuit with **adversarial witness values** (arbitrary public keys, arbitrary identities) to verify that access control cannot be bypassed.
- [ ] Use formal verification tools if available to check circuit constraints.
- [ ] Assume all witness values are attacker-controlled β€” design your constraints accordingly.

### Mental Model

- **Traditional blockchain (Ethereum)**: `msg.sender` is trustworthy because the protocol verifies the signature. You can use it for access control.
- **ZK-based blockchain (Midnight)**: Witness values are prover-chosen. You must add explicit constraints to verify any claim. You cannot trust any value that is not constrained.

---

## Running the Proof of Concept

### Prerequisites

- Node.js 18+
- npm or yarn

### Setup

```bash
cd ~/Work/bounties/295-ownpublickey
npm install
```

### Execute the Attack PoC

```bash
npx tsx src/attack-poc.ts
```

The PoC script runs through four phases:

1. **Contract Initialization**: Sets up the administrator's public key and secret key. Initializes both the vulnerable and secure contracts.
2. **Legitimate Transaction**: Demonstrates that the administrator can withdraw funds from both contracts using their credentials.
3. **Attack Demonstration (Core)**: Attacker Mallory observes the admin's public key and attempts to drain funds. The vulnerable contract is completely bypassed. The secure contract withstands the attack.
4. **Summary**: A structured comparison of the two approaches with key lessons.

### Expected Output

The attack phase output clearly shows the vulnerability:

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ATTACK on [Vulnerable Contract - ownPublicKey()]       β”‚
β”‚  Method: Replay the admin's public key as the witness   β”‚
β”‚  Knowledge required: NONE β€” just the public key bytes   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  βœ… Public key MATCHES! Verification PASSED

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  🚨  ATTACK SUCCEEDED!                                  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  🚨 Mallory withdrew 999 tokens from the vulnerable contract!
```

Followed by:

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ATTEMPT on [Secure Contract - witness + persistentHash]β”‚
β”‚  Method: Try to reverse-engineer the secret key         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  ❌ Secret key hash does not match commitment β€” FAILED

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  βœ… SECURE CONTRACT WITHSTOOD THE ATTACK                β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

---

## Frequently Asked Questions

### Q: Is `kernel.self()` always unsafe?

No. `kernel.self()` is safe to use for:
- Computing token types (`tokenType(domain, kernel.self())`) β€” this just needs a unique identifier for the contract, not access control.
- Emitting events that identify the contract.
- Any purpose where the value is informational rather than authoritative.

It is **unsafe** to use for:
- Access control decisions: "Only if `kernel.self() == adminPubKey`."
- Identity verification: "The caller must be the contract itself."
- Any purpose where the value is trusted to represent the caller's identity.

### Q: What about `kernel.caller` or similar constructs?

Midnight's Compact language does not expose a direct "caller identity" in the circuit context. All identity information must be explicitly provided and constrained by the developer. There is no equivalent to Ethereum's `msg.sender` that is automatically trustworthy.

### Q: Can I use public key cryptography in ZK circuits?

Yes, but you must add explicit signature verification constraints. The circuit can verify that a signature is valid for a given public key and message. However, this is more computationally expensive than the `persistentHash` approach and requires careful implementation.

### Q: What if I need multi-user access control?

The `persistentHash` pattern scales to multiple users by storing multiple commitments, each corresponding to a different user's secret key. The circuit checks if the provided secret matches any of the stored commitments.

### Q: Can I update the admin secret after deployment?

Yes. Create a circuit that takes the old secret (to prove authorization) and a new secret, verifies the old secret's commitment, and updates the commitment to `persistentHash(newSecret)`.

### Q: Is SHA-256 (persistentHash) quantum-safe?

No. SHA-256 is vulnerable to Grover's algorithm, which would reduce the effective security from 256 bits to 128 bits. For most applications, 128 bits of post-quantum security is still sufficient, but if you are concerned about long-term quantum threats, consider using quantum-resistant hash functions or signature schemes when they become available in Compact.

---

## Further Reading

- [Midnight Official Documentation β€” Compact Language Reference](https://docs.midnight.network/compact/reference/compact-reference)
- [Midnight Developer Guides β€” Smart Contract Security Best Practices](https://docs.midnight.network/developers/guides/security)
- [Bounty #293 β€” Compact Standard Library Tutorial](../293-compact-stdlib/) β€” Detailed usage of `persistentHash`, `persistentCommit`, and other standard library functions
- [NIST SP 800-107 β€” Recommendation for Applications Using Approved Hash Algorithms](https://csrc.nist.gov/publications/detail/sp/800-107/rev-1/final)
- [Zero-Knowledge Proof Basics](https://zkp.science/)
- [SHA-256 Cryptanalysis β€” Current Status](https://en.wikipedia.org/wiki/SHA-2#Cryptanalysis_and_validation)

---

## Conclusion

The `ownPublicKey()` vulnerability is a classic example of a fundamental mismatch between developer expectations and ZK circuit semantics. Developers accustomed to traditional blockchain environments assume that identity information is automatically trustworthy. In ZK-based systems like Midnight, this assumption is dangerously wrong.

**The core lesson is simple**: In Zero-Knowledge circuits, you cannot trust any value that the prover supplies without constraint. Every witness must be bound to real-world knowledge through cryptographic constraints. For access control, the proven pattern is to use `persistentHash` commitments combined with witness secret keys β€” proving **what you know**, not **who you claim to be**.

By understanding and applying this principle, developers can build secure Midnight applications that correctly leverage the power of zero-knowledge proofs without falling into the `ownPublicKey()` trap.

---

*This tutorial is part of the Midnight Blockchain Security Research series. Bounty #295 aims to help developers understand the core security principles of ZK circuit access control.*

**License**: MIT

**Security Research Team**: Midnight Blockchain Security Research