Privacy Features (SDK)

Obelysk provides four privacy primitives through the SDK. Each uses ElGamal encryption on the STARK curve to hide amounts while maintaining on-chain verifiability through Schnorr proofs and Merkle trees.

🏊
Privacy Pools
Deposit into shielded sets with Merkle proofs. Break the link between deposit and withdrawal.
πŸ”
Confidential Transfer
Peer-to-peer encrypted amounts. ElGamal ciphertexts + 6 Schnorr proofs per transfer.
πŸ‘»
Stealth Payments
One-time ephemeral addresses via ECDH. Only the recipient knows they received funds.
πŸŒ‘
Dark Pool Trading
Commit-reveal batch auctions. Uniform clearing price, zero front-running, zero MEV.

Privacy Pools

Privacy pools let you deposit tokens into a shielded set and withdraw later without linking the two transactions. Each deposit generates a note β€” a cryptographic receipt containing your secret and nullifier.

Save Your Note!

The note is the only way to withdraw your funds. If you lose it, your tokens are permanently locked. There is no recovery mechanism. Store it encrypted, backed up, and offline.

Deposit

const note = await obelysk.privacyPool.deposit({
  token: 'eth',
  amount: '1.0',
});

// CRITICAL: Save this note securely
console.log('Note secret:', note.secret);
console.log('Note nullifier:', note.nullifier);
console.log('Leaf index:', note.leafIndex);
What Happens On-Chain

Your deposit creates a Pedersen commitment (C = amountΒ·G + blindingΒ·H) and inserts it into a LeanIMT Merkle tree (depth 20). The commitment hides the amount while the Merkle tree enables efficient membership proofs.

Withdraw

const result = await obelysk.privacyPool.withdraw({
  token: 'eth',
  amount: '1.0',
  secret: note.secret,
  nullifier: note.nullifier,
  leafIndex: note.leafIndex,
  recipient: '0xYOUR_ADDRESS',
});

console.log('Withdrawal tx:', result.transactionHash);

The withdrawal generates a Merkle inclusion proof against the pool's commitment tree and a range proof to verify the amount β€” without revealing which deposit is being spent.

Pool Stats

const stats = await obelysk.privacyPool.getPoolStats('eth');
console.log('Total deposits:', stats.totalDeposits);
console.log('Total withdrawn:', stats.totalWithdrawn);
console.log('Current TVL:', stats.totalDeposited - stats.totalWithdrawn);

// Check if a nullifier has been used (double-spend prevention)
const spent = await obelysk.privacyPool.isNullifierUsed('eth', note.nullifier);

Confidential Transfer

Send tokens peer-to-peer with encrypted amounts. The contract only ever sees ElGamal ciphertexts β€” never plaintext values. The sender proves sufficient balance using Schnorr proofs, and the recipient decrypts with their ElGamal key pair.

Proof System

Each confidential transfer requires 6 zero-knowledge proofs verified on-chain: ownership proof, blinding proof, encryption proof (Chaum-Pedersen), range proof (32-bit bounds), and two same-encryption proofs ensuring sender/receiver/auditor ciphertexts encrypt the same value.

Fund β†’ Transfer β†’ Withdraw

// 1. Fund your encrypted balance (public β†’ private)
await obelysk.confidentialTransfer.fund({
  token: 'sage',
  amount: '5000',
});

// 2. Transfer to another address (private β†’ private)
//    Amount is encrypted on-chain. Only sender + recipient can see it.
await obelysk.confidentialTransfer.transfer({
  token: 'sage',
  amount: '1000',
  recipient: '0xRECIPIENT_ADDRESS',
});

// 3. Withdraw back to plaintext balance (private β†’ public)
await obelysk.confidentialTransfer.withdraw({
  token: 'sage',
  amount: '2000',
});

Read Encrypted Balances

// Get encrypted balance (returns ElGamal ciphertext)
const balance = await obelysk.confidentialTransfer.getEncryptedBalance(
  '0xADDRESS',
  'sage'
);
console.log('Ciphertext:', balance);

// Get someone's public key
const pubkey = await obelysk.confidentialTransfer.getPublicKey('0xADDRESS');

Stealth Payments

Stealth payments let you send tokens to a recipient without anyone else knowing who received them. The recipient publishes a meta-address once, and senders derive a fresh one-time address for each payment.

1
Recipient registers a meta-address
A meta-address consists of a spending public key and viewing public key. Published once, used for all incoming payments.
2
Sender derives a stealth address
Using ECDH key derivation: shared_secret = ECDH(ephemeral_sk, viewing_pubkey). The stealth address is derived from this shared secret.
3
Payment is announced on-chain
The announcement contains the ephemeral public key and a view tag (first byte of shared secret) for fast scanning.
4
Recipient scans and claims
The recipient uses their viewing key to scan announcements. View tags filter 99.6% of irrelevant announcements. Matching payments are claimed with a Schnorr proof.

Send a Stealth Payment

const result = await obelysk.stealth.send({
  to: '0xRECIPIENT_ADDRESS',
  token: 'sage',
  amount: '1000',
});

console.log('Stealth address:', result.stealthAddress);
console.log('Ephemeral key:', result.ephemeralPubKey);

Scan and Claim (Recipient)

// Scan for payments to your meta-address
const announcements = await obelysk.stealth.scan({
  metaAddress: myMetaAddress,
  fromBlock: 100000,
});

console.log('Found payments:', announcements.length);

// Claim a specific payment
for (const ann of announcements) {
  await obelysk.stealth.claim({
    announcement: ann,
    recipient: '0xYOUR_WALLET',
  });
}
View Tags for Performance

View tags are the first byte of the ECDH shared secret. They let the recipient filter out 255/256 (99.6%) of irrelevant announcements without performing full ECDH, making scanning fast even with millions of announcements.


Dark Pool

The dark pool uses a commit-reveal batch auction with a 3-block lifecycle. Orders are encrypted during the commit phase, revealed when the phase changes, and matched at a uniform clearing price. This prevents front-running, sandwich attacks, and MEV extraction.

Epoch Lifecycle

1
Block N: COMMIT
Traders submit sealed orders. Only the order hash and amount commitment are visible on-chain. Price, quantity, and direction are hidden.
2
Block N+1: REVEAL
Traders open their commitments by revealing price, amount, and salt. The contract verifies each reveal matches its commit hash.
3
Block N+2: SETTLE
Permissionless on-chain settlement. All orders are matched at a single uniform clearing price per pair. Filled traders claim their proceeds.

Full Trading Flow

// 1. Deposit tokens into the dark pool
await obelysk.darkPool.deposit({
  token: 'sage',
  amount: '10000',
});

// 2. Check the current epoch and phase
const epoch = await obelysk.darkPool.getEpochInfo();
console.log(`Epoch ${epoch.epochNumber}: ${epoch.phase}`);

// 3. Commit an order (during COMMIT phase)
const order = await obelysk.darkPool.commitOrder({
  pair: 'SAGE/STRK',
  side: 'sell',
  price: '0.011',
  amount: '5000',
});

// 4. Reveal the order (during REVEAL phase)
await obelysk.darkPool.revealOrder({
  commitment: order.commitment,
  pair: 'SAGE/STRK',
  side: 'sell',
  price: '0.011',
  amount: '5000',
  salt: order.salt,
});

// 5. Claim fills after settlement
await obelysk.darkPool.claimFill({
  epochNumber: epoch.epochNumber,
  pair: 'SAGE/STRK',
});
Supported Trading Pairs

5 assets registered (ETH, STRK, USDC, wBTC, SAGE) across 5 trading pairs: ETH/STRK, ETH/USDC, wBTC/ETH, wBTC/STRK, wBTC/USDC. A 30bps fee applies to matched trades.

Privacy Guarantees

PropertyDuring CommitDuring RevealAfter Settlement
Order priceHiddenVisibleVisible
Order amountHiddenVisibleVisible
Trader identityHidden (relayer)Hidden (relayer)Hidden (relayer)
Front-running possible?NoNoN/A
MEV extractable?NoNoNo

Which Privacy Primitive Should I Use?

NeedUseWhy
Break deposit/withdrawal linkPrivacy PoolsMerkle tree anonymity set
Send encrypted amounts P2PConfidential TransferElGamal homomorphic encryption
Pay someone without revealing recipientStealth PaymentsOne-time ephemeral addresses
Trade without front-runningDark PoolCommit-reveal batch auctions
Maximum unlinkabilityVM31 UTXO VaultFixed denominations, entire pool is anonymity set
Private AMM swapShielded SwapEkubo sees router, not your address

Next Steps