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.
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
| Account | Purpose |
|---|---|
BridgeState | Global configuration, merkle root, statistics |
NullifierAccount | Per-nullifier PDA, tracks spent nullifiers |
BridgeVault | PDA 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_proofSubmit 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 # secondsBridge 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-nodeEnvironment Variables:
BRIDGE_ENABLED=true- Enable bridge event listeningBRIDGE_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-initInitialize Bridge State
cargo run --bin bridge-init -- \
--merkle-depth 20 \
--min-deposit 0.01 \
--withdrawal-fee 0.001Testing
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 jsonView Transaction History
# On Solana Explorer
https://explorer.solana.com/address/DSysqF2oYAuDRLfPajMnRULce2MjC3AtTszCkcDv1jco?cluster=devnetFees
| Action | Fee |
|---|---|
| Deposit | Network fee only (~0.000005 SOL) |
| Withdrawal | 0.001 SOL (configurable) |
Withdrawal fees go to bridge vault for operational costs.