Private Trading
Technical Details

Technical Details

This section provides in-depth technical information about the private trading implementation for developers and advanced users.

⚠️

Testnet Only: Private trading is currently available only on testnet. Visit testnet.hx.finance (opens in a new tab) to try out these features before they launch on mainnet.

Architecture Overview

Modular Privacy System Architecture

The hx.finance privacy system uses a modular three-layer architecture that enables seamless upgrades without disrupting users:

Understanding the Components

ZK Circuits vs Verifiers vs Verification Keys

  1. ZK Circuits (zk-circuits/innocence-circuits/)

    • Generate proofs (innocence, balance, trade)
    • Written in Rust using SP1 framework
    • When updated, produce new verification keys
  2. SP1Verifier Contract

    • The actual on-chain verifier that checks proofs
    • Uses verification keys (vkeys) to validate proofs
    • One verifier can handle multiple proof types
  3. Verification Keys (VKeys)

    • Generated by circuits when compiled
    • Used by SP1Verifier to verify proofs
    • Different for each circuit version

Smart Contract Architecture

The Problem Solved by Modular Design

When you update a circuit:

  • It generates a new vkey
  • The contract needs this new vkey
  • But hardcoding vkeys means redeploying everything
  • Users lose their innocence status and deposits

The Solution: Three-Layer Architecture

Key Contracts

ProofRegistry.sol

  • Stores verification keys for each proof type
  • Supports multiple versions simultaneously
  • Tracks which circuit version generated each vkey

InnocenceRegistry.sol

  • Permanently stores user innocence status
  • Survives all upgrades
  • Single source of truth for compliance

HyperliquidPrivacySystemModular.sol

  • Uses registries instead of hardcoded values
  • Accepts vkey version in proof functions
  • Maintains all privacy functionality

How Circuit Updates Work

With the modular architecture, circuit updates are seamless:

# 1. Update circuit
cd zk-circuits/innocence-circuits
cargo run --bin prove
 
# 2. Register new vkey (no redeploy!)
proofRegistry.registerVKey(
    ProofType.INNOCENCE,
    newVKey,
    "innocence-v2"
);
 
# 3. Users can use either version
proveInnocence(proof, publicValues, 0);     // Use latest
proveInnocence(proof, publicValues, 1);     // Use v1

User Experience

The modular design ensures a seamless experience:

When Circuits Are Updated:

  • Innocence status preserved in registry
  • Can continue using previous proof versions
  • No need to re-prove or re-deposit
  • Seamless transition

For All Users:

  • Automatically use latest vkey version by default
  • Option to specify version if needed
  • Enhanced security with each update

Implementation Flow

1. Initial Deployment

// Deploy all contracts
1. Deploy SP1Verifier (unchanged)
2. Deploy InnocenceRegistry
3. Deploy ProofRegistry
4. Deploy HyperliquidPrivacySystemModular
5. Register initial vkeys in ProofRegistry

2. Circuit Update Process

// When circuit needs update
1. Update circuit code
2. Generate new proof and vkey
3. Register new vkey version:
   await proofRegistry.registerVKey(INNOCENCE, newVKey, "v2");
4. Old proofs still work!

3. Emergency Response

// If a vkey is compromised
await proofRegistry.deactivateVKey(INNOCENCE, compromisedVersion);
// Only affects that version, system continues working

Benefits of Modular Architecture

  • No Disruption: Users never lose access or need to re-prove
  • Flexible Updates: Update circuits without touching main contract
  • Version Support: Multiple proof versions work simultaneously
  • Emergency Control: Can disable specific versions if needed
  • Audit Trail: Track all vkey versions and their sources

Gas Considerations

  • Additional external call adds ~2000 gas
  • Still far cheaper than redeploying everything
  • Users save gas by not re-proving innocence

Security Notes

  • Only owner can register new vkeys
  • Old versions can be deprecated gradually
  • Emergency pause still available
  • All registries have access control

Key Functions

// Prove you're not sanctioned (with optional vkey version)
function proveInnocence(
    bytes calldata innocenceProof, 
    bytes calldata publicValues,
    uint8 vkeyVersion // 0 = latest, 1+ = specific version
) external
 
// Make a private deposit
function deposit(
    uint64 token, 
    uint256 amount, 
    bytes32 commitment
) external
 
// Execute private swap (with vkey version)
function privateSwap(
    bytes calldata proof, 
    bytes calldata publicValues, 
    uint24 fee, // or bytes calldata path
    uint8 vkeyVersion
) external
 
// Withdraw funds (with vkey version)
function withdraw(
    bytes32 nullifier,
    address recipient,
    uint64 token,
    uint256 amount,
    bytes calldata balanceProof,
    bytes calldata publicValues,
    uint8 vkeyVersion
) external

Zero-Knowledge Circuits

SP1-Based Implementation

All privacy proofs are generated using SP1 (Succinct Processor 1) zero-knowledge circuits.

Proof Formats

Innocence Proof (61 bytes)

[0:20]   - Depositor address
[20:52]  - Sanctions Merkle root
[52:60]  - Timestamp (uint64)
[60:61]  - Is innocent flag (bool)

Balance Proof (104 bytes)

[0:32]   - Commitment
[32:64]  - Merkle root
[64:96]  - Minimum balance
[96:104] - Asset ID (uint64)

Trade Proof (168 bytes)

[0:32]    - Commitment
[32:64]   - Nullifier hash
[64:96]   - Merkle root
[96:104]  - Token in ID
[104:112] - Token out ID
[112:119] - Amount in (uint56, divided by 256)
[119:127] - Min amount out (uint64)
[127:135] - Nonce (uint64)
[135:168] - Padding (33 bytes)

Amount precision: Input amounts are stored as uint56 divided by 256 to save space while maintaining reasonable precision.

Commitment System

Commitment Generation

commitment = hash(secret || nullifier)

Where:

  • secret: 32-byte random value
  • nullifier: 32-byte random value
  • hash: Poseidon hash function

Nullifier Management

Nullifiers prevent double-spending:

  • Each commitment has one nullifier
  • For swaps: nullifierHash = hash(nullifier || nonce)
  • For withdrawals: Direct nullifier usage
  • Stored in mapping to prevent reuse

Token Management

Supported Tokens

TokenIDAddressDecimals
WHYPE00x5555...555518
UBTC10x9FDB...34638
UETH20xBe67...790718
USDE30x5d3a...ef3418

Balance Accounting

  • Virtual balances tracked per commitment
  • No actual token pooling (tokens remain in contract)
  • Balance proofs ensure sufficient funds
  • Atomic swaps maintain consistency

Router Integration

AMM Integration

Router Address: 0xEBd14cdF290185Cc4d0b5eC73A0e095d780e5D2f

Single-Hop Swaps

ISwapRouter.ExactInputSingleParams({
    tokenIn: tokenInAddress,
    tokenOut: tokenOutAddress,
    fee: fee,
    recipient: address(this),
    deadline: block.timestamp,
    amountIn: amountIn,
    amountOutMinimum: minAmountOut,
    sqrtPriceLimitX96: 0
})

Multi-Hop Swaps

  • Path encoded as: token0 || fee0 || token1 || fee1 || token2...
  • Automatic routing through multiple pools
  • Best execution path calculation

Security Features

Reentrancy Protection

  • OpenZeppelin's ReentrancyGuard
  • Check-effects-interactions pattern
  • State updates before external calls

Commitment Uniqueness

  • Each commitment must be unique
  • Tracked in commitmentIndices mapping
  • Prevents duplicate deposits

Nonce Enforcement

  • Sequential nonces per commitment
  • Prevents replay attacks
  • Ensures transaction ordering

Approval Management

// Reset approval before setting new
IERC20(token).approve(spender, 0);
IERC20(token).approve(spender, amount);

Sanctions Management

Sanctions Tree

  • Static Merkle tree containing sanctioned addresses
  • Updated via off-chain oracle
  • Only Merkle root stored on-chain
  • Python updater script for root updates

Gas Optimization

Packed Formats

  • Proof data tightly packed
  • Minimal calldata usage
  • Efficient storage patterns

Batch Capabilities

  • Multiple operations per transaction (planned)
  • Shared proof verification
  • Reduced overhead

Integration Guide

For Developers

// Example: Making a private deposit
const { ethers } = require('ethers');
const crypto = require('crypto');
 
function generateCommitment() {
    // Generate random 32-byte values
    const secret = '0x' + crypto.randomBytes(32).toString('hex');
    const nullifier = '0x' + crypto.randomBytes(32).toString('hex');
    
    // In production, use Poseidon hash
    // For this example, using keccak256
    const commitment = ethers.utils.keccak256(
        ethers.utils.solidityPack(['bytes32', 'bytes32'], [secret, nullifier])
    );
    
    return { commitment, secret, nullifier };
}
 
async function privateDeposit(amount, tokenId) {
    // Generate commitment
    const { commitment, secret, nullifier } = generateCommitment();
    
    // Commitment storage handled by the UI:
    // - Automatically encrypted and stored in browser
    // - Downloaded as encrypted file
    // - Optional hardware wallet backup via UI
    await saveCommitmentData(secret, nullifier);
    
    // Execute deposit
    const tx = await privacyContract.deposit(
        tokenId,
        amount,
        commitment
    );
    
    return tx.hash;
}

Deployment Information

Mainnet Deployment

  • Network: Hyperliquid Mainnet
  • Chain ID: 998
  • Contract: [Deployed Address]
  • Verifier: [Verifier Address]

Configuration

  • Innocence validity: 30 days
  • Min deposit: 0.001 tokens
  • Max commitments: ~1 million
  • Supported tokens: 4 (expandable)

Future Enhancements

Planned Features

  • Private liquidity provision
  • Cross-chain privacy bridges
  • Batch operations
  • Hardware wallet integration
  • Governance participation

Research Areas

  • Optimistic rollup integration
  • Account abstraction support
  • Social recovery mechanisms
  • Decentralized sequencer

For the latest updates and detailed implementation code, visit our GitHub repository (opens in a new tab).