> ## Documentation Index
> Fetch the complete documentation index at: https://docs.writz.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Privacy edition (in progress)

> **Status: design in progress, testnet only, not part of Writz v1.** Writz v1 (Writz Lend on `bitcoin-spv` and `writz-lend`) has no privacy layer: positions, amounts and collateral are public on chain. Nothing on this page protects a Writz Lend position today.

Privacy is not part of the Writz protocol. It is being designed as a separate, opt-in edition: its own contracts (today `commitment-tree`, `zk-verifier` and the Circom circuits, all under `privacy/` in the repository), its own lending pool, its own audit and its own trusted-setup ceremony. It is planned for after v1 is live on mainnet, on its own subdomain, and has no date; it is not on the v1 mainnet plan. Users who do not want privacy never interact with it, and `writz-lend` never calls these contracts.

The rest of this page records the current design of that edition, as built and deployed on testnet. Zero-knowledge proofs would let the edition's contracts verify facts about a position (that the loan is adequately collateralized, that the deposit is real, that a repayment is correct) without learning the underlying numbers.

***

## Zero-Knowledge Proofs in Plain English

A zero-knowledge proof lets you prove something is true without revealing anything else.

The classic analogy: imagine you want to prove to someone that you know the solution to a Sudoku puzzle, without revealing the solution. A ZK proof is a mathematical protocol that makes this possible - you can demonstrate you know the answer, and the verifier becomes convinced, without ever seeing the answer.

In the privacy edition, the "fact" being proved is: *"I have a deposit commitment in this Merkle tree, my loan amount is X% or less of my collateral value, and I have the secret that unlocks this commitment."*

The ZK proof convinces the Soroban contract that all of this is true - without the contract ever learning the deposit amount, the loan amount, or anything else about the position.

***

## What Is Hidden, What Is Visible

| Information | Visibility |
| - | - |
| That a commitment exists in the tree | Public (commitment hash stored on-chain) |
| The collateral amount | **Private - never on-chain** |
| The loan amount | **Public** - the USDC transfer and the borrow/repay events show it (see below) |
| The health factor | **Private - never on-chain** |
| The user's public key linked to a position | **Private** |
| The depositor's Stellar address and the Bitcoin deposit transaction | **Public** - the deposit event links them, and the deposit transaction is public |
| That a liquidation occurred | Public (event emitted) |
| Who was liquidated | **Private** (only a nullifier is revealed, not a user identity) |
| How much debt was repaid in a liquidation | Public - the liquidator must know the amount to pay it. The published amount is provably correct (bound to the private commitment via a circuit constraint, not caller-supplied), but it is not hidden. |
| Total TVL (aggregate) | Public |
| Total USDC outstanding (aggregate) | Public |

The protocol sees only commitments and nullifiers - opaque hashes that prove actions occurred without revealing the substance of those actions.

**What the ZK layer does not hide:** the commitment-tree contract publishes the depositor's address with its Bitcoin transaction id, and the borrow and repay events and USDC transfers carry the Stellar address and exact amount. Anyone reading the ledger can link a Stellar address to a Bitcoin deposit and see loan sizes. Hiding these would take different transfer rails, not event changes, so this is a known limit of the current design.

***

## The Commitment Scheme

At the heart of the ZK layer is a **commitment**. A commitment is a hash of private data:

```
liq_key    = Poseidon(secret)
commitment = Poseidon(collateral, debt, liq_key, nonce, timelock_height)
```

Where:

* `collateral` is the BTC deposit amount in satoshis (private)
* `debt` is the USDC owed, in stroops (private)
* `secret` is derived from the user's wallet signature and never leaves the device (private)
* `liq_key` is enough to liquidate the position, not to borrow, repay or release it (private; also encrypted to the protocol keeper, see below)
* `nonce` rotates on every change, so the new commitment cannot be linked to the old one (private)
* `timelock_height` is the CLTV height of the deposit's Bitcoin script (private after deposit)

**Poseidon** is a ZK-friendly hash function - designed specifically to be efficient inside ZK circuits. It is collision-resistant and hiding: given only the commitment, computing the preimage is computationally infeasible.

**Keeper notes.** Every borrow and repay proof also encrypts the new position's `liq_key`, nonce and amounts to the protocol keeper's key, inside the circuit, and the contract publishes the ciphertext in the event. Only the keeper can open it, and what it learns lets it liquidate an undercollateralized position without the owner, never move the owner's funds. Observers learn nothing from it.

The commitment is stored on-chain in a Merkle tree. The preimage stays with the user.

***

## The Nullifier

When a commitment is "consumed" (used in a borrow, repay, or withdraw), a **nullifier** is published:

```
nullifier = Poseidon(liq_key, nonce)
```

The nullifier is stored on-chain in a spent-nullifier set. It proves this commitment has been used - preventing double-spending - without revealing which commitment it corresponds to or what amount it represents.

This is the same technique used by Tornado Cash, Aztec Network, and other ZK privacy protocols: the nullifier is the receipt that something happened, but it is unlinkable to the original commitment without the position's private keys.

***

## The ZK Circuits

The privacy edition uses five Circom circuits (deposit, borrow/repay, liquidation, zero-debt release and Merkle insertion), each compiled to a Groth16 proof system over the BN254 curve. The constraint counts below are measured with `circom --O2`.

### 1. Deposit Circuit

**What it proves:** "I know (collateral, secret, nonce) such that `commitment = Poseidon(collateral, 0, Poseidon(secret), nonce, timelock_height)`, the collateral equals what the SPV-verified transaction paid, and the transaction ID matches my claimed deposit."

**Public inputs:**

* The commitment hash and nullifier (stored on-chain)
* The `txid` returned by the SPV contract (binding the ZK proof to a specific Bitcoin transaction)
* The minimum deposit and the real amount the contract parsed from the transaction
* `timelock_height`, which the contract checks against the deposit's Bitcoin script (#212)

**Private inputs:**

* Collateral (satoshis)
* Secret
* Nonce

**Constraint count:** 855

**Why the txid binding matters:** Without this binding, a malicious user could submit a ZK deposit proof that references a Bitcoin transaction that doesn't actually exist. The txid binding ensures that every ZK commitment corresponds to a real, SPV-verified Bitcoin deposit.

***

### 2. Borrow/Repay Circuit

**What it proves for borrow:** "I know the preimage of a commitment in the Merkle tree, the Oracle price P, and a loan amount L such that: `L / (amount × P) ≤ 1/min_ratio` (the loan is within the allowed LTV)."

**What it proves for repay:** "I know the preimage of a commitment in the Merkle tree, and the repay amount correctly reduces the outstanding debt."

**Public inputs:**

* The Merkle root (current state of the commitment tree)
* The oracle price
* The minimum collateral ratio (150%)
* A nullifier for the old state
* A new commitment for the updated state (after borrow or repay)
* The recipient binding
* `min_timelock_height`: a borrow proves the position's timelock is at least this, and the contract requires it to be at least the Bitcoin tip plus a 30-day margin (#212)
* The keeper's public key and the keeper note: the new position's liquidation witness, encrypted to that key inside the proof (#212)

**Private inputs:**

* Collateral, secret, nonces and timelock (commitment preimage)
* Current debt
* Delta (borrow or repay amount)
* Merkle path (proof of membership in the tree)
* A fresh ephemeral key for the keeper note

**Constraint count:** 15,410

**Key design decision - no division in ZK circuits:** Division is expensive in ZK (requires a range proof for the denominator). The collateral ratio check is implemented as a multiplication: `loan_amount × min_ratio_bp ≤ collateral_amount × price × 10000`. This avoids division entirely.

**Field negation for repay:** The repay amount is encoded as a field element negation - `p − delta_stroops` where `p` is the BN254 field prime. The circuit recovers the repay amount correctly. This was verified on-chain.

**`is_borrow` soundness fix:** earlier versions of this circuit left `is_borrow` unconstrained - nothing forced it to be `0` or `1`, and nothing tied it to the actual sign of `delta_stroops`. A prover could submit `is_borrow=0` (skipping the collateral-ratio check entirely, which is only gated on `is_borrow=1`) while still passing a positive `delta_stroops` that increases debt with zero ratio enforcement. The only thing that prevented this from being exploitable end-to-end was that `commitment-tree::repay`'s Rust-side decode of `delta_stroops` (field-negation, described above) happens to overflow `i128` for a non-negative delta - a property of that one caller, not a guarantee the circuit itself made. The circuit now directly constrains `is_borrow` to be boolean and requires `new_debt >= old_debt` when `is_borrow=1`, `new_debt <= old_debt` when `is_borrow=0` - see `privacy/circuits/src/borrow_repay.circom`, Step 3b, and the negative tests in `privacy/circuits/test/borrow_repay.test.js`.

***

### 3. Liquidation Circuit

**What it proves:** "I know the preimage of a commitment in the Merkle tree, and the position's current health ratio is below 120% at the current oracle price."

The witness is the position's `liq_key`, not its secret, so the keeper can prove it from the keeper note without the owner (#212).

**Public inputs:**

* The Merkle root
* The oracle price
* The usdc\_debt (the debt amount - extracted from the proof so the liquidator cannot inflate it)
* The liquidation threshold (120%)

**Private inputs:**

* Collateral, `liq_key`, nonce and timelock (commitment preimage)
* Current debt
* Merkle path

**Constraint count:** 5,699

**Critical security property - `usdc_debt` binding:** The liquidation circuit's `usdc_debt` public output is computed inside the circuit from the private `debt_stroops` field. The liquidator cannot supply an arbitrary debt amount - it is derived from the private commitment and constrained by the circuit. This prevents a malicious keeper from claiming a larger debt than actually exists.

***

### 4. Zero-Debt Release Circuit

Proves a leaf in the tree encodes zero debt, without revealing its collateral or keys. It publishes the leaf's `commitment` and `nullifier`: `commitment-tree::mark_released` spends the nullifier so the leaf can never borrow again, and `/api/cosign` only co-signs the BTC release once that is recorded.

***

### 5. Insertion Circuit

Proves that a new Merkle root is the current root with one commitment written into the next empty leaf. The relayer generates it from public data when a deposit is inserted, and `commitment-tree::insert_commitment` accepts the proof instead of a root, so neither the admin key nor a relayer bug can install a tree with invented or missing leaves (#211).

***

## Groth16 and Protocol X-Ray

All five circuits use **Groth16** - the most widely deployed ZK proof system, used by Zcash, Tornado Cash, and Stellar's own Private Payments reference implementation.

A Groth16 proof is a small constant-size object (3 elliptic curve points, \~128 bytes on BN254) that can be verified in constant time - regardless of the size of the computation being proven.

Verification uses **BN254 pairing checks**: elliptic curve operations that confirm the relationship between the proof and the verification key. These are computationally expensive but deterministic.

Stellar's **Protocol X-Ray** (Protocol 26) added native host functions for BN254 operations:

* `bn254_g1_msm` - multi-scalar multiplication on the G1 curve
* `bn254_pairing_check` - pairing check across multiple pairs

The `zk-verifier` contract uses these host functions to run the Groth16 verification:

```
verify = e(A, B) == e(vk.alpha, vk.beta) × e(vk_x, vk.gamma) × e(C, vk.delta)
```

This is a 4-pair pairing check. Using Protocol 26 host functions, it costs approximately 15–20M instructions - within budget for a Soroban transaction.

***

## The Merkle Commitment Tree

All commitments are stored in a Poseidon Merkle tree with depth 20 - supporting up to 1,048,576 (2²⁰) positions.

The tree root is stored on-chain. When a new commitment is inserted, the root is updated. Every ZK proof that references the tree must match the current root - ensuring ZK proofs are valid only against the current state of the protocol.

**Sparse tree:** The tree uses a sparse representation. Only occupied leaves require storage. Empty subtrees are represented by a precomputed empty-subtree hash.

**On-chain initial root:** `0x2134e76ac74b4b8765b6e37992aa15f06ff... ` (Poseidon-2 empty tree root - verified on-chain on Stellar testnet)

***

## The Trusted Setup Ceremony

Groth16 requires a one-time **trusted setup ceremony** - a multi-party computation event that generates the proving and verification keys for each circuit.

The ceremony has two phases:

1. **Powers of Tau (Phase 1):** A universal setup shared across all circuits. The edition is planned to use the Hermez ceremony artifact (the same trusted setup used by Stellar's own Private Payments reference implementation).
2. **Phase 2 (circuit-specific):** A separate ceremony for each of the edition's five circuits, incorporating the circuit-specific parameters.

The ceremony requires multiple independent participants. The security guarantee is: as long as at least one participant honestly discards their randomness, the setup is sound. A ceremony with 5+ parties from different organizations and jurisdictions provides strong security.

The ceremony transcript is published publicly. Anyone can verify it was executed correctly.

**When:** The ceremony belongs to the privacy edition and has no date. It is not on the Writz v1 mainnet path. The current development keys (`pot15` from snarkjs) are used for testing only - they are NOT suitable for production.

**Rotating the on-chain key once the real ceremony completes:** `zk-verifier.set_verification_key` (admin-only) replaces a circuit's key immediately - there is no grace period during which the previous key is also accepted. That's a deliberate choice: accepting proofs against a stale key is a strictly weaker security posture than requiring the current one. What the contract does provide is an audit trail - every call emits a `vk_rotated` event carrying a fingerprint of the old and new key, so there's an on-chain record of exactly when the dev key was replaced by the real ceremony's output, even though storage itself only ever holds the current key.

***

## What Gets Stored On-Chain

The ZK layer is designed to minimize on-chain storage while preventing double-spending and enabling verifiable state.

| Data | Storage type | Lifetime |
| - | - | - |
| Commitment (leaf hash) | Per-entry persistent | 180-day window (refreshable) |
| Nullifier (spent marker) | Per-entry persistent | 180-day window (refreshable) |
| Merkle root | Instance persistent | 180-day window (refreshable) |
| Verification keys | Instance persistent | Permanent |

**TTL management:** Soroban's storage has a time-to-live (TTL) system. Entries that are not accessed expire. The edition's contracts provide permissionless `refresh_*` functions that any keeper can call to extend the TTL of critical entries - ensuring user positions never expire unexpectedly.

***

**Back to v1:** [The Stellar Side →](https://docs.writz.xyz/how-it-works/stellar-side.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.