Tutorial: Encrypted Counter
This tutorial walks through building an encrypted counter program from scratch.What We’ll Build
A simple counter that:- Stores an encrypted value
- Increments by an encrypted amount
- Never exposes the actual values on-chain
Step 1: Project Setup
Copy
cargo new --lib fhe-counter
cd fhe-counter
Copy
[package]
name = "fhe-counter"
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" }
[features]
no-entrypoint = []
Step 2: Define State
src/lib.rs:Copy
use privora_sdk_program::prelude::*;
use borsh::{BorshSerialize, BorshDeserialize};
// CRITICAL: Set up FHE allocator
privora_sdk_program::setup_fhe_allocator!();
/// Counter state - only stores a 32-byte hash reference
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Counter {
pub value_ref: EncryptedRef<u64>,
}
impl Counter {
pub const LEN: usize = 32;
}
Step 3: Process Instructions
Copy
solana_program::entrypoint!(process_instruction);
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
0 => initialize(counter_account, &instruction_data[1..]),
// Increment
1 => increment(counter_account, &instruction_data[1..]),
_ => Err(ProgramError::InvalidInstructionData),
}
}
Step 4: Initialize Instruction
Copy
fn initialize(
counter_account: &AccountInfo,
data: &[u8],
) -> ProgramResult {
// Parse initial value hash from instruction data
let initial_hash: [u8; 32] = data[0..32]
.try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?;
let counter = Counter {
value_ref: EncryptedRef::from_hash(initial_hash),
};
counter.serialize(&mut *counter_account.data.borrow_mut())?;
msg!("Counter initialized");
Ok(())
}
Step 5: Increment Instruction
Copy
fn increment(
counter_account: &AccountInfo,
data: &[u8],
) -> ProgramResult {
// Parse increment hash from instruction data
let increment_hash: [u8; 32] = data[0..32]
.try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?;
let increment_ref = EncryptedRef::<u64>::from_hash(increment_hash);
// Load current counter
let mut counter = Counter::try_from_slice(&counter_account.data.borrow())?;
// Load encrypted values
let current_value = counter.value_ref.load()?;
let increment = increment_ref.load()?;
// Perform encrypted addition
let new_value = current_value.add(&increment)?;
// Store result and update counter
counter.value_ref = new_value.store()?;
counter.serialize(&mut *counter_account.data.borrow_mut())?;
msg!("Counter incremented");
Ok(())
}
Step 6: Write Tests
tests/counter_test.rs:Copy
use privora_sdk_testing::prelude::*;
use solana_sdk::{
instruction::{AccountMeta, Instruction},
signature::Keypair,
signer::Signer,
transaction::Transaction,
};
const PROGRAM_ID: Pubkey = solana_pubkey::pubkey!("Counter111111111111111111111111111111111111");
#[test]
fn test_counter() {
let mut env = FheTestEnv::new();
env.deploy_program(PROGRAM_ID, include_bytes!("../target/deploy/fhe_counter.so"));
let payer = env.create_funded_keypair();
let counter = Keypair::new();
// Create counter account
let rent = env.minimum_balance_for_rent_exemption(32);
let create_ix = solana_sdk::system_instruction::create_account(
&payer.pubkey(),
&counter.pubkey(),
rent,
32,
&PROGRAM_ID,
);
// Initialize with encrypted 100
let initial_hash = env.encrypt_and_submit_u64(100);
let mut init_data = vec![0u8];
init_data.extend_from_slice(&initial_hash);
let init_ix = Instruction {
program_id: PROGRAM_ID,
accounts: vec![AccountMeta::new(counter.pubkey(), false)],
data: init_data,
};
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();
// Increment by encrypted 50
let increment_hash = env.encrypt_and_submit_u64(50);
let mut inc_data = vec![1u8];
inc_data.extend_from_slice(&increment_hash);
let inc_ix = 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();
// Verify: 100 + 50 = 150
let account_data = env.svm.get_account(&counter.pubkey()).unwrap();
let result_hash: [u8; 32] = account_data.data[0..32].try_into().unwrap();
env.assert_encrypted_eq_u64(&result_hash, 150);
}
Step 7: Build and Test
Copy
# Build program
cargo build-sbf
# Run tests
cargo test
Key Takeaways
- Always set up the allocator with
setup_fhe_allocator!() - Store references, not ciphertexts in accounts
- Load, compute, store is the FHE workflow
- Sync data store before and after transactions in tests