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 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:
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
Encrypted Operations Learn FHE operations
Memory Allocator Deep dive into memory management