Skip to main content
This guide covers the essential setup steps for building FHE-enabled Solana programs with Privora.

Project Structure

A typical FHE program project:
my-fhe-program/
├── Cargo.toml
├── src/
│   ├── lib.rs           # Entry point with allocator
│   ├── state.rs         # Account structures
│   └── instructions.rs  # Instruction handlers
└── tests/
    └── integration.rs   # FHE tests

Cargo.toml Configuration

Basic Configuration

[package]
name = "my-fhe-program"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]

[dependencies]
privora-sdk-program = { git = "https://github.com/privora-xyz/privora" }
solana-program = "2.0"
borsh = "1.5"

[dev-dependencies]
privora-sdk-testing = { git = "https://github.com/privora-xyz/privora" }
solana-sdk = "2.0"

[features]
default = []
no-entrypoint = []

Key Points

SettingPurpose
crate-type = ["cdylib", "lib"]Build both shared library (for deployment) and Rust library (for testing)
no-entrypoint featureAllows using program code as library without entrypoint

Memory Allocator Setup

The FHE allocator setup is critical. Without it, your program will crash with out-of-memory errors when performing FHE operations.

Why a Custom Allocator?

FHE ciphertexts are large (10-100KB). The default Solana allocator provides only ~32KB of heap, which is insufficient. The Privora allocator provides ~1MB.

Setting Up the Allocator

Add this at the top of your lib.rs:
use privora_sdk_program::prelude::*;

// CRITICAL: Must be at crate root, before any other code
privora_sdk_program::setup_fhe_allocator!();

// Rest of your program...
solana_program::entrypoint!(process_instruction);

Custom Heap Size

For programs that need more memory:
// Custom 2MB heap
privora_sdk_program::setup_fhe_allocator!(2 * 1024 * 1024);

How It Works

The macro:
  1. Creates a static FheBumpAllocator instance
  2. Registers it as the global allocator via #[global_allocator]
  3. Only activates on target_os = "solana" (doesn’t affect tests)
// Expanded macro (for reference)
#[cfg(target_os = "solana")]
#[global_allocator]
static FHE_ALLOCATOR: FheBumpAllocator = FheBumpAllocator::new();

Program Entry Point

Standard Entry Point

use privora_sdk_program::prelude::*;

privora_sdk_program::setup_fhe_allocator!();

solana_program::entrypoint!(process_instruction);

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    // Your instruction handling
    Ok(())
}

No-Alloc Entry Point (Recommended)

For better performance, use the no-alloc variant:
use privora_sdk_program::prelude::*;

privora_sdk_program::setup_fhe_allocator!();

#[cfg(not(feature = "no-entrypoint"))]
solana_program_entrypoint::entrypoint_no_alloc!(process_instruction);

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    // Your instruction handling
    Ok(())
}

Account State Definitions

Using EncryptedRef

Store hash references, not actual ciphertexts:
use privora_sdk_program::prelude::*;
use borsh::{BorshSerialize, BorshDeserialize};

#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct Order {
    /// Order owner
    pub owner: Pubkey,              // 32 bytes

    /// Encrypted price (hash reference)
    pub price_ref: EncryptedRef<u8>,    // 32 bytes

    /// Encrypted quantity (hash reference)
    pub quantity_ref: EncryptedRef<u8>, // 32 bytes

    /// Order side
    pub side: OrderSide,            // 1 byte

    /// Order status
    pub status: OrderStatus,        // 1 byte
}

impl Order {
    pub const LEN: usize = 32 + 32 + 32 + 1 + 1; // 98 bytes
}

Account Size Calculation

Always calculate exact sizes:
impl MyAccount {
    pub const LEN: usize =
        32 +  // pubkey
        32 +  // encrypted_ref
        8 +   // u64
        1;    // u8/enum
}

Instruction Data Parsing

From Raw Bytes

pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    // First byte is instruction discriminator
    let instruction = instruction_data[0];

    match instruction {
        0 => {
            // Parse EncryptedRef from bytes
            let hash: [u8; 32] = instruction_data[1..33]
                .try_into()
                .map_err(|_| ProgramError::InvalidInstructionData)?;
            let price_ref = EncryptedRef::<u8>::from_hash(hash);

            // Process...
        }
        _ => return Err(ProgramError::InvalidInstructionData),
    }

    Ok(())
}

Using Borsh

#[derive(BorshDeserialize)]
pub struct SubmitOrderData {
    pub price_ref: EncryptedRef<u8>,   // 32 bytes
    pub quantity_ref: EncryptedRef<u8>, // 32 bytes
    pub side: OrderSide,               // 1 byte
}

pub fn submit_order(
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let data = SubmitOrderData::try_from_slice(instruction_data)?;

    // Use data.price_ref, data.quantity_ref, data.side
    Ok(())
}

Program ID Declaration

use solana_pubkey::declare_id;

declare_id!("YourProgram1111111111111111111111111111111111");

Complete Example

// src/lib.rs
use privora_sdk_program::prelude::*;
use solana_pubkey::declare_id;

mod instructions;
mod state;

pub use state::*;

declare_id!("MyProgram111111111111111111111111111111111111");

// CRITICAL: Set up allocator first
privora_sdk_program::setup_fhe_allocator!();

#[cfg(not(feature = "no-entrypoint"))]
solana_program_entrypoint::entrypoint_no_alloc!(process_instruction);

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    if instruction_data.is_empty() {
        return Err(ProgramError::InvalidInstructionData);
    }

    match instruction_data[0] {
        0 => instructions::initialize(program_id, accounts, &instruction_data[1..]),
        1 => instructions::process_fhe(program_id, accounts, &instruction_data[1..]),
        _ => Err(ProgramError::InvalidInstructionData),
    }
}

Building

# Build for Solana
cargo build-sbf

# Output: target/deploy/my_fhe_program.so

Common Issues

Out of Memory

Error: memory allocation failed
Solution: Ensure setup_fhe_allocator!() is called at crate root.

Wrong Account Size

Error: Account data too small
Solution: Calculate exact sizes and ensure accounts are created with sufficient space.

Missing Borsh Derives

Error: the trait `BorshDeserialize` is not implemented for `EncryptedRef<u8>`
Solution: EncryptedRef already implements Borsh traits. Check your other types.

Next Steps