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-unknowntarget - Solana CLI tools
- Node.js 18+ (for TypeScript client)
Step 1: Create a New Program
Copy
cargo new --lib fhe-counter
cd fhe-counter
Cargo.toml:
Copy
[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
Replacesrc/lib.rs:
Copy
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
Copy
cargo build-sbf
target/deploy/fhe_counter.so.
Step 4: Write a Test
Createtests/counter_test.rs:
Copy
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);
}
Copy
cargo test
Step 5: Client Integration (TypeScript)
Copy
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);
}