Skip to main content
The Privora Program SDK enables you to build Solana programs that perform computations on encrypted data. This guide covers the SDK structure and how to get started.

SDK Structure

The privora-sdk-program crate provides:
privora_sdk_program/
├── prelude       # Common imports
├── types         # Encrypted types (EncryptedRef, Encrypted, EncryptedBool)
├── ops           # FHE operations and syscalls
├── store         # Data store access
├── memory        # Memory allocator
├── auth          # Authorization PDA helpers
└── error         # Error types

Quick Start

1. Add Dependencies

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

2. Import the Prelude

use privora_sdk_program::prelude::*;
The prelude exports everything you need:
  • EncryptedRef<T>, Encrypted<T>, EncryptedBool
  • Common Solana types (AccountInfo, Pubkey, ProgramResult, etc.)
  • Helper macros and traits

3. Set Up the Allocator

// CRITICAL: Required for FHE operations
privora_sdk_program::setup_fhe_allocator!();
The allocator macro must be called at the crate root before any FHE operations. Without it, your program will fail with out-of-memory errors.

4. Write Your Program

use privora_sdk_program::prelude::*;
use borsh::{BorshSerialize, BorshDeserialize};

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 FHE logic here
    Ok(())
}

Key Concepts

References vs Values

TypeDescriptionSizeStorage
EncryptedRef<T>Hash pointer32 bytesOn-chain accounts
Encrypted<T>Loaded ciphertext10-100KBRuntime memory
Always store EncryptedRef in accounts, not Encrypted values.

The FHE Workflow

  1. Load: Fetch ciphertext from content store
  2. Compute: Perform FHE operations
  3. Store: Submit results back to content store
  4. Save: Store new hash reference in account

Module Reference

types

Core encrypted value types:
use privora_sdk_program::types::{
    EncryptedRef,     // Hash reference (32 bytes)
    Encrypted,        // Loaded encrypted value
    EncryptedBool,    // Encrypted boolean
    EncryptedBoolRef, // Boolean hash reference
    EncryptedInt,     // Trait for supported integer types
};

ops

FHE operations and operator implementations:
// Arithmetic (via methods or operators)
let sum = a.add(&b)?;        // or: &a + &b
let diff = a.sub(&b)?;       // or: &a - &b
let prod = a.mul(&b)?;       // or: &a * &b

// Comparison
let is_ge: EncryptedBool = a.ge(&b)?;
let is_lt: EncryptedBool = a.lt(&b)?;

// Min/Max
let min_val = a.min(&b)?;
let max_val = a.max(&b)?;

// Conditional
let result = condition.select(&true_val, &false_val)?;

store

Content store access:
use privora_sdk_program::store::{fetch_data, submit_data};

// Low-level access (prefer EncryptedRef methods)
let data = fetch_data(&hash)?;
let hash = submit_data(&ciphertext)?;

memory

Memory allocator for FHE operations:
// Use the macro at crate root
privora_sdk_program::setup_fhe_allocator!();

// Or with custom heap size
privora_sdk_program::setup_fhe_allocator!(2 * 1024 * 1024); // 2MB

auth

Authorization PDA helpers:
use privora_sdk_program::auth::{
    DecryptionAuth,   // Standard decryption authorization
    MatchAuth,        // Match-based authorization
    DecryptedResult,  // Decrypted result storage
    FHE_AUTH_PROGRAM_ID,
};

Example: Encrypted Transfer

use privora_sdk_program::prelude::*;
use borsh::{BorshSerialize, BorshDeserialize};

privora_sdk_program::setup_fhe_allocator!();

solana_program::entrypoint!(process_instruction);

#[derive(BorshSerialize, BorshDeserialize)]
pub struct Account {
    pub owner: Pubkey,
    pub balance_ref: EncryptedRef<u64>,
}

pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    let from_account = next_account_info(accounts_iter)?;
    let to_account = next_account_info(accounts_iter)?;

    // Parse amount reference from instruction data
    let amount_ref = EncryptedRef::<u64>::from_hash(
        instruction_data[0..32].try_into().unwrap()
    );

    // Load accounts
    let mut from_state = Account::try_from_slice(&from_account.data.borrow())?;
    let mut to_state = Account::try_from_slice(&to_account.data.borrow())?;

    // Load encrypted balances
    let from_balance = from_state.balance_ref.load()?;
    let to_balance = to_state.balance_ref.load()?;
    let amount = amount_ref.load()?;

    // Perform encrypted transfer
    let new_from_balance = from_balance.sub(&amount)?;
    let new_to_balance = to_balance.add(&amount)?;

    // Store results
    from_state.balance_ref = new_from_balance.store()?;
    to_state.balance_ref = new_to_balance.store()?;

    // Save to accounts
    from_state.serialize(&mut *from_account.data.borrow_mut())?;
    to_state.serialize(&mut *to_account.data.borrow_mut())?;

    msg!("Transfer completed");
    Ok(())
}

Best Practices

Always Use Prelude

Import prelude::* for all common types and traits

Set Up Allocator First

Call setup_fhe_allocator!() before any other code

Use Smallest Types

Choose u8 over u64 when possible for better performance

Store References

Never store Encrypted<T> in accounts, only EncryptedRef<T>

Next Steps