Documentation

How it works

The model

Cloak Network is one contract, CloakPool, on Robinhood Chain (chain 4663). It holds USDG and a table of accounts. An account is a public key on the BN254 curve; its balance is a ciphertext under that key. Deposits and withdrawals are public. Everything in between moves as ciphertext.

The contract verifies every proof itself, using the curve precompiles at addresses 0x06 and 0x07. There is no operator, relayer, sequencer of proofs or trusted setup. If a proof is wrong the transaction reverts; if it is right the transfer is final in that block.

Balances are counted in cents. One unit is 10,000 base units of USDG, and every value the system proves fits in 32 bits, so the pool as a whole is capped at 42,949,672.95 USDG. The cap is what guarantees that no balance can ever leave the range the proofs cover.

Encryption

A balance b under the key Y = sk·G is the pair

C = b·G + r·Y
D = r·G

Adding two ciphertexts adds the amounts, so the contract can credit and debit an account without reading it. The owner computes C − sk·D = b·G and recovers b with a baby-step giant-step search over 2³² values, which takes well under a second in a browser.

Each account has two ciphertexts: bal, which only the owner's own actions change, and pending, where incoming money lands. The owner calls rollover to merge them. This is what stops anyone from invalidating a proof in flight by sending the account a payment.

A transfer

To send b, the sender encrypts it twice with the same randomness, once to each key, and proves seven relations in a single sigma protocol:

Y_s        = sk·G                    the sender holds the key
D          = r·G
C_s        = b·G + r·Y_s             the amount, for the sender
C_s − C_r  = r·(Y_s − Y_r)           the same amount, for the recipient
V_1        = b·G + ρ1·H              b is the value in range proof 1
C_L − C_s  = b'·G + sk·(C_R − D)     b' is what is left
V_2        = b'·G + ρ2·H             b' is the value in range proof 2

H is a second generator whose discrete log nobody knows: it is derived by try-and-increment from keccak256("cloak.network/H/v1"), and the test suite re-derives it. The challenge is the keccak hash of the whole transcript, bound to the chain id, the contract address, both accounts, the current balance ciphertext and the memo. A proof is valid for one state of one account on one chain, once.

Range proofs

A range proof commits to each of the 32 bits of a value as B_i = bit·G + ρ_i·H and proves, with a two-branch OR proof, that each commitment opens to 0 or 1. The contract checks every bit and rebuilds V = Σ 2^i·B_i, which the sigma protocol ties to the ciphertext. Two of them per transfer: the amount is in range, and so is the balance left.

Bit proofs are simple and need no setup. They are also large: a transfer is about 13 kB of calldata. Shorter proofs (Bulletproofs) would cut that at the price of a more complex verifier, and are the obvious next step.

Agent policies

An agent account has its own key, its own controlling address, and a policy only its creator can change: a ceiling per request, a cap per day, and an allowlist of payees. The policy is public. The agent's spending is not: the contract keeps today's total as a ciphertext S under the agent's key and adds each payment to it homomorphically.

Every agent payment proves three more relations and two more ranges:

ceiling·G − V_3  = b·G − ρ3·H        ceiling − b  is in [0, 2³²)
daily·G − S.c    = q·G − sk·S.d      q = daily − (spent today + b)
V_4              = q·G + ρ4·H        q            is in [0, 2³²)

One cent over either limit and no valid proof exists. The total resets at 00:00 UTC. An agent cannot withdraw to an address, cannot use the plain transfer, and can always pay its parent account back.

Stealth routes

To pay Y without pointing at it, the sender picks a random k, publishes R = k·G with the transfer, and pays the one-time account Y' = Y + h·G where h = keccak(k·Y). The recipient computes the same h from sk·R and holds the key sk + h. transferMany settles several such routes in one transaction, each proven against the balance the previous one leaves.

View grants

To show someone one transfer, the account holder gives them T = sk·D for that ciphertext, with a Chaum-Pedersen proof that log_G(Y) = log_D(T). The reader computes C − T = b·G and solves for b. verifyOpening on the contract confirms it. The grant opens that ciphertext and no other, and the key never leaves its owner.

x402

/api/x402/feed is a paid resource. Without payment it answers 402 with a price in cents, a Cloak account and a memo. The agent pays with agentTransfer, then retries with the transfer in an X-Payment header:

GET /api/x402/feed
← 402  { accepts: [{ scheme: "cloak-confidential", maxAmountRequired: "25",
                     payTo: { id, x, y }, memo }] }

agentTransfer(args, policyArgs)          on the pool

GET /api/x402/feed
X-Payment: base64({ tx, to, memo, cr, d })
← 200  { paid, settlement, head }

The server holds the payee key. It opens its side of the ciphertext and checks that it contains the price, to the cent. Once the pool is deployed it also looks the transaction up on chain; in the sandbox the chain lives in your tab, so the server says so in its answer instead.

Contract

FunctionWhoWhat it checks
registeranyoneSchnorr proof of the key, bound to the caller
depositanyonePulls USDG, adds the public amount to pending
rollovercontrollerMerges pending into the spendable balance
transfercontroller7 relations, 2 range proofs
transferManycontrollerSeveral transfers, all or nothing
createAgentparentKey proof bound to the agent and its policy owner
agentTransferagent10 relations, 4 range proofs, allowlist, freeze
withdrawcontroller3 relations, 1 range proof, pays out USDG
verifyOpeningviewA view grant for one ciphertext
claimHandlecontroller3 to 20 characters, a-z 0-9, unique

Token: USDG at 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168. Pool: not deployed yet. The console runs the same bytecode on an EVM inside your browser.

Measured gas

From the end-to-end test suite, 49 checks on the compiled bytecode, intrinsic and calldata gas included:

register64,702
deposit128,273
transfer2,605,291
agentTransfer5,112,708
transferMany (3 routes)8,200,792
withdraw1,283,886

What it does not do

  • It hides amounts, not participants. Which account paid which account is public unless the payment used stealth routes, and the sender is public either way.
  • Deposits and withdrawals are public amounts.
  • The contract has not been audited. Until it is, treat the pool as experimental.
  • If you lose a key, the balance under it is gone. Nobody can recover it, including the owner of the contract.