Browse Source

[zk-token-sdk] Add `VerifyGroupedCiphertext2HandlesValidity` and `VerifyBatchedGroupedCiphertext2HandlesValidity` proof instructions (#31816)

* add grouped ciphertext validity proof data

* add batched grouped ciphertext validity proof data

* rename proof contexts and data for consistency

* add grouped ciphertext validity proof instructions

* Update zk-token-sdk/src/instruction/batched_grouped_ciphertext_validity.rs

Co-authored-by: Tyera <teulberg@gmail.com>

---------

Co-authored-by: Tyera <teulberg@gmail.com>
samkim-crypto 2 years ago
parent
commit
0495051a67

+ 126 - 1
programs/zk-token-proof-tests/tests/process_transaction.rs

@@ -11,6 +11,7 @@ use {
     solana_zk_token_sdk::{
         encryption::{
             elgamal::ElGamalKeypair,
+            grouped_elgamal::GroupedElGamal,
             pedersen::{Pedersen, PedersenOpening},
         },
         instruction::*,
@@ -21,7 +22,7 @@ use {
     std::mem::size_of,
 };
 
-const VERIFY_INSTRUCTION_TYPES: [ProofInstruction; 11] = [
+const VERIFY_INSTRUCTION_TYPES: [ProofInstruction; 13] = [
     ProofInstruction::VerifyZeroBalance,
     ProofInstruction::VerifyWithdraw,
     ProofInstruction::VerifyCiphertextCiphertextEquality,
@@ -33,6 +34,8 @@ const VERIFY_INSTRUCTION_TYPES: [ProofInstruction; 11] = [
     ProofInstruction::VerifyBatchedRangeProofU128,
     ProofInstruction::VerifyBatchedRangeProofU256,
     ProofInstruction::VerifyCiphertextCommitmentEquality,
+    ProofInstruction::VerifyGroupedCiphertext2HandlesValidity,
+    ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity,
 ];
 
 #[tokio::test]
@@ -572,6 +575,128 @@ async fn test_ciphertext_commitment_equality() {
     .await;
 }
 
+#[tokio::test]
+async fn test_grouped_ciphertext_2_handles_validity() {
+    let destination_pubkey = ElGamalKeypair::new_rand().public;
+    let auditor_pubkey = ElGamalKeypair::new_rand().public;
+
+    let amount: u64 = 55;
+    let opening = PedersenOpening::new_rand();
+    let grouped_ciphertext =
+        GroupedElGamal::encrypt_with([&destination_pubkey, &auditor_pubkey], amount, &opening);
+
+    let success_proof_data = GroupedCiphertext2HandlesValidityProofData::new(
+        &destination_pubkey,
+        &auditor_pubkey,
+        &grouped_ciphertext,
+        amount,
+        &opening,
+    )
+    .unwrap();
+
+    let incorrect_opening = PedersenOpening::new_rand();
+    let fail_proof_data = GroupedCiphertext2HandlesValidityProofData::new(
+        &destination_pubkey,
+        &auditor_pubkey,
+        &grouped_ciphertext,
+        amount,
+        &incorrect_opening,
+    )
+    .unwrap();
+
+    test_verify_proof_without_context(
+        ProofInstruction::VerifyGroupedCiphertext2HandlesValidity,
+        &success_proof_data,
+        &fail_proof_data,
+    )
+    .await;
+
+    test_verify_proof_with_context(
+        ProofInstruction::VerifyGroupedCiphertext2HandlesValidity,
+        size_of::<ProofContextState<GroupedCiphertext2HandlesValidityProofContext>>(),
+        &success_proof_data,
+        &fail_proof_data,
+    )
+    .await;
+
+    test_close_context_state(
+        ProofInstruction::VerifyGroupedCiphertext2HandlesValidity,
+        size_of::<ProofContextState<GroupedCiphertext2HandlesValidityProofContext>>(),
+        &success_proof_data,
+    )
+    .await;
+}
+
+#[tokio::test]
+async fn test_batched_grouped_ciphertext_2_handles_validity() {
+    let destination_pubkey = ElGamalKeypair::new_rand().public;
+    let auditor_pubkey = ElGamalKeypair::new_rand().public;
+
+    let amount_lo: u64 = 55;
+    let amount_hi: u64 = 22;
+
+    let opening_lo = PedersenOpening::new_rand();
+    let opening_hi = PedersenOpening::new_rand();
+
+    let grouped_ciphertext_lo = GroupedElGamal::encrypt_with(
+        [&destination_pubkey, &auditor_pubkey],
+        amount_lo,
+        &opening_lo,
+    );
+    let grouped_ciphertext_hi = GroupedElGamal::encrypt_with(
+        [&destination_pubkey, &auditor_pubkey],
+        amount_hi,
+        &opening_hi,
+    );
+
+    let success_proof_data = BatchedGroupedCiphertext2HandlesValidityProofData::new(
+        &destination_pubkey,
+        &auditor_pubkey,
+        &grouped_ciphertext_lo,
+        &grouped_ciphertext_hi,
+        amount_lo,
+        amount_hi,
+        &opening_lo,
+        &opening_hi,
+    )
+    .unwrap();
+
+    let incorrect_opening = PedersenOpening::new_rand();
+    let fail_proof_data = BatchedGroupedCiphertext2HandlesValidityProofData::new(
+        &destination_pubkey,
+        &auditor_pubkey,
+        &grouped_ciphertext_lo,
+        &grouped_ciphertext_hi,
+        amount_lo,
+        amount_hi,
+        &incorrect_opening,
+        &opening_hi,
+    )
+    .unwrap();
+
+    test_verify_proof_without_context(
+        ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity,
+        &success_proof_data,
+        &fail_proof_data,
+    )
+    .await;
+
+    test_verify_proof_with_context(
+        ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity,
+        size_of::<ProofContextState<BatchedGroupedCiphertext2HandlesValidityProofContext>>(),
+        &success_proof_data,
+        &fail_proof_data,
+    )
+    .await;
+
+    test_close_context_state(
+        ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity,
+        size_of::<ProofContextState<BatchedGroupedCiphertext2HandlesValidityProofContext>>(),
+        &success_proof_data,
+    )
+    .await;
+}
+
 async fn test_verify_proof_without_context<T, U>(
     proof_instruction: ProofInstruction,
     success_proof_data: &T,

+ 23 - 0
programs/zk-token-proof/src/lib.rs

@@ -244,5 +244,28 @@ declare_process_instruction!(process_instruction, 0, |invoke_context| {
                 CiphertextCommitmentEqualityProofContext,
             >(invoke_context)
         }
+        ProofInstruction::VerifyGroupedCiphertext2HandlesValidity => {
+            invoke_context
+                .consume_checked(6_440)
+                .map_err(|_| InstructionError::ComputationalBudgetExceeded)?;
+            ic_msg!(invoke_context, "VerifyGroupedCiphertext2HandlesValidity");
+            process_verify_proof::<
+                GroupedCiphertext2HandlesValidityProofData,
+                GroupedCiphertext2HandlesValidityProofContext,
+            >(invoke_context)
+        }
+        ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity => {
+            invoke_context
+                .consume_checked(12_575)
+                .map_err(|_| InstructionError::ComputationalBudgetExceeded)?;
+            ic_msg!(
+                invoke_context,
+                "VerifyBatchedGroupedCiphertext2HandlesValidity"
+            );
+            process_verify_proof::<
+                BatchedGroupedCiphertext2HandlesValidityProofData,
+                BatchedGroupedCiphertext2HandlesValidityProofContext,
+            >(invoke_context)
+        }
     }
 });

+ 206 - 0
zk-token-sdk/src/instruction/batched_grouped_ciphertext_validity.rs

@@ -0,0 +1,206 @@
+//! The batched grouped-ciphertext validity proof instruction.
+//!
+//! A batched grouped-ciphertext validity proof certifies the validity of two grouped ElGamal
+//! ciphertext that are encrypted using the same set of ElGamal public keys. A batched
+//! grouped-ciphertext validity proof is shorter and more efficient than two individual
+//! grouped-ciphertext validity proofs.
+//!
+//! Currently, the batched grouped-ciphertext validity proof is restricted to ciphertexts with two
+//! handles. In accordance with the SPL Token program application, the first decryption handle
+//! associated with the proof is referred to as the "destination" handle and the second decryption
+//! handle is referred to as the "auditor" handle. Furthermore, the first grouped ciphertext is
+//! referred to as the "lo" ciphertext and the second grouped ciphertext is referred to as the "hi"
+//! ciphertext.
+
+#[cfg(not(target_os = "solana"))]
+use {
+    crate::{
+        encryption::{
+            elgamal::ElGamalPubkey, grouped_elgamal::GroupedElGamalCiphertext,
+            pedersen::PedersenOpening,
+        },
+        errors::ProofError,
+        sigma_proofs::validity_proof::AggregatedValidityProof,
+        transcript::TranscriptProtocol,
+    },
+    merlin::Transcript,
+};
+use {
+    crate::{
+        instruction::{ProofType, ZkProofData},
+        zk_token_elgamal::pod,
+    },
+    bytemuck::{Pod, Zeroable},
+};
+
+/// The instruction data that is needed for the
+/// `ProofInstruction::VerifyBatchedGroupedCiphertextValidity` instruction.
+///
+/// It includes the cryptographic proof as well as the context data information needed to verify
+/// the proof.
+#[derive(Clone, Copy, Pod, Zeroable)]
+#[repr(C)]
+pub struct BatchedGroupedCiphertext2HandlesValidityProofData {
+    pub context: BatchedGroupedCiphertext2HandlesValidityProofContext,
+
+    pub proof: pod::AggregatedValidityProof,
+}
+
+#[derive(Clone, Copy, Pod, Zeroable)]
+#[repr(C)]
+pub struct BatchedGroupedCiphertext2HandlesValidityProofContext {
+    pub destination_pubkey: pod::ElGamalPubkey, // 32 bytes
+
+    pub auditor_pubkey: pod::ElGamalPubkey, // 32 bytes
+
+    pub grouped_ciphertext_lo: pod::GroupedElGamalCiphertext2Handles, // 96 bytes
+
+    pub grouped_ciphertext_hi: pod::GroupedElGamalCiphertext2Handles, // 96 bytes
+}
+
+#[cfg(not(target_os = "solana"))]
+impl BatchedGroupedCiphertext2HandlesValidityProofData {
+    pub fn new(
+        destination_pubkey: &ElGamalPubkey,
+        auditor_pubkey: &ElGamalPubkey,
+        grouped_ciphertext_lo: &GroupedElGamalCiphertext<2>,
+        grouped_ciphertext_hi: &GroupedElGamalCiphertext<2>,
+        amount_lo: u64,
+        amount_hi: u64,
+        opening_lo: &PedersenOpening,
+        opening_hi: &PedersenOpening,
+    ) -> Result<Self, ProofError> {
+        let pod_destination_pubkey = pod::ElGamalPubkey(destination_pubkey.to_bytes());
+        let pod_auditor_pubkey = pod::ElGamalPubkey(auditor_pubkey.to_bytes());
+        let pod_grouped_ciphertext_lo = (*grouped_ciphertext_lo).into();
+        let pod_grouped_ciphertext_hi = (*grouped_ciphertext_hi).into();
+
+        let context = BatchedGroupedCiphertext2HandlesValidityProofContext {
+            destination_pubkey: pod_destination_pubkey,
+            auditor_pubkey: pod_auditor_pubkey,
+            grouped_ciphertext_lo: pod_grouped_ciphertext_lo,
+            grouped_ciphertext_hi: pod_grouped_ciphertext_hi,
+        };
+
+        let mut transcript = context.new_transcript();
+
+        let proof = AggregatedValidityProof::new(
+            (destination_pubkey, auditor_pubkey),
+            (amount_lo, amount_hi),
+            (opening_lo, opening_hi),
+            &mut transcript,
+        )
+        .into();
+
+        Ok(Self { context, proof })
+    }
+}
+
+impl ZkProofData<BatchedGroupedCiphertext2HandlesValidityProofContext>
+    for BatchedGroupedCiphertext2HandlesValidityProofData
+{
+    const PROOF_TYPE: ProofType = ProofType::BatchedGroupedCiphertext2HandlesValidity;
+
+    fn context_data(&self) -> &BatchedGroupedCiphertext2HandlesValidityProofContext {
+        &self.context
+    }
+
+    #[cfg(not(target_os = "solana"))]
+    fn verify_proof(&self) -> Result<(), ProofError> {
+        let mut transcript = self.context.new_transcript();
+
+        let destination_pubkey = self.context.destination_pubkey.try_into()?;
+        let auditor_pubkey = self.context.auditor_pubkey.try_into()?;
+        let grouped_ciphertext_lo: GroupedElGamalCiphertext<2> =
+            self.context.grouped_ciphertext_lo.try_into()?;
+        let grouped_ciphertext_hi: GroupedElGamalCiphertext<2> =
+            self.context.grouped_ciphertext_hi.try_into()?;
+
+        let destination_handle_lo = grouped_ciphertext_lo.handles.get(0).unwrap();
+        let auditor_handle_lo = grouped_ciphertext_lo.handles.get(1).unwrap();
+
+        let destination_handle_hi = grouped_ciphertext_hi.handles.get(0).unwrap();
+        let auditor_handle_hi = grouped_ciphertext_hi.handles.get(1).unwrap();
+
+        let proof: AggregatedValidityProof = self.proof.try_into()?;
+
+        proof
+            .verify(
+                (&destination_pubkey, &auditor_pubkey),
+                (
+                    &grouped_ciphertext_lo.commitment,
+                    &grouped_ciphertext_hi.commitment,
+                ),
+                (destination_handle_lo, destination_handle_hi),
+                (auditor_handle_lo, auditor_handle_hi),
+                &mut transcript,
+            )
+            .map_err(|e| e.into())
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl BatchedGroupedCiphertext2HandlesValidityProofContext {
+    fn new_transcript(&self) -> Transcript {
+        let mut transcript = Transcript::new(b"BatchedGroupedCiphertextValidityProof");
+
+        transcript.append_pubkey(b"destination-pubkey", &self.destination_pubkey);
+        transcript.append_pubkey(b"auditor-pubkey", &self.auditor_pubkey);
+        transcript.append_grouped_ciphertext_2_handles(
+            b"grouped-ciphertext-lo",
+            &self.grouped_ciphertext_lo,
+        );
+        transcript.append_grouped_ciphertext_2_handles(
+            b"grouped-ciphertext-hi",
+            &self.grouped_ciphertext_hi,
+        );
+
+        transcript
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use {
+        super::*,
+        crate::encryption::{elgamal::ElGamalKeypair, grouped_elgamal::GroupedElGamal},
+    };
+
+    #[test]
+    fn test_ciphertext_validity_proof_instruction_correctness() {
+        let destination_pubkey = ElGamalKeypair::new_rand().public;
+        let auditor_pubkey = ElGamalKeypair::new_rand().public;
+
+        let amount_lo: u64 = 11;
+        let amount_hi: u64 = 22;
+
+        let opening_lo = PedersenOpening::new_rand();
+        let opening_hi = PedersenOpening::new_rand();
+
+        let grouped_ciphertext_lo = GroupedElGamal::encrypt_with(
+            [&destination_pubkey, &auditor_pubkey],
+            amount_lo,
+            &opening_lo,
+        );
+
+        let grouped_ciphertext_hi = GroupedElGamal::encrypt_with(
+            [&destination_pubkey, &auditor_pubkey],
+            amount_hi,
+            &opening_hi,
+        );
+
+        let proof_data = BatchedGroupedCiphertext2HandlesValidityProofData::new(
+            &destination_pubkey,
+            &auditor_pubkey,
+            &grouped_ciphertext_lo,
+            &grouped_ciphertext_hi,
+            amount_lo,
+            amount_hi,
+            &opening_lo,
+            &opening_hi,
+        )
+        .unwrap();
+
+        assert!(proof_data.verify_proof().is_ok());
+    }
+}

+ 166 - 0
zk-token-sdk/src/instruction/grouped_ciphertext_validity.rs

@@ -0,0 +1,166 @@
+//! The grouped-ciphertext validity proof instruction.
+//!
+//! A grouped-ciphertext validity proof certifies that a grouped ElGamal ciphertext is
+//! well-defined, i.e. the ciphertext can be decrypted by private keys associated with its
+//! decryption handles. To generate the proof, a prover must provide the Pedersen opening
+//! associated with the grouped ciphertext's commitment.
+//!
+//! Currently, the grouped-ciphertext validity proof is restricted to ciphertexts with two handles.
+//! In accordance with the SPL Token program application, the first decryption handle associated
+//! with the proof is referred to as the "destination" handle and the second decryption handle is
+//! referred to as the "auditor" handle.
+
+#[cfg(not(target_os = "solana"))]
+use {
+    crate::{
+        encryption::{
+            elgamal::ElGamalPubkey, grouped_elgamal::GroupedElGamalCiphertext,
+            pedersen::PedersenOpening,
+        },
+        errors::ProofError,
+        sigma_proofs::validity_proof::ValidityProof,
+        transcript::TranscriptProtocol,
+    },
+    merlin::Transcript,
+};
+use {
+    crate::{
+        instruction::{ProofType, ZkProofData},
+        zk_token_elgamal::pod,
+    },
+    bytemuck::{Pod, Zeroable},
+};
+
+/// The instruction data that is needed for the `ProofInstruction::VerifyGroupedCiphertextValidity`
+/// instruction.
+///
+/// It includes the cryptographic proof as well as the context data information needed to verify
+/// the proof.
+#[derive(Clone, Copy, Pod, Zeroable)]
+#[repr(C)]
+pub struct GroupedCiphertext2HandlesValidityProofData {
+    pub context: GroupedCiphertext2HandlesValidityProofContext,
+
+    pub proof: pod::ValidityProof,
+}
+
+#[derive(Clone, Copy, Pod, Zeroable)]
+#[repr(C)]
+pub struct GroupedCiphertext2HandlesValidityProofContext {
+    pub destination_pubkey: pod::ElGamalPubkey, // 32 bytes
+
+    pub auditor_pubkey: pod::ElGamalPubkey, // 32 bytes
+
+    pub grouped_ciphertext: pod::GroupedElGamalCiphertext2Handles, // 96 bytes
+}
+
+#[cfg(not(target_os = "solana"))]
+impl GroupedCiphertext2HandlesValidityProofData {
+    pub fn new(
+        destination_pubkey: &ElGamalPubkey,
+        auditor_pubkey: &ElGamalPubkey,
+        grouped_ciphertext: &GroupedElGamalCiphertext<2>,
+        amount: u64,
+        opening: &PedersenOpening,
+    ) -> Result<Self, ProofError> {
+        let pod_destination_pubkey = pod::ElGamalPubkey(destination_pubkey.to_bytes());
+        let pod_auditor_pubkey = pod::ElGamalPubkey(auditor_pubkey.to_bytes());
+        let pod_grouped_ciphertext = (*grouped_ciphertext).into();
+
+        let context = GroupedCiphertext2HandlesValidityProofContext {
+            destination_pubkey: pod_destination_pubkey,
+            auditor_pubkey: pod_auditor_pubkey,
+            grouped_ciphertext: pod_grouped_ciphertext,
+        };
+
+        let mut transcript = context.new_transcript();
+
+        let proof = ValidityProof::new(
+            (destination_pubkey, auditor_pubkey),
+            amount,
+            opening,
+            &mut transcript,
+        )
+        .into();
+
+        Ok(Self { context, proof })
+    }
+}
+
+impl ZkProofData<GroupedCiphertext2HandlesValidityProofContext>
+    for GroupedCiphertext2HandlesValidityProofData
+{
+    const PROOF_TYPE: ProofType = ProofType::GroupedCiphertext2HandlesValidity;
+
+    fn context_data(&self) -> &GroupedCiphertext2HandlesValidityProofContext {
+        &self.context
+    }
+
+    #[cfg(not(target_os = "solana"))]
+    fn verify_proof(&self) -> Result<(), ProofError> {
+        let mut transcript = self.context.new_transcript();
+
+        let destination_pubkey = self.context.destination_pubkey.try_into()?;
+        let auditor_pubkey = self.context.auditor_pubkey.try_into()?;
+        let grouped_ciphertext: GroupedElGamalCiphertext<2> =
+            self.context.grouped_ciphertext.try_into()?;
+
+        let destination_handle = grouped_ciphertext.handles.get(0).unwrap();
+        let auditor_handle = grouped_ciphertext.handles.get(1).unwrap();
+
+        let proof: ValidityProof = self.proof.try_into()?;
+
+        proof
+            .verify(
+                &grouped_ciphertext.commitment,
+                (&destination_pubkey, &auditor_pubkey),
+                (destination_handle, auditor_handle),
+                &mut transcript,
+            )
+            .map_err(|e| e.into())
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl GroupedCiphertext2HandlesValidityProofContext {
+    fn new_transcript(&self) -> Transcript {
+        let mut transcript = Transcript::new(b"CiphertextValidityProof");
+
+        transcript.append_pubkey(b"destination-pubkey", &self.destination_pubkey);
+        transcript.append_pubkey(b"auditor-pubkey", &self.auditor_pubkey);
+        transcript
+            .append_grouped_ciphertext_2_handles(b"grouped-ciphertext", &self.grouped_ciphertext);
+
+        transcript
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use {
+        super::*,
+        crate::encryption::{elgamal::ElGamalKeypair, grouped_elgamal::GroupedElGamal},
+    };
+
+    #[test]
+    fn test_ciphertext_validity_proof_instruction_correctness() {
+        let destination_pubkey = ElGamalKeypair::new_rand().public;
+        let auditor_pubkey = ElGamalKeypair::new_rand().public;
+
+        let amount: u64 = 55;
+        let opening = PedersenOpening::new_rand();
+        let grouped_ciphertext =
+            GroupedElGamal::encrypt_with([&destination_pubkey, &auditor_pubkey], amount, &opening);
+
+        let proof_data = GroupedCiphertext2HandlesValidityProofData::new(
+            &destination_pubkey,
+            &auditor_pubkey,
+            &grouped_ciphertext,
+            amount,
+            &opening,
+        )
+        .unwrap();
+
+        assert!(proof_data.verify_proof().is_ok());
+    }
+}

+ 11 - 0
zk-token-sdk/src/instruction/mod.rs

@@ -1,6 +1,8 @@
+pub mod batched_grouped_ciphertext_validity;
 pub mod batched_range_proof;
 pub mod ciphertext_ciphertext_equality;
 pub mod ciphertext_commitment_equality;
+pub mod grouped_ciphertext_validity;
 pub mod pubkey_validity;
 pub mod range_proof;
 pub mod transfer;
@@ -11,6 +13,10 @@ pub mod zero_balance;
 use crate::errors::ProofError;
 use num_derive::{FromPrimitive, ToPrimitive};
 pub use {
+    batched_grouped_ciphertext_validity::{
+        BatchedGroupedCiphertext2HandlesValidityProofContext,
+        BatchedGroupedCiphertext2HandlesValidityProofData,
+    },
     batched_range_proof::{
         batched_range_proof_u128::BatchedRangeProofU128Data,
         batched_range_proof_u256::BatchedRangeProofU256Data,
@@ -23,6 +29,9 @@ pub use {
     ciphertext_commitment_equality::{
         CiphertextCommitmentEqualityProofContext, CiphertextCommitmentEqualityProofData,
     },
+    grouped_ciphertext_validity::{
+        GroupedCiphertext2HandlesValidityProofContext, GroupedCiphertext2HandlesValidityProofData,
+    },
     pubkey_validity::{PubkeyValidityData, PubkeyValidityProofContext},
     range_proof::{RangeProofContext, RangeProofU64Data},
     transfer::{
@@ -49,6 +58,8 @@ pub enum ProofType {
     BatchedRangeProofU128,
     BatchedRangeProofU256,
     CiphertextCommitmentEquality,
+    GroupedCiphertext2HandlesValidity,
+    BatchedGroupedCiphertext2HandlesValidity,
 }
 
 pub trait ZkProofData<T: Pod> {

+ 15 - 0
zk-token-sdk/src/transcript.rs

@@ -37,6 +37,13 @@ pub trait TranscriptProtocol {
     /// Append an ElGamal ciphertext with the given `label`.
     fn append_ciphertext(&mut self, label: &'static [u8], point: &pod::ElGamalCiphertext);
 
+    /// Append a grouped ElGamal ciphertext with the given `label`.
+    fn append_grouped_ciphertext_2_handles(
+        &mut self,
+        label: &'static [u8],
+        point: &pod::GroupedElGamalCiphertext2Handles,
+    );
+
     /// Append a Pedersen commitment with the given `label`.
     fn append_commitment(&mut self, label: &'static [u8], point: &pod::PedersenCommitment);
 
@@ -137,6 +144,14 @@ impl TranscriptProtocol for Transcript {
         self.append_message(label, &ciphertext.0);
     }
 
+    fn append_grouped_ciphertext_2_handles(
+        &mut self,
+        label: &'static [u8],
+        grouped_ciphertext: &pod::GroupedElGamalCiphertext2Handles,
+    ) {
+        self.append_message(label, &grouped_ciphertext.0);
+    }
+
     fn append_commitment(&mut self, label: &'static [u8], commitment: &pod::PedersenCommitment) {
         self.append_message(label, &commitment.0);
     }

+ 51 - 0
zk-token-sdk/src/zk_token_proof_instruction.rs

@@ -276,6 +276,57 @@ pub enum ProofInstruction {
     ///   `CiphertextCommitmentEqualityProofData`
     ///
     VerifyCiphertextCommitmentEquality,
+
+    /// Verify a grouped-ciphertext validity proof.
+    ///
+    /// A grouped-ciphertext validity proof certifies that a grouped ElGamal ciphertext is
+    /// well-defined, i.e. the ciphertext can be decrypted by private keys associated with its
+    /// decryption handles.
+    ///
+    /// This instruction can be configured to optionally initialize a proof context state account.
+    /// If creating a context state account, an account must be pre-allocated to the exact size of
+    /// `ProofContextState<GroupedCiphertextValidityProofContext>` and assigned to the ZkToken
+    /// proof program prior to the execution of this instruction.
+    ///
+    /// Accounts expected by this instruction:
+    ///
+    ///   * Creating a proof context account
+    ///   0. `[writable]` The proof context account
+    ///   1. `[]` The proof context account owner
+    ///
+    ///   * Otherwise
+    ///   None
+    ///
+    /// Data expected by this instruction:
+    ///   `GroupedCiphertextValidityProofContext`
+    ///
+    VerifyGroupedCiphertext2HandlesValidity,
+
+    /// Verify a batched grouped-ciphertext validity proof.
+    ///
+    /// A batched grouped-ciphertext validity proof certifies the validity of two grouped ElGamal
+    /// ciphertext that are encrypted using the same set of ElGamal public keys. A batched
+    /// grouped-ciphertext validity proof is shorter and more efficient than two individual
+    /// grouped-ciphertext validity proofs.
+    ///
+    /// This instruction can be configured to optionally initialize a proof context state account.
+    /// If creating a context state account, an account must be pre-allocated to the exact size of
+    /// `ProofContextState<BatchedGroupedCiphertextValidityProofContext>` and assigned to the
+    /// ZkToken proof program prior to the execution of this instruction.
+    ///
+    /// Accounts expected by this instruction:
+    ///
+    ///   * Creating a proof context account
+    ///   0. `[writable]` The proof context account
+    ///   1. `[]` The proof context account owner
+    ///
+    ///   * Otherwise
+    ///   None
+    ///
+    /// Data expected by this instruction:
+    ///   `BatchedGroupedCiphertextValidityProofContext`
+    ///
+    VerifyBatchedGroupedCiphertext2HandlesValidity,
 }
 
 /// Pubkeys associated with a context state account to be used as parameters to functions.