Skip to main content
The ZK ElGamal Proof SDK is built on a sophisticated cryptographic foundation that combines encryption, commitments, and zero-knowledge proofs to enable confidential transactions on Solana. This section explains the core concepts that power the SDK.

Cryptographic Building Blocks

The SDK is built on three fundamental cryptographic primitives:

ElGamal Encryption

Twisted ElGamal encryption scheme for confidential amounts

Pedersen Commitments

Cryptographic commitments with homomorphic properties

Zero-Knowledge Proofs

Proving statements without revealing sensitive data

The Ristretto Group on Curve25519

All cryptographic operations in the SDK are performed over the Ristretto prime-order group representation of Curve25519. This provides:
  • Security: 128-bit security level
  • Performance: Fast group operations optimized for modern processors
  • Prime-order group: Eliminates cofactor-related vulnerabilities
  • Canonical encoding: Each group element has exactly one valid encoding
The SDK uses the curve25519-dalek implementation for all elliptic curve operations.
The Ristretto group was specifically designed to provide a clean, prime-order group abstraction over Curve25519, eliminating many of the pitfalls associated with cofactor handling in Edwards curves.

Base Points and Constants

The SDK defines two fundamental base points used throughout the cryptographic operations:
/// Pedersen base point for encoding messages (from zk-sdk/src/encryption/pedersen.rs:20-25)
pub const G: RistrettoPoint = RISTRETTO_BASEPOINT_POINT;

/// Pedersen base point for encoding openings (derived via hash)
pub static H: std::sync::LazyLock<RistrettoPoint> = std::sync::LazyLock::new(|| {
    RistrettoPoint::hash_from_bytes::<Sha3_512>(RISTRETTO_BASEPOINT_COMPRESSED.as_bytes())
});
  • G: The standard Ristretto basepoint, used for encoding message values
  • H: A secondary basepoint derived by hashing G, used for randomness in commitments
The base point H is computed deterministically from G using SHA3-512 hashing. This ensures that the discrete log relationship between G and H is unknown, which is critical for the security of Pedersen commitments.

Key Lengths and Constants

All cryptographic components have standardized byte lengths:
// From zk-sdk/src/lib.rs:38-43
const UNIT_LEN: usize = 32;
const RISTRETTO_POINT_LEN: usize = 32;  // Compressed point
const SCALAR_LEN: usize = 32;           // Scalar field element

// From zk-sdk/src/encryption/mod.rs:30-56
pub const PEDERSEN_COMMITMENT_LEN: usize = 32;
pub const PEDERSEN_OPENING_LEN: usize = 32;
pub const DECRYPT_HANDLE_LEN: usize = 32;
pub const ELGAMAL_CIPHERTEXT_LEN: usize = 64;  // commitment + handle
pub const ELGAMAL_PUBKEY_LEN: usize = 32;
pub const ELGAMAL_SECRET_KEY_LEN: usize = 32;
pub const ELGAMAL_KEYPAIR_LEN: usize = 64;     // pubkey + secret

The Twisted ElGamal Approach

Unlike traditional ElGamal encryption, the SDK implements a twisted variant where:
  1. Messages are encrypted in the exponent: Values are encoded as value * G rather than direct group elements
  2. Ciphertexts are Pedersen commitments: Each ciphertext is a commitment with an additional decryption handle
  3. Proof systems are unified: The same proof techniques work for both commitments and ciphertexts
This design choice enables powerful homomorphic properties and seamless integration with zero-knowledge proof systems.
// Traditional: message m is a group element
let ciphertext = (r*G, m + r*pubkey)

Homomorphic Properties

All three primitives support homomorphic operations:

Addition

// Adding encrypted values (from zk-sdk/src/encryption/elgamal.rs:717-723)
let ciphertext_sum = &ciphertext_0 + &ciphertext_1;
// Decrypts to: amount_0 + amount_1

// Adding commitments
let commitment_sum = &commitment_0 + &commitment_1;

Subtraction

// Subtracting encrypted values
let ciphertext_diff = &ciphertext_0 - &ciphertext_1;
// Decrypts to: amount_0 - amount_1

Scalar Multiplication

// Multiplying by a known scalar
let scalar = Scalar::from(5u64);
let ciphertext_product = &ciphertext * &scalar;
// Decrypts to: amount * 5
Homomorphic operations preserve the encryption but can lead to negative values in the scalar field. Always use range proofs to ensure values remain in the expected positive range.

Discrete Logarithm Decryption

Since messages are encrypted “in the exponent”, decryption requires solving a discrete logarithm problem:
// From zk-sdk/src/encryption/elgamal.rs:127-131
fn decrypt(secret: &ElGamalSecretKey, ciphertext: &ElGamalCiphertext) -> DiscreteLog {
    DiscreteLog::new_for_g(
        ciphertext.commitment.get_point() - &(&secret.0 * &ciphertext.handle.0),
    )
}
The result is a DiscreteLog instance that must be solved to recover the original value. For small values (up to 2^32), this is computed efficiently using lookup tables and baby-step giant-step algorithms.
The discrete log computation is intentionally limited to small values. This is not a limitation in practice, as the SDK is designed for confidential token amounts, which are typically well within the 64-bit range.

Transcript and Fiat-Shamir

All zero-knowledge proofs use the Fiat-Shamir heuristic to convert interactive sigma protocols into non-interactive proofs. The SDK uses Merlin transcripts for this purpose.
// From zk-sdk/src/lib.rs:32-36
pub const TRANSCRIPT_DOMAIN: &[u8] = b"solana-zk-elgamal-proof-program-v1";
Critical Security Note: The transcript domain separator TRANSCRIPT_DOMAIN MUST be changed for any fork or separate deployment to prevent cross-chain proof replay attacks.

Security Considerations

Constant-Time Operations

Secret keys and openings implement constant-time equality checks to prevent timing attacks:
// From zk-sdk/src/encryption/elgamal.rs:640-644
impl ConstantTimeEq for ElGamalSecretKey {
    fn ct_eq(&self, other: &Self) -> Choice {
        self.0.ct_eq(&other.0)
    }
}

Zeroization

Sensitive values are automatically zeroized when dropped:
// From zk-sdk/src/encryption/elgamal.rs:149-156
#[derive(Clone, Deserialize, PartialEq, Eq, Serialize, Zeroize)]
#[zeroize(drop)]
pub struct ElGamalKeypair {
    public: ElGamalPubkey,
    secret: ElGamalSecretKey,
}

Identity Element Checks

Proof verification explicitly rejects identity elements to prevent malleability attacks:
// From sigma proof verification
if pubkey.get_point().is_identity() 
    || commitment.get_point().is_identity() {
    return Err(EqualityProofVerificationError::InvalidProof);
}

Next Steps

Now that you understand the foundational concepts, dive deeper into each component:

ElGamal Encryption

Learn about twisted ElGamal encryption, key generation, and decryption

Pedersen Commitments

Understand cryptographic commitments and their properties

Zero-Knowledge Proofs

Explore the various proof systems available in the SDK

Build docs developers (and LLMs) love