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

# Confidential token flow

> The register, deposit, confidential_transfer, merge, and withdraw operations, plus auditor disclosure, and exactly what is public vs hidden at each step.

Confiroll pays with an OpenZeppelin-style **confidential token** on Soroban. A balance has two
internal buckets: **receiving** (incoming, not yet merged) and **spendable** (merged, ready
to send/withdraw). Amounts live in commitments and encrypted event fields, not in plaintext
arguments.

The client generates proofs with **Noir** (witness solving) and **bb.js `UltraHonk`** with a
**keccak** Fiat-Shamir transcript, which is mandatory because the on-chain verifier expects
keccak. Only `register`, `confidential_transfer`, and `withdraw` carry proofs; `deposit` and
`merge` do not.

## The lifecycle at a glance

```mermaid theme={"system"}
flowchart LR
    A[register] --> B[deposit]
    B --> C[confidential_transfer]
    C --> D[merge]
    D --> E[withdraw]
    C -.public events.-> F[auditor disclosure]
    classDef pub fill:#fde8e8,stroke:#e57373,color:#7a1f1f;
    classDef hid fill:#e7f0ff,stroke:#5b78e6,color:#1b2a63;
    class B,E pub;
    class C,D hid;
```

## What's public vs hidden

| Operation               | Proof | Public on-chain                                                 | Hidden            | Secrets needed                  |
| ----------------------- | ----- | --------------------------------------------------------------- | ----------------- | ------------------------------- |
| `register`              | Yes   | `Y`, `PVK`, `addr_f`, `auditor_id`                              | `sk`              | CT `sk` + ed25519               |
| `deposit`               | No    | **the deposited amount** (plain `i128`), `from`, `to`           | None              | ed25519                         |
| `confidential_transfer` | Yes   | `from`, `to`, encrypted event fields                            | **the amount**    | sender CT `sk` + sender ed25519 |
| `merge`                 | No    | `account` only                                                  | amounts           | ed25519                         |
| `withdraw`              | Yes   | **the withdrawn amount** (plain `i128`), remaining stays hidden | remaining balance | CT `sk` + ed25519               |

<Warning>
  **Two amounts are public by design.** The **total you deposit** is visible on-chain (deposit
  takes a plaintext amount), and the **specific amount you withdraw** is re-revealed on exit. So
  Confiroll hides the **per-recipient split** of a payroll and each contractor's ongoing
  balance, not the aggregate you funded or the amount someone cashes out. State this precisely;
  never imply the totals are hidden.
</Warning>

## Step by step

<Steps>
  <Step title="register(account, auditor_id, data)">
    Proves knowledge of `sk` such that `Y = sk * H` and `PVK = vk * H`, and publishes the
    account's public keys plus the `auditor_id` it registers under. A recipient **must**
    register before they can be paid. The sender needs the recipient's public `PVK` to
    encrypt to. Opening balance is the identity point (zero).
  </Step>

  <Step title="deposit(from, to, amount)">
    Moves **public** USDC into the confidential token. No proof; the `amount` is a plaintext
    argument, so the deposited total is visible. Credits the *receiving* bucket. In a payroll
    run the employer deposits the batch total (plus a little headroom) up front.
  </Step>

  <Step title="confidential_transfer(from, to, data)">
    The payout. There is **no amount argument**. The value lives only inside the proof and the
    encrypted event. The proof enforces sender balance conservation, encrypts the amount to the
    recipient via ECDH against their `PVK`, and writes **two auditor ciphertext channels** (a
    recipient channel and a sender channel) that the circuit forces to agree. The recipient
    needs nothing at pay time; they decrypt later from the event with their viewing key.
  </Step>

  <Step title="merge(account)">
    Folds the *receiving* bucket into *spendable* by point addition. No proof, no amounts in
    the event, just the account. Required before freshly received funds can be spent or
    withdrawn.
  </Step>

  <Step title="withdraw(from, to, amount, data)">
    Converts a confidential balance back to public USDC. The withdrawn `amount` is a plaintext
    argument (re-revealed by design, so the USDC is verifiable); the **remaining** balance is
    re-blinded and stays confidential. The recipient ends holding ordinary USDC. There is no
    in-app fiat off-ramp.
  </Step>
</Steps>

## Proving

Three of the five operations carry a zero-knowledge proof. The proving pipeline is the same for
each, and the transcript choice is not optional:

<Steps>
  <Step title="Noir solves the witness">
    The circuit is written in Noir. Given your private inputs (`sk`, the amount, blindings) and
    the public inputs, Noir solves the witness: it computes every intermediate value the circuit
    constrains.
  </Step>

  <Step title="bb.js generates the UltraHonk proof">
    The `bb.js` prover (WASM, running in the browser) takes the solved witness and produces an
    `UltraHonk` proof. This is the step that needs `sk`, and it runs entirely on your device.
  </Step>

  <Step title="Keccak transcript is mandatory">
    The Fiat-Shamir transcript uses **keccak**. This is required, not a preference: the
    Nethermind Soroban verifier that checks the proof on-chain expects a keccak transcript, so a
    proof built with any other hash is rejected.
  </Step>
</Steps>

<Info>
  Only `register`, `confidential_transfer`, and `withdraw` carry proofs. `deposit` and `merge`
  carry no proof: `deposit` takes a plaintext amount and needs no secret knowledge, and `merge`
  is a public point addition of your own buckets. This is why the two proof-free operations sit
  outside the proving pipeline entirely.
</Info>

## Auditor / escrow disclosure

Every `confidential_transfer` bakes in an encrypted copy readable only by the holder of a
single **escrow scalar `k`** (registered on-chain as `K_aud = k * H` under an `auditor_id`).
Disclosure is a pure **read + decrypt**: it signs nothing and submits no transaction:

* ECDH between `k` and the event's public ephemeral point recovers the masking value, then
  subtracts it from the ciphertext to reveal the amount.
* The **sender** channel yields the amount plus the sender's post-transfer balance; the
  **recipient** channel yields the amount plus its blinding. The two channels cross-check to
  the same value, and because the circuit constrains both, the ciphertexts can't be omitted or
  forged.

### Two auditor channels, cross-checked

The dual channels are what make disclosure trustworthy. A single ciphertext could be wrong or
missing without anyone noticing; two constrained channels cannot:

| Channel           | Encrypted to                | Yields on decrypt                               | Purpose                                    |
| ----------------- | --------------------------- | ----------------------------------------------- | ------------------------------------------ |
| Recipient channel | the recipient's viewing key | the amount + its blinding                       | Lets the recipient read what they received |
| Sender channel    | the escrow key `K_aud`      | the amount + the sender's post-transfer balance | Lets the escrow holder audit the payout    |

The circuit **forces both channels to encode the same amount**. Because both are constrained
inside the proof, the sender cannot omit a channel, cannot write two channels that disagree,
and cannot fake a value. When the escrow holder decrypts, the sender channel and the recipient
channel resolve to one number. If they did not agree, the proof would not have verified, so a
verified transfer is one whose auditor channels are present and consistent.

<Info>
  Who holds `k` decides who can audit. In the custody model the **employer** holds the escrow
  key for their own payroll, so Confiroll stays blind while the employer can still disclose to an
  auditor. Disclosure runs on the employer's device against public event data. On the testnet
  preview this decryption runs in the CLI tooling (`pnpm auditor`), and the API route
  `POST /auditor/disclose` returns `501`. See
  [The non-custodial model](/developers/non-custodial-model).
</Info>

## What each operation reveals

A one-line recap you can hold in your head:

| Operation               | Reveals on-chain                         | Keeps hidden                 |
| ----------------------- | ---------------------------------------- | ---------------------------- |
| `register`              | your public keys and chosen `auditor_id` | `sk`                         |
| `deposit`               | the deposited total, and the parties     | nothing to hide here         |
| `confidential_transfer` | who paid whom, and when                  | the amount and your balances |
| `merge`                 | that an account merged its buckets       | all amounts                  |
| `withdraw`              | the withdrawn amount, and the parties    | the remaining balance        |

The two rows that reveal an amount are `deposit` and `withdraw`, and both do so by design so the
public USDC is verifiable. Everything in between hides the per-recipient split.

## FAQ

<AccordionGroup>
  <Accordion title="Can I skip register and just receive a payment?">
    No. A recipient must `register` first. Registration publishes the recipient's public viewing
    key `PVK`, and the sender needs that `PVK` to encrypt the amount to the recipient inside the
    proof. Without it there is no key to encrypt to, so the transfer cannot be built. This is the
    onboarding step a contractor completes before a first payout.
  </Accordion>

  <Accordion title="Why do deposit and withdraw expose an amount but transfer does not?">
    `deposit` and `withdraw` are the boundary between public USDC and the confidential balance,
    and the public USDC leg has to be verifiable, so those amounts are plaintext by design.
    `confidential_transfer` moves value entirely inside the confidential token, so it carries no
    amount argument at all: the value lives in the proof and encrypted event fields. Hiding the
    per-recipient split is exactly the transfer step.
  </Accordion>

  <Accordion title="What stops a sender from writing a fake or missing auditor channel?">
    The circuit. Both the recipient channel and the sender channel are constrained inside the
    proof to encode the same amount, so a transfer that omits a channel or writes disagreeing
    channels does not produce a valid proof and never verifies on-chain. A verified transfer is
    one whose two auditor channels are present and cross-check to a single value.
  </Accordion>
</AccordionGroup>
