Skip to main content

Overview

The ciphertext-ciphertext equality proof certifies that two ElGamal ciphertexts (possibly encrypted under different public keys) contain the same plaintext message. The prover must provide:
  • The decryption key for the first ciphertext
  • The randomness (opening) used to generate the second ciphertext
The protocol guarantees computational soundness and perfect zero-knowledge in the random oracle model.

Proof Structure

The CiphertextCiphertextEqualityProof contains seven components:
Y_0
CompressedRistretto
Commitment for the first public key
Y_1
CompressedRistretto
Commitment combining message and first ciphertext handle
Y_2
CompressedRistretto
Commitment for the second ciphertext
Y_3
CompressedRistretto
Commitment for the second public key
z_s
Scalar
Masked secret key of the first keypair
z_x
Scalar
Masked plaintext message
z_r
Scalar
Masked randomness of the second ciphertext

Proof Data Context

pub struct CiphertextCiphertextEqualityProofContext {
    pub first_pubkey: PodElGamalPubkey,        // 32 bytes
    pub second_pubkey: PodElGamalPubkey,       // 32 bytes
    pub first_ciphertext: PodElGamalCiphertext,  // 64 bytes
    pub second_ciphertext: PodElGamalCiphertext, // 64 bytes
}

Generating a Proof

use solana_zk_sdk::{
    encryption::{
        elgamal::{ElGamalKeypair, ElGamalPubkey},
        pedersen::PedersenOpening,
    },
    zk_elgamal_proof_program::proof_data::CiphertextCiphertextEqualityProofData,
};

let first_keypair = ElGamalKeypair::new_rand();
let second_keypair = ElGamalKeypair::new_rand();
let message: u64 = 55;

// Encrypt message with first keypair
let first_ciphertext = first_keypair.pubkey().encrypt(message);

// Encrypt same message with second keypair (keeping the opening)
let second_opening = PedersenOpening::new_rand();
let second_ciphertext = second_keypair
    .pubkey()
    .encrypt_with(message, &second_opening);

// Generate proof of equality
let proof_data = CiphertextCiphertextEqualityProofData::new(
    &first_keypair,
    second_keypair.pubkey(),
    &first_ciphertext,
    &second_ciphertext,
    &second_opening,
    message,
)?;

Verification

The verification performs a batch check of multiple algebraic relations using challenge scalars c, w, ww, and www:
z_s*P_first - c*H = Y_0
w*z_x*G + w*z_s*D_first - w*c*C_first = w*Y_1
ww*z_x*G + ww*z_r*H - ww*c*C_second = ww*Y_2
www*z_r*P_second - www*c*D_second = www*Y_3
The verification rejects identity points for public keys and the first ciphertext. However, the second ciphertext is allowed to be the identity point, as this is a common state in Token-2022.

Use Cases

  • Token transfers: Proving the amount withdrawn from one account equals the amount deposited to another
  • Cross-account validation: Verifying consistency between encrypted balances across different accounts
  • Confidential transactions: Ensuring input and output amounts match in private transactions
  • Multi-party computations: Proving encrypted shares represent the same value

Security Considerations

The prover must have:
  1. The secret key for the first ElGamal keypair
  2. The Pedersen opening used for the second ciphertext
Without both pieces of information, generating a valid proof is computationally infeasible.

Proof Size

Total size: 224 bytes (7 × 32 bytes)
  • 4 Ristretto points (128 bytes)
  • 3 scalars (96 bytes)

Source Code

Sigma proof implementation: zk-sdk/src/sigma_proofs/ciphertext_ciphertext_equality.rs:41 Proof data structure: zk-sdk/src/zk_elgamal_proof_program/proof_data/ciphertext_ciphertext_equality.rs:39

Build docs developers (and LLMs) love