From f3e7e628139e557e6d4fe0e43b33ba3414f3a152 Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Thu, 30 Sep 2021 10:25:36 -0700 Subject: [PATCH] Refactor sdk/src/pod.rs --- zk-token-sdk/src/errors.rs | 2 +- zk-token-sdk/src/instruction/close_account.rs | 10 +- zk-token-sdk/src/instruction/mod.rs | 2 +- zk-token-sdk/src/instruction/transfer.rs | 46 +- .../src/instruction/update_account_pk.rs | 30 +- zk-token-sdk/src/instruction/withdraw.rs | 10 +- zk-token-sdk/src/lib.rs | 2 +- zk-token-sdk/src/pod.rs | 596 ------------------ zk-token-sdk/src/zk_token_elgamal/convert.rs | 292 +++++++++ zk-token-sdk/src/zk_token_elgamal/mod.rs | 3 + zk-token-sdk/src/zk_token_elgamal/ops.rs | 272 ++++++++ zk-token-sdk/src/zk_token_elgamal/pod.rs | 45 ++ 12 files changed, 664 insertions(+), 646 deletions(-) delete mode 100644 zk-token-sdk/src/pod.rs create mode 100644 zk-token-sdk/src/zk_token_elgamal/convert.rs create mode 100644 zk-token-sdk/src/zk_token_elgamal/mod.rs create mode 100644 zk-token-sdk/src/zk_token_elgamal/ops.rs create mode 100644 zk-token-sdk/src/zk_token_elgamal/pod.rs diff --git a/zk-token-sdk/src/errors.rs b/zk-token-sdk/src/errors.rs index ad7a251e8..d5215d4c5 100644 --- a/zk-token-sdk/src/errors.rs +++ b/zk-token-sdk/src/errors.rs @@ -14,6 +14,6 @@ pub enum ProofError { InvalidBitsize, /// This error occurs when there are insufficient generators for the proof. InvalidGeneratorsLength, - /// This error occurs when TODO + /// This error occurs a `zk_token_elgamal::pod::ElGamalCT` contains invalid ElGamalCT ciphertext InconsistentCTData, } diff --git a/zk-token-sdk/src/instruction/close_account.rs b/zk-token-sdk/src/instruction/close_account.rs index d4cdcd619..7993805e7 100644 --- a/zk-token-sdk/src/instruction/close_account.rs +++ b/zk-token-sdk/src/instruction/close_account.rs @@ -1,5 +1,5 @@ use { - crate::pod::*, + crate::zk_token_elgamal::pod, bytemuck::{Pod, Zeroable}, }; #[cfg(not(target_arch = "bpf"))] @@ -31,7 +31,7 @@ use { #[repr(C)] pub struct CloseAccountData { /// The source account available balance in encrypted form - pub balance: PodElGamalCT, // 64 bytes + pub balance: pod::ElGamalCT, // 64 bytes /// Proof that the source account available balance is zero pub proof: CloseAccountProof, // 64 bytes @@ -63,8 +63,8 @@ impl Verifiable for CloseAccountData { #[repr(C)] #[allow(non_snake_case)] pub struct CloseAccountProof { - pub R: PodCompressedRistretto, // 32 bytes - pub z: PodScalar, // 32 bytes + pub R: pod::CompressedRistretto, // 32 bytes + pub z: pod::Scalar, // 32 bytes } #[allow(non_snake_case)] @@ -156,7 +156,7 @@ mod test { assert!(proof.verify(&balance).is_err()); // A zeroed cyphertext should be considered as an account balance of 0 - let zeroed_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap(); + let zeroed_ct: ElGamalCT = pod::ElGamalCT::zeroed().try_into().unwrap(); let proof = CloseAccountProof::new(&source_sk, &zeroed_ct); assert!(proof.verify(&zeroed_ct).is_ok()); } diff --git a/zk-token-sdk/src/instruction/mod.rs b/zk-token-sdk/src/instruction/mod.rs index 7c0672cdf..4154d9f1b 100644 --- a/zk-token-sdk/src/instruction/mod.rs +++ b/zk-token-sdk/src/instruction/mod.rs @@ -1,5 +1,5 @@ mod close_account; -pub mod transfer; +mod transfer; mod update_account_pk; mod withdraw; diff --git a/zk-token-sdk/src/instruction/transfer.rs b/zk-token-sdk/src/instruction/transfer.rs index 8f34878fd..ff9f14ce4 100644 --- a/zk-token-sdk/src/instruction/transfer.rs +++ b/zk-token-sdk/src/instruction/transfer.rs @@ -1,5 +1,5 @@ use { - crate::pod::*, + crate::zk_token_elgamal::pod, bytemuck::{Pod, Zeroable}, }; #[cfg(not(target_arch = "bpf"))] @@ -145,7 +145,7 @@ pub struct TransferRangeProofData { /// 1. the source account has enough funds for the transfer (i.e. the final balance is a /// 64-bit positive number) /// 2. the transfer amount is a 64-bit positive number - pub proof: PodRangeProof128, // 736 bytes + pub proof: pod::RangeProof128, // 736 bytes /// Ephemeral state between the two transfer instruction data pub ephemeral_state: TransferEphemeralState, // 128 bytes @@ -185,7 +185,7 @@ pub struct TransferValidityProofData { pub transfer_public_keys: TransferPubKeys, // 96 bytes /// The final spendable ciphertext after the transfer - pub new_spendable_ct: PodElGamalCT, // 64 bytes + pub new_spendable_ct: pod::ElGamalCT, // 64 bytes /// Proof that certifies that the decryption handles are generated correctly pub proof: ValidityProof, // 160 bytes @@ -201,10 +201,10 @@ pub struct TransferValidityProofData { #[derive(Clone, Copy, Pod, Zeroable, PartialEq)] #[repr(C)] pub struct TransferEphemeralState { - pub spendable_comm_verification: PodPedersenComm, // 32 bytes - pub x: PodScalar, // 32 bytes - pub z: PodScalar, // 32 bytes - pub t_x_blinding: PodScalar, // 32 bytes + pub spendable_comm_verification: pod::PedersenComm, // 32 bytes + pub x: pod::Scalar, // 32 bytes + pub z: pod::Scalar, // 32 bytes + pub t_x_blinding: pod::Scalar, // 32 bytes } #[cfg(not(target_arch = "bpf"))] @@ -222,8 +222,9 @@ impl Verifiable for TransferValidityProofData { /// Just a grouping struct for the two proofs that are needed for a transfer instruction. The two /// proofs have to be generated together as they share joint data. +#[cfg(not(target_arch = "bpf"))] pub struct TransferProofs { - pub range_proof: PodRangeProof128, + pub range_proof: pod::RangeProof128, pub validity_proof: ValidityProof, } @@ -330,15 +331,15 @@ impl TransferProofs { #[repr(C)] pub struct ValidityProof { // Proof component for the spendable ciphertext components: R - pub R: PodCompressedRistretto, // 32 bytes + pub R: pod::CompressedRistretto, // 32 bytes // Proof component for the spendable ciphertext components: z - pub z: PodScalar, // 32 bytes + pub z: pod::Scalar, // 32 bytes // Proof component for the transaction amount components: T_src - pub T_joint: PodCompressedRistretto, // 32 bytes + pub T_joint: pod::CompressedRistretto, // 32 bytes // Proof component for the transaction amount components: T_1 - pub T_1: PodCompressedRistretto, // 32 bytes + pub T_1: pod::CompressedRistretto, // 32 bytes // Proof component for the transaction amount components: T_2 - pub T_2: PodCompressedRistretto, // 32 bytes + pub T_2: pod::CompressedRistretto, // 32 bytes } #[allow(non_snake_case)] @@ -454,26 +455,26 @@ impl ValidityProof { #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct TransferPubKeys { - pub source_pk: PodElGamalPK, // 32 bytes - pub dest_pk: PodElGamalPK, // 32 bytes - pub auditor_pk: PodElGamalPK, // 32 bytes + pub source_pk: pod::ElGamalPK, // 32 bytes + pub dest_pk: pod::ElGamalPK, // 32 bytes + pub auditor_pk: pod::ElGamalPK, // 32 bytes } /// The transfer amount commitments needed for a transfer #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct TransferComms { - pub lo: PodPedersenComm, // 32 bytes - pub hi: PodPedersenComm, // 32 bytes + pub lo: pod::PedersenComm, // 32 bytes + pub hi: pod::PedersenComm, // 32 bytes } /// The decryption handles needed for a transfer #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct TransferHandles { - pub source: PodPedersenDecHandle, // 32 bytes - pub dest: PodPedersenDecHandle, // 32 bytes - pub auditor: PodPedersenDecHandle, // 32 bytes + pub source: pod::PedersenDecHandle, // 32 bytes + pub dest: pod::PedersenDecHandle, // 32 bytes + pub auditor: pod::PedersenDecHandle, // 32 bytes } /// Split u64 number into two u32 numbers @@ -502,10 +503,11 @@ pub fn combine_u32_handles( handle_lo + handle_hi * Scalar::from(TWO_32) } -#[cfg(not(target_arch = "bpf"))] +/* pub fn combine_u32_ciphertexts(ct_lo: ElGamalCT, ct_hi: ElGamalCT) -> ElGamalCT { ct_lo + ct_hi * Scalar::from(TWO_32) } +*/ #[cfg(test)] mod test { diff --git a/zk-token-sdk/src/instruction/update_account_pk.rs b/zk-token-sdk/src/instruction/update_account_pk.rs index 0a1d1c571..90ade71ba 100644 --- a/zk-token-sdk/src/instruction/update_account_pk.rs +++ b/zk-token-sdk/src/instruction/update_account_pk.rs @@ -1,5 +1,5 @@ use { - crate::pod::*, + crate::zk_token_elgamal::pod, bytemuck::{Pod, Zeroable}, }; #[cfg(not(target_arch = "bpf"))] @@ -34,16 +34,16 @@ use { #[repr(C)] pub struct UpdateAccountPkData { /// Current ElGamal encryption key - pub current_pk: PodElGamalPK, // 32 bytes + pub current_pk: pod::ElGamalPK, // 32 bytes /// Current encrypted available balance - pub current_ct: PodElGamalCT, // 64 bytes + pub current_ct: pod::ElGamalCT, // 64 bytes /// New ElGamal encryption key - pub new_pk: PodElGamalPK, // 32 bytes + pub new_pk: pod::ElGamalPK, // 32 bytes /// New encrypted available balance - pub new_ct: PodElGamalCT, // 64 bytes + pub new_ct: pod::ElGamalCT, // 64 bytes /// Proof that the current and new ciphertexts are consistent pub proof: UpdateAccountPkProof, // 160 bytes @@ -89,11 +89,11 @@ impl Verifiable for UpdateAccountPkData { #[repr(C)] #[allow(non_snake_case)] pub struct UpdateAccountPkProof { - pub R_0: PodCompressedRistretto, // 32 bytes - pub R_1: PodCompressedRistretto, // 32 bytes - pub z_sk_0: PodScalar, // 32 bytes - pub z_sk_1: PodScalar, // 32 bytes - pub z_x: PodScalar, // 32 bytes + pub R_0: pod::CompressedRistretto, // 32 bytes + pub R_1: pod::CompressedRistretto, // 32 bytes + pub z_sk_0: pod::Scalar, // 32 bytes + pub z_sk_1: pod::Scalar, // 32 bytes + pub z_x: pod::Scalar, // 32 bytes } #[allow(non_snake_case)] @@ -233,7 +233,7 @@ mod test { // A zeroed cipehrtext should be considered as an account balance of 0 let balance: u64 = 0; - let zeroed_ct_as_current_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap(); + let zeroed_ct_as_current_ct: ElGamalCT = pod::ElGamalCT::zeroed().try_into().unwrap(); let new_ct: ElGamalCT = new_pk.encrypt(balance); let proof = UpdateAccountPkProof::new( balance, @@ -244,8 +244,8 @@ mod test { ); assert!(proof.verify(&zeroed_ct_as_current_ct, &new_ct).is_ok()); - let current_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap(); - let zeroed_ct_as_new_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap(); + let current_ct: ElGamalCT = pod::ElGamalCT::zeroed().try_into().unwrap(); + let zeroed_ct_as_new_ct: ElGamalCT = pod::ElGamalCT::zeroed().try_into().unwrap(); let proof = UpdateAccountPkProof::new( balance, ¤t_sk, @@ -255,8 +255,8 @@ mod test { ); assert!(proof.verify(¤t_ct, &zeroed_ct_as_new_ct).is_ok()); - let zeroed_ct_as_current_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap(); - let zeroed_ct_as_new_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap(); + let zeroed_ct_as_current_ct: ElGamalCT = pod::ElGamalCT::zeroed().try_into().unwrap(); + let zeroed_ct_as_new_ct: ElGamalCT = pod::ElGamalCT::zeroed().try_into().unwrap(); let proof = UpdateAccountPkProof::new( balance, ¤t_sk, diff --git a/zk-token-sdk/src/instruction/withdraw.rs b/zk-token-sdk/src/instruction/withdraw.rs index 26fcd451a..76ebb39b5 100644 --- a/zk-token-sdk/src/instruction/withdraw.rs +++ b/zk-token-sdk/src/instruction/withdraw.rs @@ -1,5 +1,5 @@ use { - crate::pod::*, + crate::zk_token_elgamal::pod, bytemuck::{Pod, Zeroable}, }; #[cfg(not(target_arch = "bpf"))] @@ -32,7 +32,7 @@ use { pub struct WithdrawData { /// The source account available balance *after* the withdraw (encrypted by /// `source_pk` - pub final_balance_ct: PodElGamalCT, // 64 bytes + pub final_balance_ct: pod::ElGamalCT, // 64 bytes /// Proof that the account is solvent pub proof: WithdrawProof, // 736 bytes @@ -81,11 +81,11 @@ impl Verifiable for WithdrawData { #[allow(non_snake_case)] pub struct WithdrawProof { /// Wrapper for range proof: R component - pub R: PodCompressedRistretto, // 32 bytes + pub R: pod::CompressedRistretto, // 32 bytes /// Wrapper for range proof: z component - pub z: PodScalar, // 32 bytes + pub z: pod::Scalar, // 32 bytes /// Associated range proof - pub range_proof: PodRangeProof64, // 672 bytes + pub range_proof: pod::RangeProof64, // 672 bytes } #[allow(non_snake_case)] diff --git a/zk-token-sdk/src/lib.rs b/zk-token-sdk/src/lib.rs index 0942ceaae..d1dc4d3d0 100644 --- a/zk-token-sdk/src/lib.rs +++ b/zk-token-sdk/src/lib.rs @@ -14,6 +14,6 @@ mod range_proof; mod transcript; mod instruction; -pub mod pod; +pub mod zk_token_elgamal; pub mod zk_token_proof_instruction; pub mod zk_token_proof_program; diff --git a/zk-token-sdk/src/pod.rs b/zk-token-sdk/src/pod.rs deleted file mode 100644 index d614be783..000000000 --- a/zk-token-sdk/src/pod.rs +++ /dev/null @@ -1,596 +0,0 @@ -//! Plain Old Data wrappers for types that need to be sent over the wire - -use bytemuck::{Pod, Zeroable}; -#[cfg(not(target_arch = "bpf"))] -use { - crate::{ - encryption::elgamal::{ElGamalCT, ElGamalPK}, - encryption::pedersen::{PedersenComm, PedersenDecHandle}, - errors::ProofError, - range_proof::RangeProof, - }, - curve25519_dalek::{ - constants::RISTRETTO_BASEPOINT_COMPRESSED, ristretto::CompressedRistretto, scalar::Scalar, - }, - std::{ - convert::{TryFrom, TryInto}, - fmt, - }, -}; - -#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] -#[repr(transparent)] -pub struct PodScalar([u8; 32]); - -#[cfg(not(target_arch = "bpf"))] -impl From for PodScalar { - fn from(scalar: Scalar) -> Self { - Self(scalar.to_bytes()) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl From for Scalar { - fn from(pod: PodScalar) -> Self { - Scalar::from_bits(pod.0) - } -} - -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(transparent)] -pub struct PodCompressedRistretto([u8; 32]); - -#[cfg(not(target_arch = "bpf"))] -impl From for PodCompressedRistretto { - fn from(cr: CompressedRistretto) -> Self { - Self(cr.to_bytes()) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl From for CompressedRistretto { - fn from(pod: PodCompressedRistretto) -> Self { - Self(pod.0) - } -} - -#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] -#[repr(transparent)] -pub struct PodElGamalCT([u8; 64]); - -#[cfg(not(target_arch = "bpf"))] -impl From for PodElGamalCT { - fn from(ct: ElGamalCT) -> Self { - Self(ct.to_bytes()) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl TryFrom for ElGamalCT { - type Error = ProofError; - - fn try_from(pod: PodElGamalCT) -> Result { - Self::from_bytes(&pod.0).ok_or(ProofError::InconsistentCTData) - } -} - -impl From<(PodPedersenComm, PodPedersenDecHandle)> for PodElGamalCT { - fn from((pod_comm, pod_decrypt_handle): (PodPedersenComm, PodPedersenDecHandle)) -> Self { - let mut buf = [0_u8; 64]; - buf[..32].copy_from_slice(&pod_comm.0); - buf[32..].copy_from_slice(&pod_decrypt_handle.0); - PodElGamalCT(buf) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl fmt::Debug for PodElGamalCT { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} - -#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] -#[repr(transparent)] -pub struct PodElGamalPK([u8; 32]); - -#[cfg(not(target_arch = "bpf"))] -impl From for PodElGamalPK { - fn from(pk: ElGamalPK) -> Self { - Self(pk.to_bytes()) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl TryFrom for ElGamalPK { - type Error = ProofError; - - fn try_from(pod: PodElGamalPK) -> Result { - Self::from_bytes(&pod.0).ok_or(ProofError::InconsistentCTData) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl fmt::Debug for PodElGamalPK { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} - -#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] -#[repr(transparent)] -pub struct PodPedersenComm([u8; 32]); - -#[cfg(not(target_arch = "bpf"))] -impl From for PodPedersenComm { - fn from(comm: PedersenComm) -> Self { - Self(comm.to_bytes()) - } -} - -// For proof verification, interpret PodPedersenComm directly as CompressedRistretto -#[cfg(not(target_arch = "bpf"))] -impl From for CompressedRistretto { - fn from(pod: PodPedersenComm) -> Self { - Self(pod.0) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl TryFrom for PedersenComm { - type Error = ProofError; - - fn try_from(pod: PodPedersenComm) -> Result { - Self::from_bytes(&pod.0).ok_or(ProofError::InconsistentCTData) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl fmt::Debug for PodPedersenComm { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} - -#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] -#[repr(transparent)] -pub struct PodPedersenDecHandle([u8; 32]); - -#[cfg(not(target_arch = "bpf"))] -impl From for PodPedersenDecHandle { - fn from(handle: PedersenDecHandle) -> Self { - Self(handle.to_bytes()) - } -} - -// For proof verification, interpret PodPedersenDecHandle as CompressedRistretto -#[cfg(not(target_arch = "bpf"))] -impl From for CompressedRistretto { - fn from(pod: PodPedersenDecHandle) -> Self { - Self(pod.0) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl TryFrom for PedersenDecHandle { - type Error = ProofError; - - fn try_from(pod: PodPedersenDecHandle) -> Result { - Self::from_bytes(&pod.0).ok_or(ProofError::InconsistentCTData) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl fmt::Debug for PodPedersenDecHandle { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} - -/// Serialization of range proofs for 64-bit numbers (for `Withdraw` instruction) -#[derive(Clone, Copy)] -#[repr(transparent)] -pub struct PodRangeProof64([u8; 672]); - -// `PodRangeProof64` is a Pod and Zeroable. -// Add the marker traits manually because `bytemuck` only adds them for some `u8` arrays -unsafe impl Zeroable for PodRangeProof64 {} -unsafe impl Pod for PodRangeProof64 {} - -#[cfg(not(target_arch = "bpf"))] -impl TryFrom for PodRangeProof64 { - type Error = ProofError; - - fn try_from(proof: RangeProof) -> Result { - if proof.ipp_proof.serialized_size() != 448 { - return Err(ProofError::VerificationError); - } - - let mut buf = [0_u8; 672]; - buf[..32].copy_from_slice(proof.A.as_bytes()); - buf[32..64].copy_from_slice(proof.S.as_bytes()); - buf[64..96].copy_from_slice(proof.T_1.as_bytes()); - buf[96..128].copy_from_slice(proof.T_2.as_bytes()); - buf[128..160].copy_from_slice(proof.t_x.as_bytes()); - buf[160..192].copy_from_slice(proof.t_x_blinding.as_bytes()); - buf[192..224].copy_from_slice(proof.e_blinding.as_bytes()); - buf[224..672].copy_from_slice(&proof.ipp_proof.to_bytes()); - Ok(PodRangeProof64(buf)) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl TryFrom for RangeProof { - type Error = ProofError; - - fn try_from(pod: PodRangeProof64) -> Result { - Self::from_bytes(&pod.0) - } -} - -/// Serialization of range proofs for 128-bit numbers (for `TransferRangeProof` instruction) -#[derive(Clone, Copy)] -#[repr(transparent)] -pub struct PodRangeProof128([u8; 736]); - -// `PodRangeProof128` is a Pod and Zeroable. -// Add the marker traits manually because `bytemuck` only adds them for some `u8` arrays -unsafe impl Zeroable for PodRangeProof128 {} -unsafe impl Pod for PodRangeProof128 {} - -#[cfg(not(target_arch = "bpf"))] -impl TryFrom for PodRangeProof128 { - type Error = ProofError; - - fn try_from(proof: RangeProof) -> Result { - if proof.ipp_proof.serialized_size() != 512 { - return Err(ProofError::VerificationError); - } - - let mut buf = [0_u8; 736]; - buf[..32].copy_from_slice(proof.A.as_bytes()); - buf[32..64].copy_from_slice(proof.S.as_bytes()); - buf[64..96].copy_from_slice(proof.T_1.as_bytes()); - buf[96..128].copy_from_slice(proof.T_2.as_bytes()); - buf[128..160].copy_from_slice(proof.t_x.as_bytes()); - buf[160..192].copy_from_slice(proof.t_x_blinding.as_bytes()); - buf[192..224].copy_from_slice(proof.e_blinding.as_bytes()); - buf[224..736].copy_from_slice(&proof.ipp_proof.to_bytes()); - Ok(PodRangeProof128(buf)) - } -} - -#[cfg(not(target_arch = "bpf"))] -impl TryFrom for RangeProof { - type Error = ProofError; - - fn try_from(pod: PodRangeProof128) -> Result { - Self::from_bytes(&pod.0) - } -} - -pub struct PodElGamalArithmetic; - -#[cfg(not(target_arch = "bpf"))] -impl PodElGamalArithmetic { - const TWO_32: u64 = 4294967296; - - // On input two scalars x0, x1 and two ciphertexts ct0, ct1, - // returns `Some(x0*ct0 + x1*ct1)` or `None` if the input was invalid - fn add_pod_ciphertexts( - scalar_0: Scalar, - pod_ct_0: PodElGamalCT, - scalar_1: Scalar, - pod_ct_1: PodElGamalCT, - ) -> Option { - let ct_0: ElGamalCT = pod_ct_0.try_into().ok()?; - let ct_1: ElGamalCT = pod_ct_1.try_into().ok()?; - - let ct_sum = ct_0 * scalar_0 + ct_1 * scalar_1; - Some(PodElGamalCT::from(ct_sum)) - } - - fn combine_lo_hi(pod_ct_lo: PodElGamalCT, pod_ct_hi: PodElGamalCT) -> Option { - Self::add_pod_ciphertexts( - Scalar::one(), - pod_ct_lo, - Scalar::from(Self::TWO_32), - pod_ct_hi, - ) - } - - pub fn add(pod_ct_0: PodElGamalCT, pod_ct_1: PodElGamalCT) -> Option { - Self::add_pod_ciphertexts(Scalar::one(), pod_ct_0, Scalar::one(), pod_ct_1) - } - - pub fn add_with_lo_hi( - pod_ct_0: PodElGamalCT, - pod_ct_1_lo: PodElGamalCT, - pod_ct_1_hi: PodElGamalCT, - ) -> Option { - let pod_ct_1 = Self::combine_lo_hi(pod_ct_1_lo, pod_ct_1_hi)?; - Self::add_pod_ciphertexts(Scalar::one(), pod_ct_0, Scalar::one(), pod_ct_1) - } - - pub fn subtract(pod_ct_0: PodElGamalCT, pod_ct_1: PodElGamalCT) -> Option { - Self::add_pod_ciphertexts(Scalar::one(), pod_ct_0, -Scalar::one(), pod_ct_1) - } - - pub fn subtract_with_lo_hi( - pod_ct_0: PodElGamalCT, - pod_ct_1_lo: PodElGamalCT, - pod_ct_1_hi: PodElGamalCT, - ) -> Option { - let pod_ct_1 = Self::combine_lo_hi(pod_ct_1_lo, pod_ct_1_hi)?; - Self::add_pod_ciphertexts(Scalar::one(), pod_ct_0, -Scalar::one(), pod_ct_1) - } - - pub fn add_to(pod_ct: PodElGamalCT, amount: u64) -> Option { - let mut amount_as_pod_ct = [0_u8; 64]; - amount_as_pod_ct[..32].copy_from_slice(RISTRETTO_BASEPOINT_COMPRESSED.as_bytes()); - Self::add_pod_ciphertexts( - Scalar::one(), - pod_ct, - Scalar::from(amount), - PodElGamalCT(amount_as_pod_ct), - ) - } - - pub fn subtract_from(pod_ct: PodElGamalCT, amount: u64) -> Option { - let mut amount_as_pod_ct = [0_u8; 64]; - amount_as_pod_ct[..32].copy_from_slice(RISTRETTO_BASEPOINT_COMPRESSED.as_bytes()); - Self::add_pod_ciphertexts( - Scalar::one(), - pod_ct, - -Scalar::from(amount), - PodElGamalCT(amount_as_pod_ct), - ) - } -} -#[cfg(target_arch = "bpf")] -#[allow(unused_variables)] -impl PodElGamalArithmetic { - pub fn add(pod_ct_0: PodElGamalCT, pod_ct_1: PodElGamalCT) -> Option { - None - } - - pub fn add_with_lo_hi( - pod_ct_0: PodElGamalCT, - pod_ct_1_lo: PodElGamalCT, - pod_ct_1_hi: PodElGamalCT, - ) -> Option { - None - } - - pub fn subtract(pod_ct_0: PodElGamalCT, pod_ct_1: PodElGamalCT) -> Option { - None - } - - pub fn subtract_with_lo_hi( - pod_ct_0: PodElGamalCT, - pod_ct_1_lo: PodElGamalCT, - pod_ct_1_hi: PodElGamalCT, - ) -> Option { - None - } - - pub fn add_to(pod_ct: PodElGamalCT, amount: u64) -> Option { - None - } - - pub fn subtract_from(pod_ct: PodElGamalCT, amount: u64) -> Option { - None - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::encryption::{ - elgamal::{ElGamal, ElGamalCT}, - pedersen::{Pedersen, PedersenOpen}, - }, - crate::instruction::transfer::split_u64_into_u32, - merlin::Transcript, - rand::rngs::OsRng, - std::convert::TryInto, - }; - - #[test] - fn test_zero_ct() { - let spendable_balance = PodElGamalCT::zeroed(); - let spendable_ct: ElGamalCT = spendable_balance.try_into().unwrap(); - - // spendable_ct should be an encryption of 0 for any public key when - // `PedersenOpen::default()` is used - let (pk, _) = ElGamal::keygen(); - let balance: u64 = 0; - assert_eq!( - spendable_ct, - pk.encrypt_with(balance, &PedersenOpen::default()) - ); - - // homomorphism should work like any other ciphertext - let open = PedersenOpen::random(&mut OsRng); - let transfer_amount_ct = pk.encrypt_with(55_u64, &open); - let transfer_amount_pod: PodElGamalCT = transfer_amount_ct.into(); - - let sum = PodElGamalArithmetic::add(spendable_balance, transfer_amount_pod).unwrap(); - - let expected: PodElGamalCT = pk.encrypt_with(55_u64, &open).into(); - assert_eq!(expected, sum); - } - - #[test] - fn test_add_to() { - let spendable_balance = PodElGamalCT::zeroed(); - - let added_ct = PodElGamalArithmetic::add_to(spendable_balance, 55).unwrap(); - - let (pk, _) = ElGamal::keygen(); - let expected: PodElGamalCT = pk.encrypt_with(55_u64, &PedersenOpen::default()).into(); - - assert_eq!(expected, added_ct); - } - - #[test] - fn test_subtract_from() { - let amount = 77_u64; - let (pk, _) = ElGamal::keygen(); - let open = PedersenOpen::random(&mut OsRng); - let encrypted_amount: PodElGamalCT = pk.encrypt_with(amount, &open).into(); - - let subtracted_ct = PodElGamalArithmetic::subtract_from(encrypted_amount, 55).unwrap(); - - let expected: PodElGamalCT = pk.encrypt_with(22_u64, &open).into(); - - assert_eq!(expected, subtracted_ct); - } - - #[test] - fn test_pod_range_proof_64() { - let (comm, open) = Pedersen::commit(55_u64); - - let mut transcript_create = Transcript::new(b"Test"); - let mut transcript_verify = Transcript::new(b"Test"); - - let proof = RangeProof::create(vec![55], vec![64], vec![&open], &mut transcript_create); - - let proof_serialized: PodRangeProof64 = proof.try_into().unwrap(); - let proof_deserialized: RangeProof = proof_serialized.try_into().unwrap(); - - assert!(proof_deserialized - .verify( - vec![&comm.get_point().compress()], - vec![64], - &mut transcript_verify - ) - .is_ok()); - - // should fail to serialize to PodRangeProof128 - let proof = RangeProof::create(vec![55], vec![64], vec![&open], &mut transcript_create); - - assert!(TryInto::::try_into(proof).is_err()); - } - - #[test] - fn test_pod_range_proof_128() { - let (comm_1, open_1) = Pedersen::commit(55_u64); - let (comm_2, open_2) = Pedersen::commit(77_u64); - let (comm_3, open_3) = Pedersen::commit(99_u64); - - let mut transcript_create = Transcript::new(b"Test"); - let mut transcript_verify = Transcript::new(b"Test"); - - let proof = RangeProof::create( - vec![55, 77, 99], - vec![64, 32, 32], - vec![&open_1, &open_2, &open_3], - &mut transcript_create, - ); - - let comm_1_point = comm_1.get_point().compress(); - let comm_2_point = comm_2.get_point().compress(); - let comm_3_point = comm_3.get_point().compress(); - - let proof_serialized: PodRangeProof128 = proof.try_into().unwrap(); - let proof_deserialized: RangeProof = proof_serialized.try_into().unwrap(); - - assert!(proof_deserialized - .verify( - vec![&comm_1_point, &comm_2_point, &comm_3_point], - vec![64, 32, 32], - &mut transcript_verify, - ) - .is_ok()); - - // should fail to serialize to PodRangeProof64 - let proof = RangeProof::create( - vec![55, 77, 99], - vec![64, 32, 32], - vec![&open_1, &open_2, &open_3], - &mut transcript_create, - ); - - assert!(TryInto::::try_into(proof).is_err()); - } - - #[test] - fn test_transfer_arithmetic() { - // setup - - // transfer amount - let transfer_amount: u64 = 55; - let (amount_lo, amount_hi) = split_u64_into_u32(transfer_amount); - - // generate public keys - let (source_pk, _) = ElGamal::keygen(); - let (dest_pk, _) = ElGamal::keygen(); - let (auditor_pk, _) = ElGamal::keygen(); - - // commitments associated with TransferRangeProof - let (comm_lo, open_lo) = Pedersen::commit(amount_lo); - let (comm_hi, open_hi) = Pedersen::commit(amount_hi); - - let comm_lo: PodPedersenComm = comm_lo.into(); - let comm_hi: PodPedersenComm = comm_hi.into(); - - // decryption handles associated with TransferValidityProof - let handle_source_lo: PodPedersenDecHandle = source_pk.gen_decrypt_handle(&open_lo).into(); - let handle_dest_lo: PodPedersenDecHandle = dest_pk.gen_decrypt_handle(&open_lo).into(); - let _handle_auditor_lo: PodPedersenDecHandle = - auditor_pk.gen_decrypt_handle(&open_lo).into(); - - let handle_source_hi: PodPedersenDecHandle = source_pk.gen_decrypt_handle(&open_hi).into(); - let handle_dest_hi: PodPedersenDecHandle = dest_pk.gen_decrypt_handle(&open_hi).into(); - let _handle_auditor_hi: PodPedersenDecHandle = - auditor_pk.gen_decrypt_handle(&open_hi).into(); - - // source spendable and recipient pending - let source_open = PedersenOpen::random(&mut OsRng); - let dest_open = PedersenOpen::random(&mut OsRng); - - let source_spendable_ct: PodElGamalCT = source_pk.encrypt_with(77_u64, &source_open).into(); - let dest_pending_ct: PodElGamalCT = dest_pk.encrypt_with(77_u64, &dest_open).into(); - - // program arithmetic for the source account - - // 1. Combine commitments and handles - let source_lo_ct: PodElGamalCT = (comm_lo, handle_source_lo).into(); - let source_hi_ct: PodElGamalCT = (comm_hi, handle_source_hi).into(); - - // 2. Combine lo and hi ciphertexts - let source_combined_ct = - PodElGamalArithmetic::combine_lo_hi(source_lo_ct, source_hi_ct).unwrap(); - - // 3. Subtract from available balance - let final_source_spendable = - PodElGamalArithmetic::subtract(source_spendable_ct, source_combined_ct).unwrap(); - - // test - let final_source_open = source_open - - (open_lo.clone() + open_hi.clone() * Scalar::from(PodElGamalArithmetic::TWO_32)); - let expected_source: PodElGamalCT = - source_pk.encrypt_with(22_u64, &final_source_open).into(); - assert_eq!(expected_source, final_source_spendable); - - // same for the destination account - - // 1. Combine commitments and handles - let dest_lo_ct: PodElGamalCT = (comm_lo, handle_dest_lo).into(); - let dest_hi_ct: PodElGamalCT = (comm_hi, handle_dest_hi).into(); - - // 2. Combine lo and hi ciphertexts - let dest_combined_ct = PodElGamalArithmetic::combine_lo_hi(dest_lo_ct, dest_hi_ct).unwrap(); - - // 3. Add to pending balance - let final_dest_pending = - PodElGamalArithmetic::add(dest_pending_ct, dest_combined_ct).unwrap(); - - let final_dest_open = - dest_open + (open_lo + open_hi * Scalar::from(PodElGamalArithmetic::TWO_32)); - let expected_dest_ct: PodElGamalCT = dest_pk.encrypt_with(132_u64, &final_dest_open).into(); - assert_eq!(expected_dest_ct, final_dest_pending); - } -} diff --git a/zk-token-sdk/src/zk_token_elgamal/convert.rs b/zk-token-sdk/src/zk_token_elgamal/convert.rs new file mode 100644 index 000000000..39315ee2a --- /dev/null +++ b/zk-token-sdk/src/zk_token_elgamal/convert.rs @@ -0,0 +1,292 @@ +use super::pod; +pub use target_arch::*; + +impl From<(pod::PedersenComm, pod::PedersenDecHandle)> for pod::ElGamalCT { + fn from((comm, decrypt_handle): (pod::PedersenComm, pod::PedersenDecHandle)) -> Self { + let mut buf = [0_u8; 64]; + buf[..32].copy_from_slice(&comm.0); + buf[32..].copy_from_slice(&decrypt_handle.0); + pod::ElGamalCT(buf) + } +} + +#[cfg(not(target_arch = "bpf"))] +mod target_arch { + use { + super::pod, + crate::{ + encryption::elgamal::{ElGamalCT, ElGamalPK}, + encryption::pedersen::{PedersenComm, PedersenDecHandle}, + errors::ProofError, + range_proof::RangeProof, + }, + curve25519_dalek::{ristretto::CompressedRistretto, scalar::Scalar}, + std::{convert::TryFrom, fmt}, + }; + + impl From for pod::Scalar { + fn from(scalar: Scalar) -> Self { + Self(scalar.to_bytes()) + } + } + + impl From for Scalar { + fn from(pod: pod::Scalar) -> Self { + Scalar::from_bits(pod.0) + } + } + + impl From for pod::ElGamalCT { + fn from(ct: ElGamalCT) -> Self { + Self(ct.to_bytes()) + } + } + + impl TryFrom for ElGamalCT { + type Error = ProofError; + + fn try_from(ct: pod::ElGamalCT) -> Result { + Self::from_bytes(&ct.0).ok_or(ProofError::InconsistentCTData) + } + } + + impl fmt::Debug for pod::ElGamalCT { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.0) + } + } + + impl From for pod::ElGamalPK { + fn from(pk: ElGamalPK) -> Self { + Self(pk.to_bytes()) + } + } + + impl TryFrom for ElGamalPK { + type Error = ProofError; + + fn try_from(pk: pod::ElGamalPK) -> Result { + Self::from_bytes(&pk.0).ok_or(ProofError::InconsistentCTData) + } + } + + impl fmt::Debug for pod::ElGamalPK { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.0) + } + } + + impl From for pod::CompressedRistretto { + fn from(cr: CompressedRistretto) -> Self { + Self(cr.to_bytes()) + } + } + + impl From for CompressedRistretto { + fn from(pod: pod::CompressedRistretto) -> Self { + Self(pod.0) + } + } + + impl From for pod::PedersenComm { + fn from(comm: PedersenComm) -> Self { + Self(comm.to_bytes()) + } + } + + // For proof verification, interpret pod::PedersenComm directly as CompressedRistretto + #[cfg(not(target_arch = "bpf"))] + impl From for CompressedRistretto { + fn from(pod: pod::PedersenComm) -> Self { + Self(pod.0) + } + } + + #[cfg(not(target_arch = "bpf"))] + impl TryFrom for PedersenComm { + type Error = ProofError; + + fn try_from(pod: pod::PedersenComm) -> Result { + Self::from_bytes(&pod.0).ok_or(ProofError::InconsistentCTData) + } + } + + #[cfg(not(target_arch = "bpf"))] + impl fmt::Debug for pod::PedersenComm { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.0) + } + } + + #[cfg(not(target_arch = "bpf"))] + impl From for pod::PedersenDecHandle { + fn from(handle: PedersenDecHandle) -> Self { + Self(handle.to_bytes()) + } + } + + // For proof verification, interpret pod::PedersenDecHandle as CompressedRistretto + #[cfg(not(target_arch = "bpf"))] + impl From for CompressedRistretto { + fn from(pod: pod::PedersenDecHandle) -> Self { + Self(pod.0) + } + } + + #[cfg(not(target_arch = "bpf"))] + impl TryFrom for PedersenDecHandle { + type Error = ProofError; + + fn try_from(pod: pod::PedersenDecHandle) -> Result { + Self::from_bytes(&pod.0).ok_or(ProofError::InconsistentCTData) + } + } + + #[cfg(not(target_arch = "bpf"))] + impl fmt::Debug for pod::PedersenDecHandle { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.0) + } + } + + impl TryFrom for pod::RangeProof64 { + type Error = ProofError; + + fn try_from(proof: RangeProof) -> Result { + if proof.ipp_proof.serialized_size() != 448 { + return Err(ProofError::VerificationError); + } + + let mut buf = [0_u8; 672]; + buf[..32].copy_from_slice(proof.A.as_bytes()); + buf[32..64].copy_from_slice(proof.S.as_bytes()); + buf[64..96].copy_from_slice(proof.T_1.as_bytes()); + buf[96..128].copy_from_slice(proof.T_2.as_bytes()); + buf[128..160].copy_from_slice(proof.t_x.as_bytes()); + buf[160..192].copy_from_slice(proof.t_x_blinding.as_bytes()); + buf[192..224].copy_from_slice(proof.e_blinding.as_bytes()); + buf[224..672].copy_from_slice(&proof.ipp_proof.to_bytes()); + Ok(pod::RangeProof64(buf)) + } + } + + impl TryFrom for RangeProof { + type Error = ProofError; + + fn try_from(pod: pod::RangeProof64) -> Result { + Self::from_bytes(&pod.0) + } + } + + #[cfg(not(target_arch = "bpf"))] + impl TryFrom for pod::RangeProof128 { + type Error = ProofError; + + fn try_from(proof: RangeProof) -> Result { + if proof.ipp_proof.serialized_size() != 512 { + return Err(ProofError::VerificationError); + } + + let mut buf = [0_u8; 736]; + buf[..32].copy_from_slice(proof.A.as_bytes()); + buf[32..64].copy_from_slice(proof.S.as_bytes()); + buf[64..96].copy_from_slice(proof.T_1.as_bytes()); + buf[96..128].copy_from_slice(proof.T_2.as_bytes()); + buf[128..160].copy_from_slice(proof.t_x.as_bytes()); + buf[160..192].copy_from_slice(proof.t_x_blinding.as_bytes()); + buf[192..224].copy_from_slice(proof.e_blinding.as_bytes()); + buf[224..736].copy_from_slice(&proof.ipp_proof.to_bytes()); + Ok(pod::RangeProof128(buf)) + } + } + + impl TryFrom for RangeProof { + type Error = ProofError; + + fn try_from(pod: pod::RangeProof128) -> Result { + Self::from_bytes(&pod.0) + } + } +} + +#[cfg(target_arch = "bpf")] +#[allow(unused_variables)] +mod target_arch {} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{encryption::pedersen::Pedersen, range_proof::RangeProof}, + merlin::Transcript, + std::convert::TryInto, + }; + + #[test] + fn test_pod_range_proof_64() { + let (comm, open) = Pedersen::commit(55_u64); + + let mut transcript_create = Transcript::new(b"Test"); + let mut transcript_verify = Transcript::new(b"Test"); + + let proof = RangeProof::create(vec![55], vec![64], vec![&open], &mut transcript_create); + + let proof_serialized: pod::RangeProof64 = proof.try_into().unwrap(); + let proof_deserialized: RangeProof = proof_serialized.try_into().unwrap(); + + assert!(proof_deserialized + .verify( + vec![&comm.get_point().compress()], + vec![64], + &mut transcript_verify + ) + .is_ok()); + + // should fail to serialize to pod::RangeProof128 + let proof = RangeProof::create(vec![55], vec![64], vec![&open], &mut transcript_create); + + assert!(TryInto::::try_into(proof).is_err()); + } + + #[test] + fn test_pod_range_proof_128() { + let (comm_1, open_1) = Pedersen::commit(55_u64); + let (comm_2, open_2) = Pedersen::commit(77_u64); + let (comm_3, open_3) = Pedersen::commit(99_u64); + + let mut transcript_create = Transcript::new(b"Test"); + let mut transcript_verify = Transcript::new(b"Test"); + + let proof = RangeProof::create( + vec![55, 77, 99], + vec![64, 32, 32], + vec![&open_1, &open_2, &open_3], + &mut transcript_create, + ); + + let comm_1_point = comm_1.get_point().compress(); + let comm_2_point = comm_2.get_point().compress(); + let comm_3_point = comm_3.get_point().compress(); + + let proof_serialized: pod::RangeProof128 = proof.try_into().unwrap(); + let proof_deserialized: RangeProof = proof_serialized.try_into().unwrap(); + + assert!(proof_deserialized + .verify( + vec![&comm_1_point, &comm_2_point, &comm_3_point], + vec![64, 32, 32], + &mut transcript_verify, + ) + .is_ok()); + + // should fail to serialize to pod::RangeProof64 + let proof = RangeProof::create( + vec![55, 77, 99], + vec![64, 32, 32], + vec![&open_1, &open_2, &open_3], + &mut transcript_create, + ); + + assert!(TryInto::::try_into(proof).is_err()); + } +} diff --git a/zk-token-sdk/src/zk_token_elgamal/mod.rs b/zk-token-sdk/src/zk_token_elgamal/mod.rs new file mode 100644 index 000000000..2663aef6e --- /dev/null +++ b/zk-token-sdk/src/zk_token_elgamal/mod.rs @@ -0,0 +1,3 @@ +pub mod convert; +pub mod ops; +pub mod pod; diff --git a/zk-token-sdk/src/zk_token_elgamal/ops.rs b/zk-token-sdk/src/zk_token_elgamal/ops.rs new file mode 100644 index 000000000..2bb6aa6d0 --- /dev/null +++ b/zk-token-sdk/src/zk_token_elgamal/ops.rs @@ -0,0 +1,272 @@ +pub use target_arch::*; + +#[cfg(not(target_arch = "bpf"))] +mod target_arch { + use { + crate::{encryption::elgamal::ElGamalCT, zk_token_elgamal::pod}, + curve25519_dalek::{constants::RISTRETTO_BASEPOINT_COMPRESSED, scalar::Scalar}, + std::convert::TryInto, + }; + pub const TWO_32: u64 = 4294967296; + + // On input two scalars x0, x1 and two ciphertexts ct0, ct1, + // returns `Some(x0*ct0 + x1*ct1)` or `None` if the input was invalid + fn add_ciphertexts( + scalar_0: Scalar, + ct_0: pod::ElGamalCT, + scalar_1: Scalar, + ct_1: pod::ElGamalCT, + ) -> Option { + let ct_0: ElGamalCT = ct_0.try_into().ok()?; + let ct_1: ElGamalCT = ct_1.try_into().ok()?; + + let ct_sum = ct_0 * scalar_0 + ct_1 * scalar_1; + Some(pod::ElGamalCT::from(ct_sum)) + } + + pub(crate) fn combine_lo_hi( + ct_lo: pod::ElGamalCT, + ct_hi: pod::ElGamalCT, + ) -> Option { + add_ciphertexts(Scalar::one(), ct_lo, Scalar::from(TWO_32), ct_hi) + } + + pub fn add(ct_0: pod::ElGamalCT, ct_1: pod::ElGamalCT) -> Option { + add_ciphertexts(Scalar::one(), ct_0, Scalar::one(), ct_1) + } + + pub fn add_with_lo_hi( + ct_0: pod::ElGamalCT, + ct_1_lo: pod::ElGamalCT, + ct_1_hi: pod::ElGamalCT, + ) -> Option { + let ct_1 = combine_lo_hi(ct_1_lo, ct_1_hi)?; + add_ciphertexts(Scalar::one(), ct_0, Scalar::one(), ct_1) + } + + pub fn subtract(ct_0: pod::ElGamalCT, ct_1: pod::ElGamalCT) -> Option { + add_ciphertexts(Scalar::one(), ct_0, -Scalar::one(), ct_1) + } + + pub fn subtract_with_lo_hi( + ct_0: pod::ElGamalCT, + ct_1_lo: pod::ElGamalCT, + ct_1_hi: pod::ElGamalCT, + ) -> Option { + let ct_1 = combine_lo_hi(ct_1_lo, ct_1_hi)?; + add_ciphertexts(Scalar::one(), ct_0, -Scalar::one(), ct_1) + } + + pub fn add_to(ct: pod::ElGamalCT, amount: u64) -> Option { + let mut amount_as_ct = [0_u8; 64]; + amount_as_ct[..32].copy_from_slice(RISTRETTO_BASEPOINT_COMPRESSED.as_bytes()); + add_ciphertexts( + Scalar::one(), + ct, + Scalar::from(amount), + pod::ElGamalCT(amount_as_ct), + ) + } + + pub fn subtract_from(ct: pod::ElGamalCT, amount: u64) -> Option { + let mut amount_as_ct = [0_u8; 64]; + amount_as_ct[..32].copy_from_slice(RISTRETTO_BASEPOINT_COMPRESSED.as_bytes()); + add_ciphertexts( + Scalar::one(), + ct, + -Scalar::from(amount), + pod::ElGamalCT(amount_as_ct), + ) + } +} + +#[cfg(target_arch = "bpf")] +#[allow(unused_variables)] +mod target_arch { + use crate::zk_token_elgamal::pod; + + pub fn add(ct_0: pod::ElGamalCT, ct_1: pod::ElGamalCT) -> Option { + None + } + + pub fn add_with_lo_hi( + ct_0: pod::ElGamalCT, + ct_1_lo: pod::ElGamalCT, + ct_1_hi: pod::ElGamalCT, + ) -> Option { + None + } + + pub fn subtract(ct_0: pod::ElGamalCT, ct_1: pod::ElGamalCT) -> Option { + None + } + + pub fn subtract_with_lo_hi( + ct_0: pod::ElGamalCT, + ct_1_lo: pod::ElGamalCT, + ct_1_hi: pod::ElGamalCT, + ) -> Option { + None + } + + pub fn add_to(ct: pod::ElGamalCT, amount: u64) -> Option { + None + } + + pub fn subtract_from(ct: pod::ElGamalCT, amount: u64) -> Option { + None + } +} + +#[cfg(test)] +mod tests { + use { + crate::{ + encryption::{ + elgamal::{ElGamal, ElGamalCT}, + pedersen::{Pedersen, PedersenOpen}, + }, + zk_token_elgamal::{ops, pod}, + }, + bytemuck::Zeroable, + curve25519_dalek::scalar::Scalar, + rand::rngs::OsRng, + std::convert::TryInto, + }; + + #[test] + fn test_zero_ct() { + let spendable_balance = pod::ElGamalCT::zeroed(); + let spendable_ct: ElGamalCT = spendable_balance.try_into().unwrap(); + + // spendable_ct should be an encryption of 0 for any public key when + // `PedersenOpen::default()` is used + let (pk, _) = ElGamal::keygen(); + let balance: u64 = 0; + assert_eq!( + spendable_ct, + pk.encrypt_with(balance, &PedersenOpen::default()) + ); + + // homomorphism should work like any other ciphertext + let open = PedersenOpen::random(&mut OsRng); + let transfer_amount_ct = pk.encrypt_with(55_u64, &open); + let transfer_amount_pod: pod::ElGamalCT = transfer_amount_ct.into(); + + let sum = ops::add(spendable_balance, transfer_amount_pod).unwrap(); + + let expected: pod::ElGamalCT = pk.encrypt_with(55_u64, &open).into(); + assert_eq!(expected, sum); + } + + #[test] + fn test_add_to() { + let spendable_balance = pod::ElGamalCT::zeroed(); + + let added_ct = ops::add_to(spendable_balance, 55).unwrap(); + + let (pk, _) = ElGamal::keygen(); + let expected: pod::ElGamalCT = pk.encrypt_with(55_u64, &PedersenOpen::default()).into(); + + assert_eq!(expected, added_ct); + } + + #[test] + fn test_subtract_from() { + let amount = 77_u64; + let (pk, _) = ElGamal::keygen(); + let open = PedersenOpen::random(&mut OsRng); + let encrypted_amount: pod::ElGamalCT = pk.encrypt_with(amount, &open).into(); + + let subtracted_ct = ops::subtract_from(encrypted_amount, 55).unwrap(); + + let expected: pod::ElGamalCT = pk.encrypt_with(22_u64, &open).into(); + + assert_eq!(expected, subtracted_ct); + } + + /// Split u64 number into two u32 numbers + fn split_u64_into_u32(amt: u64) -> (u32, u32) { + let lo = amt as u32; + let hi = (amt >> 32) as u32; + + (lo, hi) + } + + #[test] + fn test_transfer_arithmetic() { + // transfer amount + let transfer_amount: u64 = 55; + let (amount_lo, amount_hi) = split_u64_into_u32(transfer_amount); + + // generate public keys + let (source_pk, _) = ElGamal::keygen(); + let (dest_pk, _) = ElGamal::keygen(); + let (auditor_pk, _) = ElGamal::keygen(); + + // commitments associated with TransferRangeProof + let (comm_lo, open_lo) = Pedersen::commit(amount_lo); + let (comm_hi, open_hi) = Pedersen::commit(amount_hi); + + let comm_lo: pod::PedersenComm = comm_lo.into(); + let comm_hi: pod::PedersenComm = comm_hi.into(); + + // decryption handles associated with TransferValidityProof + let handle_source_lo: pod::PedersenDecHandle = + source_pk.gen_decrypt_handle(&open_lo).into(); + let handle_dest_lo: pod::PedersenDecHandle = dest_pk.gen_decrypt_handle(&open_lo).into(); + let _handle_auditor_lo: pod::PedersenDecHandle = + auditor_pk.gen_decrypt_handle(&open_lo).into(); + + let handle_source_hi: pod::PedersenDecHandle = + source_pk.gen_decrypt_handle(&open_hi).into(); + let handle_dest_hi: pod::PedersenDecHandle = dest_pk.gen_decrypt_handle(&open_hi).into(); + let _handle_auditor_hi: pod::PedersenDecHandle = + auditor_pk.gen_decrypt_handle(&open_hi).into(); + + // source spendable and recipient pending + let source_open = PedersenOpen::random(&mut OsRng); + let dest_open = PedersenOpen::random(&mut OsRng); + + let source_spendable_ct: pod::ElGamalCT = + source_pk.encrypt_with(77_u64, &source_open).into(); + let dest_pending_ct: pod::ElGamalCT = dest_pk.encrypt_with(77_u64, &dest_open).into(); + + // program arithmetic for the source account + + // 1. Combine commitments and handles + let source_lo_ct: pod::ElGamalCT = (comm_lo, handle_source_lo).into(); + let source_hi_ct: pod::ElGamalCT = (comm_hi, handle_source_hi).into(); + + // 2. Combine lo and hi ciphertexts + let source_combined_ct = ops::combine_lo_hi(source_lo_ct, source_hi_ct).unwrap(); + + // 3. Subtract from available balance + let final_source_spendable = + ops::subtract(source_spendable_ct, source_combined_ct).unwrap(); + + // test + let final_source_open = + source_open - (open_lo.clone() + open_hi.clone() * Scalar::from(ops::TWO_32)); + let expected_source: pod::ElGamalCT = + source_pk.encrypt_with(22_u64, &final_source_open).into(); + assert_eq!(expected_source, final_source_spendable); + + // same for the destination account + + // 1. Combine commitments and handles + let dest_lo_ct: pod::ElGamalCT = (comm_lo, handle_dest_lo).into(); + let dest_hi_ct: pod::ElGamalCT = (comm_hi, handle_dest_hi).into(); + + // 2. Combine lo and hi ciphertexts + let dest_combined_ct = ops::combine_lo_hi(dest_lo_ct, dest_hi_ct).unwrap(); + + // 3. Add to pending balance + let final_dest_pending = ops::add(dest_pending_ct, dest_combined_ct).unwrap(); + + let final_dest_open = dest_open + (open_lo + open_hi * Scalar::from(ops::TWO_32)); + let expected_dest_ct: pod::ElGamalCT = + dest_pk.encrypt_with(132_u64, &final_dest_open).into(); + assert_eq!(expected_dest_ct, final_dest_pending); + } +} diff --git a/zk-token-sdk/src/zk_token_elgamal/pod.rs b/zk-token-sdk/src/zk_token_elgamal/pod.rs new file mode 100644 index 000000000..aa26f7fe3 --- /dev/null +++ b/zk-token-sdk/src/zk_token_elgamal/pod.rs @@ -0,0 +1,45 @@ +use bytemuck::{Pod, Zeroable}; + +#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] +#[repr(transparent)] +pub struct Scalar(pub [u8; 32]); + +#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] +#[repr(transparent)] +pub struct CompressedRistretto(pub [u8; 32]); + +#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] +#[repr(transparent)] +pub struct ElGamalCT(pub [u8; 64]); + +#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] +#[repr(transparent)] +pub struct ElGamalPK(pub [u8; 32]); + +#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] +#[repr(transparent)] +pub struct PedersenComm(pub [u8; 32]); + +#[derive(Clone, Copy, Pod, Zeroable, PartialEq)] +#[repr(transparent)] +pub struct PedersenDecHandle(pub [u8; 32]); + +/// Serialization of range proofs for 64-bit numbers (for `Withdraw` instruction) +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct RangeProof64(pub [u8; 672]); + +// `PodRangeProof64` is a Pod and Zeroable. +// Add the marker traits manually because `bytemuck` only adds them for some `u8` arrays +unsafe impl Zeroable for RangeProof64 {} +unsafe impl Pod for RangeProof64 {} + +/// Serialization of range proofs for 128-bit numbers (for `TransferRangeProof` instruction) +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct RangeProof128(pub [u8; 736]); + +// `PodRangeProof128` is a Pod and Zeroable. +// Add the marker traits manually because `bytemuck` only adds them for some `u8` arrays +unsafe impl Zeroable for RangeProof128 {} +unsafe impl Pod for RangeProof128 {}