how it works
A private transaction in KIRITE passes through three stages: deposit, mix, and withdraw. Each stage strips a different layer of metadata from the transaction.
step 1: deposit
The user deposits assets into the Shield Pool. Before the transaction is broadcast:
- The client generates a Twisted ElGamal ciphertext of the deposit amount.
- A range proof is computed to prove the amount is non-negative and within bounds, without revealing the value.
- The encrypted amount + proof are submitted to the Solana program.
- The on-chain program verifies the proof and accepts the deposit.
The chain never sees the deposit amount. It only verifies a mathematical proof that the amount is valid. The encryption happens entirely in the user's browser.
// client-side encryption (simplified)
const ciphertext = twistedElGamal.encrypt(
amount, // plaintext amount
publicKey, // recipient's public key
randomness // fresh randomness per transaction
);
const rangeProof = bulletproofs.prove(
amount,
randomness,
{ bits: 64 } // prove amount fits in u64
);
// only ciphertext + proof go on-chain
await program.deposit(ciphertext, rangeProof);step 2: mix
Inside the Shield Pool, deposits become indistinguishable. The pool operates as a multi-asset anonymity set:
- All deposits of the same token type are pooled together.
- Deposits are time-locked — they cannot be withdrawn immediately, preventing timing correlation attacks.
- The lock window is variable — randomized per deposit to prevent fixed-interval analysis.
- As the pool grows, the anonymity set expands, making individual transactions harder to trace.
anonymity set dynamics
The privacy guarantee of the Shield Pool is directly proportional to the size of the anonymity set. For a pool with n deposits of similar value:
privacy_score = 1 - (1/n)
n = 10 → 90% unlinkability
n = 100 → 99% unlinkability
n = 1000 → 99.9% unlinkabilityconfigurable privacy levels
Users choose their own privacy-speed tradeoff at withdrawal:
| level | delay | privacy | use case |
|---|---|---|---|
| instant | ~0s | low | speed over privacy |
| standard | ~10 min | medium | recommended for most users |
| maximum | 1 hr+ | highest | large amounts, maximum anonymity |
Longer delays allow more deposits to enter the pool, expanding the anonymity set and making timing correlation exponentially harder.
step 3: withdraw
When the selected privacy window expires, the user can withdraw to a stealth address:
- The user generates an ephemeral keypair — a fresh, one-time address that has never appeared on-chain.
- A withdrawal proof is generated, proving the user has a valid deposit without revealing which one.
- Assets are transferred to the stealth address.
- The recipient scans the on-chain registry to detect and claim the funds using their private key.
// stealth address generation (simplified)
const ephemeral = Keypair.generate();
const sharedSecret = ecdh(ephemeral.secretKey, recipient.publicKey);
const stealthPubkey = deriveStealthAddress(
recipient.publicKey,
sharedSecret
);
// recipient scanning
const detected = scanRegistry(recipient.secretKey);
// → returns stealth addresses the recipient can claimend-to-end flow
what an observer sees
| metadata | without kirite | with kirite |
|---|---|---|
| transaction amount | fully visible | encrypted (ciphertext only) |
| sender address | fully visible | one of n pool depositors |
| recipient address | fully visible | one-time stealth address |
| sender-receiver link | deterministic | broken by pool mixing |
| timing correlation | exact timestamp | randomized time-lock |