From 7c0e38048af5b1d8155439ce7c948e8bb06b1f42 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Wed, 22 May 2024 17:04:54 +0900 Subject: [PATCH 1/7] add `ProofGenerationError` and `ProofVerificationError` --- zk-sdk/src/elgamal_program/errors.rs | 41 +++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/zk-sdk/src/elgamal_program/errors.rs b/zk-sdk/src/elgamal_program/errors.rs index 514140d56d2c87..7d7ef2041b6d34 100644 --- a/zk-sdk/src/elgamal_program/errors.rs +++ b/zk-sdk/src/elgamal_program/errors.rs @@ -1,7 +1,46 @@ -use thiserror::Error; +use { + crate::{ + errors::ElGamalError, + range_proof::errors::{RangeProofGenerationError, RangeProofVerificationError}, + sigma_proofs::errors::*, + }, + thiserror::Error, +}; + +#[cfg(not(target_os = "solana"))] +#[derive(Error, Clone, Debug, Eq, PartialEq)] +pub enum ProofGenerationError { + #[error("not enough funds in account")] + NotEnoughFunds, + #[error("transfer fee calculation error")] + FeeCalculation, + #[error("illegal number of commitments")] + IllegalCommitmentLength, + #[error("illegal amount bit length")] + IllegalAmountBitLength, + #[error("invalid commitment")] + InvalidCommitment, + #[error("range proof generation failed")] + RangeProof(#[from] RangeProofGenerationError), + #[error("unexpected proof length")] + ProofLength, +} #[derive(Error, Clone, Debug, Eq, PartialEq)] pub enum ProofVerificationError { + #[error("range proof verification failed")] + RangeProof(#[from] RangeProofVerificationError), + #[error("sigma proof verification failed")] + SigmaProof(SigmaProofType, SigmaProofVerificationError), + #[error("ElGamal ciphertext or public key error")] + ElGamal(#[from] ElGamalError), #[error("Invalid proof context")] ProofContext, + #[error("illegal commitment length")] + IllegalCommitmentLength, + #[error("illegal amount bit length")] + IllegalAmountBitLength, } + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SigmaProofType; From d70121472302650038442e13e2a6ba6da54e2d7a Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Wed, 22 May 2024 17:17:05 +0900 Subject: [PATCH 2/7] add `zero_ciphertext` proof data module from zk-token-sdk verbatim --- .../proof_data/zero_ciphertext.rs | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs diff --git a/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs b/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs new file mode 100644 index 00000000000000..d283d68a949b96 --- /dev/null +++ b/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs @@ -0,0 +1,123 @@ + +//! The zero-balance proof instruction. +//! +//! A zero-balance proof is defined with respect to a twisted ElGamal ciphertext. The proof +//! certifies that a given ciphertext encrypts the message 0 in the field (`Scalar::zero()`). To +//! generate the proof, a prover must provide the decryption key for the ciphertext. + +#[cfg(not(target_os = "solana"))] +use { + crate::{ + encryption::elgamal::{ElGamalCiphertext, ElGamalKeypair}, + errors::{ProofGenerationError, ProofVerificationError}, + sigma_proofs::zero_balance_proof::ZeroBalanceProof, + transcript::TranscriptProtocol, + }, + merlin::Transcript, + std::convert::TryInto, +}; +use { + crate::{ + instruction::{ProofType, ZkProofData}, + zk_token_elgamal::pod, + }, + bytemuck::{Pod, Zeroable}, +}; + +/// The instruction data that is needed for the `ProofInstruction::ZeroBalance` 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 ZeroBalanceProofData { + /// The context data for the zero-balance proof + pub context: ZeroBalanceProofContext, // 96 bytes + + /// Proof that the source account available balance is zero + pub proof: pod::ZeroBalanceProof, // 96 bytes +} + +/// The context data needed to verify a zero-balance proof. +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct ZeroBalanceProofContext { + /// The source account ElGamal pubkey + pub pubkey: pod::ElGamalPubkey, // 32 bytes + + /// The source account available balance in encrypted form + pub ciphertext: pod::ElGamalCiphertext, // 64 bytes +} + +#[cfg(not(target_os = "solana"))] +impl ZeroBalanceProofData { + pub fn new( + keypair: &ElGamalKeypair, + ciphertext: &ElGamalCiphertext, + ) -> Result { + let pod_pubkey = pod::ElGamalPubkey(keypair.pubkey().into()); + let pod_ciphertext = pod::ElGamalCiphertext(ciphertext.to_bytes()); + + let context = ZeroBalanceProofContext { + pubkey: pod_pubkey, + ciphertext: pod_ciphertext, + }; + + let mut transcript = context.new_transcript(); + let proof = ZeroBalanceProof::new(keypair, ciphertext, &mut transcript).into(); + + Ok(ZeroBalanceProofData { context, proof }) + } +} + +impl ZkProofData for ZeroBalanceProofData { + const PROOF_TYPE: ProofType = ProofType::ZeroBalance; + + fn context_data(&self) -> &ZeroBalanceProofContext { + &self.context + } + + #[cfg(not(target_os = "solana"))] + fn verify_proof(&self) -> Result<(), ProofVerificationError> { + let mut transcript = self.context.new_transcript(); + let pubkey = self.context.pubkey.try_into()?; + let ciphertext = self.context.ciphertext.try_into()?; + let proof: ZeroBalanceProof = self.proof.try_into()?; + proof + .verify(&pubkey, &ciphertext, &mut transcript) + .map_err(|e| e.into()) + } +} + +#[allow(non_snake_case)] +#[cfg(not(target_os = "solana"))] +impl ZeroBalanceProofContext { + fn new_transcript(&self) -> Transcript { + let mut transcript = Transcript::new(b"ZeroBalanceProof"); + + transcript.append_pubkey(b"pubkey", &self.pubkey); + transcript.append_ciphertext(b"ciphertext", &self.ciphertext); + + transcript + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_zero_balance_proof_instruction_correctness() { + let keypair = ElGamalKeypair::new_rand(); + + // general case: encryption of 0 + let ciphertext = keypair.pubkey().encrypt(0_u64); + let zero_balance_proof_data = ZeroBalanceProofData::new(&keypair, &ciphertext).unwrap(); + assert!(zero_balance_proof_data.verify_proof().is_ok()); + + // general case: encryption of > 0 + let ciphertext = keypair.pubkey().encrypt(1_u64); + let zero_balance_proof_data = ZeroBalanceProofData::new(&keypair, &ciphertext).unwrap(); + assert!(zero_balance_proof_data.verify_proof().is_err()); + } +} From 846888072a69292ece9c423fa4b252287ec50c37 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Wed, 22 May 2024 17:18:30 +0900 Subject: [PATCH 3/7] clean up `zero_ciphertext` proof data module --- zk-sdk/src/elgamal_program/errors.rs | 10 +- zk-sdk/src/elgamal_program/proof_data/mod.rs | 2 + .../proof_data/zero_ciphertext.rs | 92 ++++++++++--------- 3 files changed, 59 insertions(+), 45 deletions(-) diff --git a/zk-sdk/src/elgamal_program/errors.rs b/zk-sdk/src/elgamal_program/errors.rs index 7d7ef2041b6d34..49ed004d8fd65f 100644 --- a/zk-sdk/src/elgamal_program/errors.rs +++ b/zk-sdk/src/elgamal_program/errors.rs @@ -43,4 +43,12 @@ pub enum ProofVerificationError { } #[derive(Clone, Debug, Eq, PartialEq)] -pub struct SigmaProofType; +pub enum SigmaProofType { + ZeroCiphertext, +} + +impl From for ProofVerificationError { + fn from(err: ZeroCiphertextProofVerificationError) -> Self { + Self::SigmaProof(SigmaProofType::ZeroCiphertext, err.0) + } +} diff --git a/zk-sdk/src/elgamal_program/proof_data/mod.rs b/zk-sdk/src/elgamal_program/proof_data/mod.rs index da3989d5ef74ff..ea8bfbac72adf3 100644 --- a/zk-sdk/src/elgamal_program/proof_data/mod.rs +++ b/zk-sdk/src/elgamal_program/proof_data/mod.rs @@ -6,12 +6,14 @@ use { pub mod errors; pub mod pod; +pub mod zero_ciphertext; #[derive(Clone, Copy, Debug, FromPrimitive, ToPrimitive, PartialEq, Eq)] #[repr(u8)] pub enum ProofType { /// Empty proof type used to distinguish if a proof context account is initialized Uninitialized, + ZeroCiphertext, } pub trait ZkProofData { diff --git a/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs b/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs index d283d68a949b96..1e4894b6f982fa 100644 --- a/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs +++ b/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs @@ -1,79 +1,80 @@ - -//! The zero-balance proof instruction. +//! The zero-ciphertext proof instruction. //! -//! A zero-balance proof is defined with respect to a twisted ElGamal ciphertext. The proof +//! A zero-ciphertext proof is defined with respect to a twisted ElGamal ciphertext. The proof //! certifies that a given ciphertext encrypts the message 0 in the field (`Scalar::zero()`). To //! generate the proof, a prover must provide the decryption key for the ciphertext. -#[cfg(not(target_os = "solana"))] use { crate::{ - encryption::elgamal::{ElGamalCiphertext, ElGamalKeypair}, - errors::{ProofGenerationError, ProofVerificationError}, - sigma_proofs::zero_balance_proof::ZeroBalanceProof, - transcript::TranscriptProtocol, + elgamal_program::{ + errors::{ProofGenerationError, ProofVerificationError}, + proof_data::{ProofType, ZkProofData}, + }, + encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + sigma_proofs::pod::PodZeroCiphertextProof, }, - merlin::Transcript, - std::convert::TryInto, + bytemuck::{bytes_of, Pod, Zeroable}, }; +#[cfg(not(target_os = "solana"))] use { crate::{ - instruction::{ProofType, ZkProofData}, - zk_token_elgamal::pod, + encryption::elgamal::{ElGamalCiphertext, ElGamalKeypair}, + sigma_proofs::zero_ciphertext::ZeroCiphertextProof, }, - bytemuck::{Pod, Zeroable}, + merlin::Transcript, + std::convert::TryInto, }; -/// The instruction data that is needed for the `ProofInstruction::ZeroBalance` instruction. +/// The instruction data that is needed for the `ProofInstruction::ZeroCiphertext` 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 ZeroBalanceProofData { - /// The context data for the zero-balance proof - pub context: ZeroBalanceProofContext, // 96 bytes +pub struct ZeroCiphertextProofData { + /// The context data for the zero-ciphertext proof + pub context: ZeroCiphertextProofContext, // 96 bytes - /// Proof that the source account available balance is zero - pub proof: pod::ZeroBalanceProof, // 96 bytes + /// Proof that the ciphertext is zero + pub proof: PodZeroCiphertextProof, // 96 bytes } -/// The context data needed to verify a zero-balance proof. +/// The context data needed to verify a zero-ciphertext proof. #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] -pub struct ZeroBalanceProofContext { - /// The source account ElGamal pubkey - pub pubkey: pod::ElGamalPubkey, // 32 bytes +pub struct ZeroCiphertextProofContext { + /// The ElGamal pubkey associated with the ElGamal ciphertext + pub pubkey: PodElGamalPubkey, // 32 bytes - /// The source account available balance in encrypted form - pub ciphertext: pod::ElGamalCiphertext, // 64 bytes + /// The ElGamal ciphertext that encrypts zero + pub ciphertext: PodElGamalCiphertext, // 64 bytes } #[cfg(not(target_os = "solana"))] -impl ZeroBalanceProofData { +impl ZeroCiphertextProofData { pub fn new( keypair: &ElGamalKeypair, ciphertext: &ElGamalCiphertext, ) -> Result { - let pod_pubkey = pod::ElGamalPubkey(keypair.pubkey().into()); - let pod_ciphertext = pod::ElGamalCiphertext(ciphertext.to_bytes()); + let pod_pubkey = PodElGamalPubkey(keypair.pubkey().into()); + let pod_ciphertext = PodElGamalCiphertext(ciphertext.to_bytes()); - let context = ZeroBalanceProofContext { + let context = ZeroCiphertextProofContext { pubkey: pod_pubkey, ciphertext: pod_ciphertext, }; let mut transcript = context.new_transcript(); - let proof = ZeroBalanceProof::new(keypair, ciphertext, &mut transcript).into(); + let proof = ZeroCiphertextProof::new(keypair, ciphertext, &mut transcript).into(); - Ok(ZeroBalanceProofData { context, proof }) + Ok(ZeroCiphertextProofData { context, proof }) } } -impl ZkProofData for ZeroBalanceProofData { - const PROOF_TYPE: ProofType = ProofType::ZeroBalance; +impl ZkProofData for ZeroCiphertextProofData { + const PROOF_TYPE: ProofType = ProofType::ZeroCiphertext; - fn context_data(&self) -> &ZeroBalanceProofContext { + fn context_data(&self) -> &ZeroCiphertextProofContext { &self.context } @@ -82,7 +83,7 @@ impl ZkProofData for ZeroBalanceProofData { let mut transcript = self.context.new_transcript(); let pubkey = self.context.pubkey.try_into()?; let ciphertext = self.context.ciphertext.try_into()?; - let proof: ZeroBalanceProof = self.proof.try_into()?; + let proof: ZeroCiphertextProof = self.proof.try_into()?; proof .verify(&pubkey, &ciphertext, &mut transcript) .map_err(|e| e.into()) @@ -91,12 +92,12 @@ impl ZkProofData for ZeroBalanceProofData { #[allow(non_snake_case)] #[cfg(not(target_os = "solana"))] -impl ZeroBalanceProofContext { +impl ZeroCiphertextProofContext { fn new_transcript(&self) -> Transcript { - let mut transcript = Transcript::new(b"ZeroBalanceProof"); + let mut transcript = Transcript::new(b"zero-ciphertext-instruction"); - transcript.append_pubkey(b"pubkey", &self.pubkey); - transcript.append_ciphertext(b"ciphertext", &self.ciphertext); + transcript.append_message(b"pubkey", bytes_of(&self.pubkey)); + transcript.append_message(b"ciphertext", bytes_of(&self.ciphertext)); transcript } @@ -107,17 +108,20 @@ mod test { use super::*; #[test] - fn test_zero_balance_proof_instruction_correctness() { + fn test_zero_ciphertext_proof_instruction_correctness() { let keypair = ElGamalKeypair::new_rand(); // general case: encryption of 0 let ciphertext = keypair.pubkey().encrypt(0_u64); - let zero_balance_proof_data = ZeroBalanceProofData::new(&keypair, &ciphertext).unwrap(); - assert!(zero_balance_proof_data.verify_proof().is_ok()); + let zero_ciphertext_proof_data = + ZeroCiphertextProofData::new(&keypair, &ciphertext).unwrap(); + assert!(zero_ciphertext_proof_data.verify_proof().is_ok()); // general case: encryption of > 0 let ciphertext = keypair.pubkey().encrypt(1_u64); - let zero_balance_proof_data = ZeroBalanceProofData::new(&keypair, &ciphertext).unwrap(); - assert!(zero_balance_proof_data.verify_proof().is_err()); + let zero_ciphertext_proof_data = + ZeroCiphertextProofData::new(&keypair, &ciphertext).unwrap(); + assert!(zero_ciphertext_proof_data.verify_proof().is_err()); } } + From 25c3c88b9d88840831f50b1b8f135522635b74eb Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Wed, 22 May 2024 17:22:55 +0900 Subject: [PATCH 4/7] add `VerifyZeroCiphertext` instruction --- zk-sdk/src/elgamal_program/instruction.rs | 92 ++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/zk-sdk/src/elgamal_program/instruction.rs b/zk-sdk/src/elgamal_program/instruction.rs index 5a94b4a0d80343..fd3f8328bef4bf 100644 --- a/zk-sdk/src/elgamal_program/instruction.rs +++ b/zk-sdk/src/elgamal_program/instruction.rs @@ -33,8 +33,10 @@ //! [`context-state`]: https://docs.solanalabs.com/runtime/zk-token-proof#context-data use { + crate::elgamal_program::proof_data::ZkProofData, + bytemuck::{bytes_of, Pod}, num_derive::{FromPrimitive, ToPrimitive}, - num_traits::ToPrimitive, + num_traits::{FromPrimitive, ToPrimitive}, solana_program::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, @@ -55,6 +57,22 @@ pub enum ProofInstruction { /// None /// CloseContextState, + + /// Verify a zero-ciphertext proof. + /// + /// A zero-ciphertext proof certifies that an ElGamal ciphertext encrypts the value zero. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[]` (Optional) Account to read the proof from + /// 1. `[writable]` (Optional) The proof context account + /// 2. `[]` (Optional) The proof context account owner + /// + /// The instruction expects either: + /// i. `ZeroCiphertextProofData` if proof is provided as instruction data + /// ii. `u32` byte offset if proof is provided as an account + /// + VerifyZeroCiphertext, } /// Pubkeys associated with a context state account to be used as parameters to functions. @@ -83,3 +101,75 @@ pub fn close_context_state( data, } } + +impl ProofInstruction { + pub fn encode_verify_proof( + &self, + context_state_info: Option, + proof_data: &T, + ) -> Instruction + where + T: Pod + ZkProofData, + U: Pod, + { + let accounts = if let Some(context_state_info) = context_state_info { + vec![ + AccountMeta::new(*context_state_info.context_state_account, false), + AccountMeta::new_readonly(*context_state_info.context_state_authority, false), + ] + } else { + vec![] + }; + + let mut data = vec![ToPrimitive::to_u8(self).unwrap()]; + data.extend_from_slice(bytes_of(proof_data)); + + Instruction { + program_id: crate::elgamal_program::id(), + accounts, + data, + } + } + + pub fn encode_verify_proof_from_account( + &self, + context_state_info: Option, + proof_account: &Pubkey, + offset: u32, + ) -> Instruction { + let accounts = if let Some(context_state_info) = context_state_info { + vec![ + AccountMeta::new(*proof_account, false), + AccountMeta::new(*context_state_info.context_state_account, false), + AccountMeta::new_readonly(*context_state_info.context_state_authority, false), + ] + } else { + vec![AccountMeta::new(*proof_account, false)] + }; + + let mut data = vec![ToPrimitive::to_u8(self).unwrap()]; + data.extend_from_slice(&offset.to_le_bytes()); + + Instruction { + program_id: crate::elgamal_program::id(), + accounts, + data, + } + } + + pub fn instruction_type(input: &[u8]) -> Option { + input + .first() + .and_then(|instruction| FromPrimitive::from_u8(*instruction)) + } + + pub fn proof_data(input: &[u8]) -> Option<&T> + where + T: Pod + ZkProofData, + U: Pod, + { + input + .get(1..) + .and_then(|data| bytemuck::try_from_bytes(data).ok()) + } +} From bcca0d30da918fd394a0f35728607f11bce85016 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Wed, 22 May 2024 17:25:42 +0900 Subject: [PATCH 5/7] cargo fmt --- zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs b/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs index 1e4894b6f982fa..c511c421b58eb4 100644 --- a/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs +++ b/zk-sdk/src/elgamal_program/proof_data/zero_ciphertext.rs @@ -124,4 +124,3 @@ mod test { assert!(zero_ciphertext_proof_data.verify_proof().is_err()); } } - From 40be06975c7a750809af38a6a09a0f00cfcea0a0 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Thu, 23 May 2024 07:53:08 +0900 Subject: [PATCH 6/7] remove `ProofGenerationError::FeeCalculation` --- zk-sdk/src/elgamal_program/errors.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/zk-sdk/src/elgamal_program/errors.rs b/zk-sdk/src/elgamal_program/errors.rs index 49ed004d8fd65f..2ff39be9478745 100644 --- a/zk-sdk/src/elgamal_program/errors.rs +++ b/zk-sdk/src/elgamal_program/errors.rs @@ -12,8 +12,6 @@ use { pub enum ProofGenerationError { #[error("not enough funds in account")] NotEnoughFunds, - #[error("transfer fee calculation error")] - FeeCalculation, #[error("illegal number of commitments")] IllegalCommitmentLength, #[error("illegal amount bit length")] From 32a637292f5e1211fe93f640cccc1d52472efd03 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Thu, 23 May 2024 08:40:50 +0900 Subject: [PATCH 7/7] remove `ProofGenerationError::NotEnoughFunds` --- zk-sdk/src/elgamal_program/errors.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/zk-sdk/src/elgamal_program/errors.rs b/zk-sdk/src/elgamal_program/errors.rs index 2ff39be9478745..10b83edbb6fa83 100644 --- a/zk-sdk/src/elgamal_program/errors.rs +++ b/zk-sdk/src/elgamal_program/errors.rs @@ -10,8 +10,6 @@ use { #[cfg(not(target_os = "solana"))] #[derive(Error, Clone, Debug, Eq, PartialEq)] pub enum ProofGenerationError { - #[error("not enough funds in account")] - NotEnoughFunds, #[error("illegal number of commitments")] IllegalCommitmentLength, #[error("illegal amount bit length")]