Skip to main content

Build Your First FHE Program

This guide walks you through building a simple FHE-enabled counter program that increments an encrypted value.

Prerequisites

  • Rust 1.70+ with wasm32-unknown-unknown target
  • Solana CLI tools
  • Node.js 18+ (for TypeScript client)

Step 1: Create a New Program

cargo new --lib fhe-counter
cd fhe-counter
Update Cargo.toml:
[package]
name = "fhe-counter"
version = "0.1.0"
edition = "2021"

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

[dependencies]
privora-sdk-program = "0.1"
solana-program = "2.0"
borsh = "1.5"

[features]
no-entrypoint = []

Step 2: Write the Program

Replace src/lib.rs:
use privora_sdk_program::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

// CRITICAL: Set up the FHE allocator for ~1MB heap
privora_sdk_program::setup_fhe_allocator!();

// Program entrypoint
solana_program::entrypoint!(process_instruction);

/// Counter state - stores a reference to encrypted value
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Counter {
    /// Reference to encrypted counter value
    pub value_ref: EncryptedRef<u64>,
}

impl Counter {
    pub const LEN: usize = 32; // EncryptedRef is 32 bytes
}

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

    match instruction_data[0] {
        // Initialize with encrypted value
        0 => {
            let value_ref = EncryptedRef::<u64>::from_hash(
                instruction_data[1..33].try_into().unwrap()
            );
            let counter = Counter { value_ref };
            counter.serialize(&mut *counter_account.data.borrow_mut())?;
            msg!("Counter initialized");
        }
        // Increment by encrypted amount
        1 => {
            let increment_ref = EncryptedRef::<u64>::from_hash(
                instruction_data[1..33].try_into().unwrap()
            );

            // Load current state
            let mut counter = Counter::try_from_slice(&counter_account.data.borrow())?;

            // Load encrypted values from the data store
            let current_value = counter.value_ref.load()?;
            let increment = increment_ref.load()?;

            // Perform FHE addition (operates on encrypted data!)
            let new_value = current_value.add(&increment)?;

            // Store result back and update reference
            counter.value_ref = new_value.store()?;
            counter.serialize(&mut *counter_account.data.borrow_mut())?;

            msg!("Counter incremented");
        }
        _ => return Err(ProgramError::InvalidInstructionData),
    }

    Ok(())
}
The setup_fhe_allocator!() macro is required for FHE operations. It configures a ~1MB heap allocator needed for FHE ciphertexts.

Step 3: Build the Program

cargo build-sbf
This produces target/deploy/fhe_counter.so.

Step 4: Write a Test

Create tests/counter_test.rs:
use privora_sdk_testing::prelude::*;
use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction};

const PROGRAM_ID: Pubkey = solana_pubkey::pubkey!("Counter111111111111111111111111111111111111");

#[test]
fn test_counter() {
    // Create test environment with FHE support
    let mut env = FheTestEnv::new();
    env.deploy_program(PROGRAM_ID, include_bytes!("../target/deploy/fhe_counter.so"));

    let payer = env.create_funded_keypair();

    // Create counter account
    let counter = Keypair::new();
    let rent = env.minimum_balance_for_rent_exemption(32);

    // Encrypt initial value (100)
    let initial_hash = env.encrypt_and_submit_u64(100);

    // Build initialize instruction
    let mut init_data = vec![0u8]; // instruction discriminator
    init_data.extend_from_slice(&initial_hash);

    let init_ix = solana_program::instruction::Instruction {
        program_id: PROGRAM_ID,
        accounts: vec![
            AccountMeta::new(counter.pubkey(), false),
        ],
        data: init_data,
    };

    // Create account and initialize
    let create_ix = solana_program::system_instruction::create_account(
        &payer.pubkey(),
        &counter.pubkey(),
        rent,
        32,
        &PROGRAM_ID,
    );

    env.sync_data_store_to_syscalls();

    let tx = Transaction::new_signed_with_payer(
        &[create_ix, init_ix],
        Some(&payer.pubkey()),
        &[&payer, &counter],
        env.latest_blockhash(),
    );
    env.svm.send_transaction(tx).unwrap();
    env.sync_data_store_from_syscalls();

    // Encrypt increment value (50)
    let increment_hash = env.encrypt_and_submit_u64(50);

    // Build increment instruction
    let mut inc_data = vec![1u8];
    inc_data.extend_from_slice(&increment_hash);

    let inc_ix = solana_program::instruction::Instruction {
        program_id: PROGRAM_ID,
        accounts: vec![
            AccountMeta::new(counter.pubkey(), false),
        ],
        data: inc_data,
    };

    env.sync_data_store_to_syscalls();

    let tx = Transaction::new_signed_with_payer(
        &[inc_ix],
        Some(&payer.pubkey()),
        &[&payer],
        env.latest_blockhash(),
    );
    env.svm.send_transaction(tx).unwrap();
    env.sync_data_store_from_syscalls();

    // Read counter and verify result
    let counter_data = env.svm.get_account(&counter.pubkey()).unwrap();
    let counter_state: Counter = borsh::from_slice(&counter_data.data).unwrap();

    // Decrypt and verify: 100 + 50 = 150
    env.assert_encrypted_eq_u64(&counter_state.value_ref.hash(), 150);
}
Run the test:
cargo test

Step 5: Client Integration (TypeScript)

import { Privora } from '@privora/sdk';
import { Keypair, PublicKey } from '@solana/web3.js';

async function main() {
  // Connect to Privora sequencer
  const privora = await Privora.connect('http://localhost:8899');

  const payer = Keypair.generate();
  await privora.requestAirdrop(payer.publicKey, 1_000_000_000);

  // Encrypt values
  const initialValue = privora.encrypt(100, 'u64');
  const incrementValue = privora.encrypt(50, 'u64');

  // Submit encrypted data
  const initialHash = await privora.submit(initialValue);
  const incrementHash = await privora.submit(incrementValue);

  // Build and send initialize transaction
  const initData = Buffer.concat([
    Buffer.from([0]),
    Buffer.from(initialHash, 'hex'),
  ]);

  const signature = await privora
    .transaction()
    .add({
      programId: PROGRAM_ID,
      keys: [{ pubkey: counterPubkey, isSigner: false, isWritable: true }],
      data: initData,
    })
    .withFheData([initialHash])
    .sign(payer)
    .send();

  console.log('Transaction:', signature);
}

Next Steps