Skip to main content
Privora uses a content-addressable store to manage FHE ciphertexts. This design keeps on-chain storage minimal while supporting large encrypted values.

Why Content-Addressable Storage?

FHE ciphertexts are large (10KB-100KB), but Solana account storage is expensive. Content-addressable storage solves this by:
  1. Storing ciphertexts off-chain in a key-value store
  2. Using SHA256 hashes as keys (content addressing)
  3. Storing only 32-byte hashes on-chain
On-chain:  [32-byte hash] ──references──> Off-chain: [100KB ciphertext]

How It Works

Data Submission

When you submit encrypted data: The hash serves as both the key and a commitment to the data.

Data Retrieval

When a program needs encrypted data:

Program Interface

Loading Data

Use EncryptedRef::load() to fetch data from the store:
use privora_sdk_program::prelude::*;

pub fn process(price_ref: EncryptedRef<u8>) -> ProgramResult {
    // Load fetches the ciphertext from the content store
    let price: Encrypted<u8> = price_ref.load()?;

    // Now we can perform operations
    let doubled = price.add(&price)?;

    Ok(())
}

Storing Data

Use Encrypted::store() to submit results to the store:
// Perform computation
let result = a.add(&b)?;

// Store result and get hash reference
let result_ref: EncryptedRef<u8> = result.store()?;

// Save the hash to account data
account.result_ref = result_ref;

Syscall Interface

Under the hood, the SDK uses two syscalls:

fetch_data

/// Fetch data from the content-addressable store.
///
/// # Arguments
/// * `hash` - 32-byte SHA256 hash of the data
///
/// # Returns
/// * `Ok(Vec<u8>)` - The ciphertext data
/// * `Err` - If data not found or fetch failed
pub fn fetch_data(hash: &[u8; 32]) -> Result<Vec<u8>, ProgramError>;

submit_data

/// Submit data to the content-addressable store.
///
/// # Arguments
/// * `data` - The ciphertext bytes to store
///
/// # Returns
/// * `Ok([u8; 32])` - The SHA256 hash of the stored data
/// * `Err` - If submission failed
pub fn submit_data(data: &[u8]) -> Result<[u8; 32], ProgramError>;

Data Lifecycle

Data Availability

The content store provides:
PropertyDescription
PersistenceData remains available after transaction
DeduplicationSame data stored once (content-addressed)
ConsistencyHash commitment ensures data integrity
AvailabilitySequencer ensures data is available for transactions

Account Design Patterns

Minimal On-Chain Storage

Store only hash references in accounts:
#[derive(BorshSerialize, BorshDeserialize)]
pub struct MinimalAccount {
    pub owner: Pubkey,              // 32 bytes
    pub encrypted_balance: EncryptedRef<u64>,  // 32 bytes (not 80KB!)
}

Multiple Encrypted Fields

#[derive(BorshSerialize, BorshDeserialize)]
pub struct OrderAccount {
    pub owner: Pubkey,              // 32 bytes
    pub price_ref: EncryptedRef<u8>,    // 32 bytes
    pub quantity_ref: EncryptedRef<u8>, // 32 bytes
    pub total_ref: EncryptedRef<u64>,   // 32 bytes
}
// Total: 128 bytes on-chain, referencing ~150KB of ciphertexts

Performance Considerations

Fetch Costs

FactorImpact
Ciphertext sizeLarger = more memory/time
Number of fetchesEach fetch has overhead
Data localitySequential fetches may be batched

Optimization Tips

  1. Batch operations: Load all needed values, then compute
  2. Minimize round-trips: Don’t load the same value multiple times
  3. Store at end: Submit results after all computation
// Good: Load once, compute, store once
let a = a_ref.load()?;
let b = b_ref.load()?;
let c = c_ref.load()?;

let result = a.add(&b)?.mul(&c)?;
let result_ref = result.store()?;

// Bad: Load repeatedly
let result = a_ref.load()?.add(&b_ref.load()?)?.mul(&c_ref.load()?)?;

Client-Side Submission

Rust Client

use privora_sdk_client::prelude::*;

let privora = PrivoraClient::new("http://localhost:8899").await?;

// Encrypt and submit
let encrypted = privora.encryptor().encrypt(100u8)?;
let hash = privora.submit(&encrypted).await?;

// Use hash in transaction
let instruction_data = [&[0u8], hash.as_slice()].concat();

TypeScript Client

const privora = await Privora.connect('http://localhost:8899');

// Encrypt and submit
const encrypted = privora.encrypt(100, 'u8');
const hash = await privora.submit(encrypted);

// Use hash in transaction
const instructionData = Buffer.concat([
  Buffer.from([0]),
  Buffer.from(hash, 'hex'),
]);

Next Steps