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
| Setting | Purpose |
|---|
crate-type = ["cdylib", "lib"] | Build both shared library (for deployment) and Rust library (for testing) |
no-entrypoint feature | Allows 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:
- Creates a static
FheBumpAllocator instance
- Registers it as the global allocator via
#[global_allocator]
- 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