ParaloomPARALOOM

Solana Bridge

Cross-chain bridge for SOL deposits and withdrawals

Solana Bridge

The Solana bridge enables bidirectional movement of SOL between the public Solana blockchain and Paraloom's privacy layer.

Solana Bridge Architecture

Overview

Current Deployment (Devnet):

  • Program ID: DSysqF2oYAuDRLfPajMnRULce2MjC3AtTszCkcDv1jco
  • Status: Active & Tested

Architecture

On-Chain Program

Anchor-based Solana program managing:

  • Deposits into privacy pool
  • Withdrawals with zkSNARK verification
  • Nullifier tracking (double-spend prevention)
  • Bridge vault management

Program Accounts

AccountPurpose
BridgeStateGlobal configuration, merkle root, statistics
NullifierAccountPer-nullifier PDA, tracks spent nullifiers
BridgeVaultPDA holding deposited SOL

Deposit Flow

User Initiates Deposit

User calls the deposit instruction with:

  • Amount (in lamports)
  • Commitment (hash of amount + randomness)

SOL Transferred to Vault

#[program]
pub fn deposit(ctx: Context<Deposit>, amount: u64, commitment: [u8; 32]) -> Result<()> {
    // Transfer SOL to vault
    transfer(
        CpiContext::new(
            ctx.accounts.system_program.to_account_info(),
            Transfer {
                from: ctx.accounts.user.to_account_info(),
                to: ctx.accounts.vault.to_account_info(),
            },
        ),
        amount,
    )?;

    // Emit deposit event
    emit!(DepositEvent {
        commitment,
        amount,
        timestamp: Clock::get()?.unix_timestamp,
    });

    Ok(())
}

Event Captured by Bridge Listener

Off-chain bridge listener polls for deposit events and processes them.

Commitment Added to Merkle Tree

Privacy pool adds commitment to incremental merkle tree:

merkle_tree.add_leaf(commitment);
let new_root = merkle_tree.root();

User Receives Shielded Note

User stores note containing:

  • Commitment
  • Amount
  • Randomness
  • Merkle path

Withdrawal Flow

User Generates Proof

User generates zkSNARK proof locally:

cargo run --bin generate_withdrawal_proof

Submit to Consensus

Proof submitted to validator network for verification.

Validators Verify (7/10)

Each validator independently verifies:

  • Proof is valid
  • Nullifier not spent
  • Merkle root matches

Submit On-Chain Transaction

If consensus reached, bridge submitter sends transaction:

#[program]
pub fn withdraw(
    ctx: Context<Withdraw>,
    proof: Vec<u8>,
    nullifier: [u8; 32],
    amount: u64,
) -> Result<()> {
    // 1. Verify zkSNARK proof
    require!(
        verify_groth16_proof(&proof, &ctx.accounts.bridge_state.merkle_root),
        ErrorCode::InvalidProof
    );

    // 2. Check nullifier not spent
    require!(
        !ctx.accounts.nullifier_account.spent,
        ErrorCode::NullifierSpent
    );

    // 3. Transfer SOL to recipient
    **ctx.accounts.vault.try_borrow_mut_lamports()? -= amount;
    **ctx.accounts.recipient.try_borrow_mut_lamports()? += amount;

    // 4. Mark nullifier as spent
    ctx.accounts.nullifier_account.spent = true;

    emit!(WithdrawEvent {
        nullifier,
        amount,
        recipient: ctx.accounts.recipient.key(),
    });

    Ok(())
}

SOL Transferred to Recipient

User receives SOL at their public wallet address.

Security

Double-Spend Prevention

Nullifiers are stored as PDAs:

#[account]
pub struct NullifierAccount {
    pub nullifier: [u8; 32],
    pub spent: bool,
    pub spent_at: i64,
}

Seeds for PDA:

seeds = [b"nullifier", nullifier.as_ref()]

On-Chain Verification

The Solana program verifies proofs using Groth16:

fn verify_groth16_proof(proof: &[u8], public_inputs: &PublicInputs) -> bool {
    // Load verifying key (compiled into program)
    let vk = VERIFYING_KEY;

    // Deserialize proof
    let proof = Proof::deserialize(proof)?;

    // Verify
    Groth16::<Bls12_381>::verify(&vk, public_inputs, &proof)
}

Bridge Authority

Multi-sig authority controls:

  • Program upgrades
  • Emergency pause
  • Fee adjustments

Configuration

Environment Variables

export SOLANA_RPC_URL="https://api.devnet.solana.com"
export SOLANA_PROGRAM_ID="DSysqF2oYAuDRLfPajMnRULce2MjC3AtTszCkcDv1jco"
export BRIDGE_AUTHORITY_KEYPAIR_PATH="./authority-keypair.json"
export BRIDGE_POLL_INTERVAL=5  # seconds

Bridge Listener

The bridge listener is integrated into the validator node. Start it via the CLI:

# Start validator with bridge listener enabled
paraloom validator start --config validator.toml

# Or run the node directly with bridge enabled
BRIDGE_ENABLED=true cargo run --bin paraloom-node

Environment Variables:

  • BRIDGE_ENABLED=true - Enable bridge event listening
  • BRIDGE_POLL_INTERVAL=5 - Event polling interval in seconds

Deployment

Deploy to Devnet

# 1. Build program
cd programs/paraloom
cargo build-sbf

# 2. Deploy
solana program deploy \
  --upgrade-authority ../../authority-keypair.json \
  target/deploy/paraloom_program.so

# 3. Initialize bridge
cargo run --bin bridge-init

Initialize Bridge State

cargo run --bin bridge-init -- \
  --merkle-depth 20 \
  --min-deposit 0.01 \
  --withdrawal-fee 0.001

Testing

Test Deposit

cargo run --bin test-deposit -- \
  --amount 1.0 \
  --recipient <SHIELDED_ADDRESS>

Test Withdrawal

cargo run --bin test-withdraw -- \
  --proof-path ./withdrawal_proof.bin \
  --nullifier <NULLIFIER> \
  --amount 1.0 \
  --recipient <SOL_ADDRESS>

Monitoring

View Bridge State

solana account <BRIDGE_STATE_ADDRESS> --output json

View Transaction History

# On Solana Explorer
https://explorer.solana.com/address/DSysqF2oYAuDRLfPajMnRULce2MjC3AtTszCkcDv1jco?cluster=devnet

Fees

ActionFee
DepositNetwork fee only (~0.000005 SOL)
Withdrawal0.001 SOL (configurable)

Withdrawal fees go to bridge vault for operational costs.

On this page