samkim-crypto 1 рік тому
батько
коміт
e621336aca

+ 1 - 0
zk-sdk/src/encryption/mod.rs

@@ -25,6 +25,7 @@ pub mod elgamal;
 pub mod grouped_elgamal;
 #[cfg(not(target_os = "solana"))]
 pub mod pedersen;
+pub mod pod;
 
 /// Byte length of an authenticated encryption secret key
 pub const AE_KEY_LEN: usize = 16;

+ 80 - 0
zk-sdk/src/encryption/pod/auth_encryption.rs

@@ -0,0 +1,80 @@
+//! Plain Old Data types for the AES128-GCM-SIV authenticated encryption scheme.
+
+#[cfg(not(target_os = "solana"))]
+use crate::{encryption::auth_encryption::AeCiphertext, errors::AuthenticatedEncryptionError};
+use {
+    crate::encryption::{pod::impl_from_str, AE_CIPHERTEXT_LEN},
+    base64::{prelude::BASE64_STANDARD, Engine},
+    bytemuck::{Pod, Zeroable},
+    std::fmt,
+};
+
+/// Maximum length of a base64 encoded authenticated encryption ciphertext
+const AE_CIPHERTEXT_MAX_BASE64_LEN: usize = 48;
+
+/// The `AeCiphertext` type as a `Pod`.
+#[derive(Clone, Copy, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct PodAeCiphertext(pub(crate) [u8; AE_CIPHERTEXT_LEN]);
+
+// `PodAeCiphertext` is a wrapper type for a byte array, which is both `Pod` and `Zeroable`. However,
+// the marker traits `bytemuck::Pod` and `bytemuck::Zeroable` can only be derived for power-of-two
+// length byte arrays. Directly implement these traits for `PodAeCiphertext`.
+unsafe impl Zeroable for PodAeCiphertext {}
+unsafe impl Pod for PodAeCiphertext {}
+
+impl fmt::Debug for PodAeCiphertext {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{:?}", self.0)
+    }
+}
+
+impl fmt::Display for PodAeCiphertext {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", BASE64_STANDARD.encode(self.0))
+    }
+}
+
+impl_from_str!(
+    TYPE = PodAeCiphertext,
+    BYTES_LEN = AE_CIPHERTEXT_LEN,
+    BASE64_LEN = AE_CIPHERTEXT_MAX_BASE64_LEN
+);
+
+impl Default for PodAeCiphertext {
+    fn default() -> Self {
+        Self::zeroed()
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl From<AeCiphertext> for PodAeCiphertext {
+    fn from(decoded_ciphertext: AeCiphertext) -> Self {
+        Self(decoded_ciphertext.to_bytes())
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl TryFrom<PodAeCiphertext> for AeCiphertext {
+    type Error = AuthenticatedEncryptionError;
+
+    fn try_from(pod_ciphertext: PodAeCiphertext) -> Result<Self, Self::Error> {
+        Self::from_bytes(&pod_ciphertext.0).ok_or(AuthenticatedEncryptionError::Deserialization)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {super::*, crate::encryption::auth_encryption::AeKey, std::str::FromStr};
+
+    #[test]
+    fn ae_ciphertext_fromstr() {
+        let ae_key = AeKey::new_rand();
+        let expected_ae_ciphertext: PodAeCiphertext = ae_key.encrypt(0_u64).into();
+
+        let ae_ciphertext_base64_str = format!("{}", expected_ae_ciphertext);
+        let computed_ae_ciphertext = PodAeCiphertext::from_str(&ae_ciphertext_base64_str).unwrap();
+
+        assert_eq!(expected_ae_ciphertext, computed_ae_ciphertext);
+    }
+}

+ 173 - 0
zk-sdk/src/encryption/pod/elgamal.rs

@@ -0,0 +1,173 @@
+//! Plain Old Data types for the ElGamal encryption scheme.
+
+use {
+    crate::encryption::{
+        pod::impl_from_str, DECRYPT_HANDLE_LEN, ELGAMAL_CIPHERTEXT_LEN, ELGAMAL_PUBKEY_LEN,
+    },
+    base64::{prelude::BASE64_STANDARD, Engine},
+    bytemuck::{Pod, Zeroable},
+    std::fmt,
+};
+#[cfg(not(target_os = "solana"))]
+use {
+    crate::{
+        encryption::elgamal::{DecryptHandle, ElGamalCiphertext, ElGamalPubkey},
+        errors::ElGamalError,
+    },
+    curve25519_dalek::ristretto::CompressedRistretto,
+};
+
+/// Maximum length of a base64 encoded ElGamal public key
+const ELGAMAL_PUBKEY_MAX_BASE64_LEN: usize = 44;
+
+/// Maximum length of a base64 encoded ElGamal ciphertext
+const ELGAMAL_CIPHERTEXT_MAX_BASE64_LEN: usize = 88;
+
+/// The `ElGamalCiphertext` type as a `Pod`.
+#[derive(Clone, Copy, Pod, Zeroable, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct PodElGamalCiphertext(pub(crate) [u8; ELGAMAL_CIPHERTEXT_LEN]);
+
+impl fmt::Debug for PodElGamalCiphertext {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{:?}", self.0)
+    }
+}
+
+impl fmt::Display for PodElGamalCiphertext {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", BASE64_STANDARD.encode(self.0))
+    }
+}
+
+impl Default for PodElGamalCiphertext {
+    fn default() -> Self {
+        Self::zeroed()
+    }
+}
+
+impl_from_str!(
+    TYPE = PodElGamalCiphertext,
+    BYTES_LEN = ELGAMAL_CIPHERTEXT_LEN,
+    BASE64_LEN = ELGAMAL_CIPHERTEXT_MAX_BASE64_LEN
+);
+
+#[cfg(not(target_os = "solana"))]
+impl From<ElGamalCiphertext> for PodElGamalCiphertext {
+    fn from(decoded_ciphertext: ElGamalCiphertext) -> Self {
+        Self(decoded_ciphertext.to_bytes())
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl TryFrom<PodElGamalCiphertext> for ElGamalCiphertext {
+    type Error = ElGamalError;
+
+    fn try_from(pod_ciphertext: PodElGamalCiphertext) -> Result<Self, Self::Error> {
+        Self::from_bytes(&pod_ciphertext.0).ok_or(ElGamalError::CiphertextDeserialization)
+    }
+}
+
+/// The `ElGamalPubkey` type as a `Pod`.
+#[derive(Clone, Copy, Default, Pod, Zeroable, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct PodElGamalPubkey(pub(crate) [u8; ELGAMAL_PUBKEY_LEN]);
+
+impl fmt::Debug for PodElGamalPubkey {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{:?}", self.0)
+    }
+}
+
+impl fmt::Display for PodElGamalPubkey {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", BASE64_STANDARD.encode(self.0))
+    }
+}
+
+impl_from_str!(
+    TYPE = PodElGamalPubkey,
+    BYTES_LEN = ELGAMAL_PUBKEY_LEN,
+    BASE64_LEN = ELGAMAL_PUBKEY_MAX_BASE64_LEN
+);
+
+#[cfg(not(target_os = "solana"))]
+impl From<ElGamalPubkey> for PodElGamalPubkey {
+    fn from(decoded_pubkey: ElGamalPubkey) -> Self {
+        Self(decoded_pubkey.into())
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl TryFrom<PodElGamalPubkey> for ElGamalPubkey {
+    type Error = ElGamalError;
+
+    fn try_from(pod_pubkey: PodElGamalPubkey) -> Result<Self, Self::Error> {
+        Self::try_from(pod_pubkey.0.as_slice())
+    }
+}
+
+/// The `DecryptHandle` type as a `Pod`.
+#[derive(Clone, Copy, Default, Pod, Zeroable, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct PodDecryptHandle(pub(crate) [u8; DECRYPT_HANDLE_LEN]);
+
+impl fmt::Debug for PodDecryptHandle {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{:?}", self.0)
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl From<DecryptHandle> for PodDecryptHandle {
+    fn from(decoded_handle: DecryptHandle) -> Self {
+        Self(decoded_handle.to_bytes())
+    }
+}
+
+// For proof verification, interpret pod::DecryptHandle as CompressedRistretto
+#[cfg(not(target_os = "solana"))]
+impl From<PodDecryptHandle> for CompressedRistretto {
+    fn from(pod_handle: PodDecryptHandle) -> Self {
+        Self(pod_handle.0)
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl TryFrom<PodDecryptHandle> for DecryptHandle {
+    type Error = ElGamalError;
+
+    fn try_from(pod_handle: PodDecryptHandle) -> Result<Self, Self::Error> {
+        Self::from_bytes(&pod_handle.0).ok_or(ElGamalError::CiphertextDeserialization)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {super::*, crate::encryption::elgamal::ElGamalKeypair, std::str::FromStr};
+
+    #[test]
+    fn elgamal_pubkey_fromstr() {
+        let elgamal_keypair = ElGamalKeypair::new_rand();
+        let expected_elgamal_pubkey: PodElGamalPubkey = (*elgamal_keypair.pubkey()).into();
+
+        let elgamal_pubkey_base64_str = format!("{}", expected_elgamal_pubkey);
+        let computed_elgamal_pubkey =
+            PodElGamalPubkey::from_str(&elgamal_pubkey_base64_str).unwrap();
+
+        assert_eq!(expected_elgamal_pubkey, computed_elgamal_pubkey);
+    }
+
+    #[test]
+    fn elgamal_ciphertext_fromstr() {
+        let elgamal_keypair = ElGamalKeypair::new_rand();
+        let expected_elgamal_ciphertext: PodElGamalCiphertext =
+            elgamal_keypair.pubkey().encrypt(0_u64).into();
+
+        let elgamal_ciphertext_base64_str = format!("{}", expected_elgamal_ciphertext);
+        let computed_elgamal_ciphertext =
+            PodElGamalCiphertext::from_str(&elgamal_ciphertext_base64_str).unwrap();
+
+        assert_eq!(expected_elgamal_ciphertext, computed_elgamal_ciphertext);
+    }
+}

+ 225 - 0
zk-sdk/src/encryption/pod/grouped_elgamal.rs

@@ -0,0 +1,225 @@
+//! Plain Old Data types for the Grouped ElGamal encryption scheme.
+
+#[cfg(not(target_os = "solana"))]
+use crate::encryption::grouped_elgamal::GroupedElGamalCiphertext;
+use {
+    crate::{
+        encryption::{
+            pod::{elgamal::PodElGamalCiphertext, pedersen::PodPedersenCommitment},
+            DECRYPT_HANDLE_LEN, ELGAMAL_CIPHERTEXT_LEN, PEDERSEN_COMMITMENT_LEN,
+        },
+        errors::ElGamalError,
+    },
+    bytemuck::{Pod, Zeroable},
+    std::fmt,
+};
+
+macro_rules! impl_extract {
+    (TYPE = $type:ident) => {
+        impl $type {
+            /// Extract the commitment component from a grouped ciphertext
+            pub fn extract_commitment(&self) -> PodPedersenCommitment {
+                // `GROUPED_ELGAMAL_CIPHERTEXT_2_HANDLES` guaranteed to be at least `PEDERSEN_COMMITMENT_LEN`
+                let commitment = self.0[..PEDERSEN_COMMITMENT_LEN].try_into().unwrap();
+                PodPedersenCommitment(commitment)
+            }
+
+            /// Extract a regular ElGamal ciphertext using the decrypt handle at a specified index.
+            pub fn try_extract_ciphertext(
+                &self,
+                index: usize,
+            ) -> Result<PodElGamalCiphertext, ElGamalError> {
+                let mut ciphertext_bytes = [0u8; ELGAMAL_CIPHERTEXT_LEN];
+                ciphertext_bytes[..PEDERSEN_COMMITMENT_LEN]
+                    .copy_from_slice(&self.0[..PEDERSEN_COMMITMENT_LEN]);
+
+                let handle_start = DECRYPT_HANDLE_LEN
+                    .checked_mul(index)
+                    .and_then(|n| n.checked_add(PEDERSEN_COMMITMENT_LEN))
+                    .ok_or(ElGamalError::CiphertextDeserialization)?;
+                let handle_end = handle_start
+                    .checked_add(DECRYPT_HANDLE_LEN)
+                    .ok_or(ElGamalError::CiphertextDeserialization)?;
+                ciphertext_bytes[PEDERSEN_COMMITMENT_LEN..].copy_from_slice(
+                    self.0
+                        .get(handle_start..handle_end)
+                        .ok_or(ElGamalError::CiphertextDeserialization)?,
+                );
+
+                Ok(PodElGamalCiphertext(ciphertext_bytes))
+            }
+        }
+    };
+}
+
+/// Byte length of a grouped ElGamal ciphertext with 2 handles
+const GROUPED_ELGAMAL_CIPHERTEXT_2_HANDLES: usize =
+    PEDERSEN_COMMITMENT_LEN + DECRYPT_HANDLE_LEN + DECRYPT_HANDLE_LEN;
+
+/// Byte length of a grouped ElGamal ciphertext with 3 handles
+const GROUPED_ELGAMAL_CIPHERTEXT_3_HANDLES: usize =
+    PEDERSEN_COMMITMENT_LEN + DECRYPT_HANDLE_LEN + DECRYPT_HANDLE_LEN + DECRYPT_HANDLE_LEN;
+
+/// The `GroupedElGamalCiphertext` type with two decryption handles as a `Pod`
+#[derive(Clone, Copy, Pod, Zeroable, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct PodGroupedElGamalCiphertext2Handles(
+    pub(crate) [u8; GROUPED_ELGAMAL_CIPHERTEXT_2_HANDLES],
+);
+
+impl fmt::Debug for PodGroupedElGamalCiphertext2Handles {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{:?}", self.0)
+    }
+}
+
+impl Default for PodGroupedElGamalCiphertext2Handles {
+    fn default() -> Self {
+        Self::zeroed()
+    }
+}
+#[cfg(not(target_os = "solana"))]
+impl From<GroupedElGamalCiphertext<2>> for PodGroupedElGamalCiphertext2Handles {
+    fn from(decoded_ciphertext: GroupedElGamalCiphertext<2>) -> Self {
+        Self(decoded_ciphertext.to_bytes().try_into().unwrap())
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl TryFrom<PodGroupedElGamalCiphertext2Handles> for GroupedElGamalCiphertext<2> {
+    type Error = ElGamalError;
+
+    fn try_from(pod_ciphertext: PodGroupedElGamalCiphertext2Handles) -> Result<Self, Self::Error> {
+        Self::from_bytes(&pod_ciphertext.0).ok_or(ElGamalError::CiphertextDeserialization)
+    }
+}
+
+impl_extract!(TYPE = PodGroupedElGamalCiphertext2Handles);
+
+/// The `GroupedElGamalCiphertext` type with three decryption handles as a `Pod`
+#[derive(Clone, Copy, Pod, Zeroable, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct PodGroupedElGamalCiphertext3Handles(
+    pub(crate) [u8; GROUPED_ELGAMAL_CIPHERTEXT_3_HANDLES],
+);
+
+impl fmt::Debug for PodGroupedElGamalCiphertext3Handles {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{:?}", self.0)
+    }
+}
+
+impl Default for PodGroupedElGamalCiphertext3Handles {
+    fn default() -> Self {
+        Self::zeroed()
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl From<GroupedElGamalCiphertext<3>> for PodGroupedElGamalCiphertext3Handles {
+    fn from(decoded_ciphertext: GroupedElGamalCiphertext<3>) -> Self {
+        Self(decoded_ciphertext.to_bytes().try_into().unwrap())
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl TryFrom<PodGroupedElGamalCiphertext3Handles> for GroupedElGamalCiphertext<3> {
+    type Error = ElGamalError;
+
+    fn try_from(pod_ciphertext: PodGroupedElGamalCiphertext3Handles) -> Result<Self, Self::Error> {
+        Self::from_bytes(&pod_ciphertext.0).ok_or(ElGamalError::CiphertextDeserialization)
+    }
+}
+
+impl_extract!(TYPE = PodGroupedElGamalCiphertext3Handles);
+
+#[cfg(test)]
+mod tests {
+    use {
+        super::*,
+        crate::encryption::{
+            elgamal::ElGamalKeypair, grouped_elgamal::GroupedElGamal, pedersen::Pedersen,
+            pod::pedersen::PodPedersenCommitment,
+        },
+    };
+
+    #[test]
+    fn test_2_handles_ciphertext_extraction() {
+        let elgamal_keypair_0 = ElGamalKeypair::new_rand();
+        let elgamal_keypair_1 = ElGamalKeypair::new_rand();
+
+        let amount: u64 = 10;
+        let (commitment, opening) = Pedersen::new(amount);
+
+        let grouped_ciphertext = GroupedElGamal::encrypt_with(
+            [elgamal_keypair_0.pubkey(), elgamal_keypair_1.pubkey()],
+            amount,
+            &opening,
+        );
+        let pod_grouped_ciphertext: PodGroupedElGamalCiphertext2Handles = grouped_ciphertext.into();
+
+        let expected_pod_commitment: PodPedersenCommitment = commitment.into();
+        let actual_pod_commitment = pod_grouped_ciphertext.extract_commitment();
+        assert_eq!(expected_pod_commitment, actual_pod_commitment);
+
+        let expected_ciphertext_0 = elgamal_keypair_0.pubkey().encrypt_with(amount, &opening);
+        let expected_pod_ciphertext_0: PodElGamalCiphertext = expected_ciphertext_0.into();
+        let actual_pod_ciphertext_0 = pod_grouped_ciphertext.try_extract_ciphertext(0).unwrap();
+        assert_eq!(expected_pod_ciphertext_0, actual_pod_ciphertext_0);
+
+        let expected_ciphertext_1 = elgamal_keypair_1.pubkey().encrypt_with(amount, &opening);
+        let expected_pod_ciphertext_1: PodElGamalCiphertext = expected_ciphertext_1.into();
+        let actual_pod_ciphertext_1 = pod_grouped_ciphertext.try_extract_ciphertext(1).unwrap();
+        assert_eq!(expected_pod_ciphertext_1, actual_pod_ciphertext_1);
+
+        let err = pod_grouped_ciphertext
+            .try_extract_ciphertext(2)
+            .unwrap_err();
+        assert_eq!(err, ElGamalError::CiphertextDeserialization);
+    }
+
+    #[test]
+    fn test_3_handles_ciphertext_extraction() {
+        let elgamal_keypair_0 = ElGamalKeypair::new_rand();
+        let elgamal_keypair_1 = ElGamalKeypair::new_rand();
+        let elgamal_keypair_2 = ElGamalKeypair::new_rand();
+
+        let amount: u64 = 10;
+        let (commitment, opening) = Pedersen::new(amount);
+
+        let grouped_ciphertext = GroupedElGamal::encrypt_with(
+            [
+                elgamal_keypair_0.pubkey(),
+                elgamal_keypair_1.pubkey(),
+                elgamal_keypair_2.pubkey(),
+            ],
+            amount,
+            &opening,
+        );
+        let pod_grouped_ciphertext: PodGroupedElGamalCiphertext3Handles = grouped_ciphertext.into();
+
+        let expected_pod_commitment: PodPedersenCommitment = commitment.into();
+        let actual_pod_commitment = pod_grouped_ciphertext.extract_commitment();
+        assert_eq!(expected_pod_commitment, actual_pod_commitment);
+
+        let expected_ciphertext_0 = elgamal_keypair_0.pubkey().encrypt_with(amount, &opening);
+        let expected_pod_ciphertext_0: PodElGamalCiphertext = expected_ciphertext_0.into();
+        let actual_pod_ciphertext_0 = pod_grouped_ciphertext.try_extract_ciphertext(0).unwrap();
+        assert_eq!(expected_pod_ciphertext_0, actual_pod_ciphertext_0);
+
+        let expected_ciphertext_1 = elgamal_keypair_1.pubkey().encrypt_with(amount, &opening);
+        let expected_pod_ciphertext_1: PodElGamalCiphertext = expected_ciphertext_1.into();
+        let actual_pod_ciphertext_1 = pod_grouped_ciphertext.try_extract_ciphertext(1).unwrap();
+        assert_eq!(expected_pod_ciphertext_1, actual_pod_ciphertext_1);
+
+        let expected_ciphertext_2 = elgamal_keypair_2.pubkey().encrypt_with(amount, &opening);
+        let expected_pod_ciphertext_2: PodElGamalCiphertext = expected_ciphertext_2.into();
+        let actual_pod_ciphertext_2 = pod_grouped_ciphertext.try_extract_ciphertext(2).unwrap();
+        assert_eq!(expected_pod_ciphertext_2, actual_pod_ciphertext_2);
+
+        let err = pod_grouped_ciphertext
+            .try_extract_ciphertext(3)
+            .unwrap_err();
+        assert_eq!(err, ElGamalError::CiphertextDeserialization);
+    }
+}

+ 28 - 0
zk-sdk/src/encryption/pod/mod.rs

@@ -0,0 +1,28 @@
+pub mod auth_encryption;
+pub mod elgamal;
+pub mod grouped_elgamal;
+pub mod pedersen;
+
+macro_rules! impl_from_str {
+    (TYPE = $type:ident, BYTES_LEN = $bytes_len:expr, BASE64_LEN = $base64_len:expr) => {
+        impl std::str::FromStr for $type {
+            type Err = crate::errors::ParseError;
+
+            fn from_str(s: &str) -> Result<Self, Self::Err> {
+                if s.len() > $base64_len {
+                    return Err(Self::Err::WrongSize);
+                }
+                let mut bytes = [0u8; $bytes_len];
+                let decoded_len = BASE64_STANDARD
+                    .decode_slice(s, &mut bytes)
+                    .map_err(|_| Self::Err::Invalid)?;
+                if decoded_len != $bytes_len {
+                    Err(Self::Err::WrongSize)
+                } else {
+                    Ok($type(bytes))
+                }
+            }
+        }
+    };
+}
+pub(crate) use impl_from_str;

+ 47 - 0
zk-sdk/src/encryption/pod/pedersen.rs

@@ -0,0 +1,47 @@
+//! Plain Old Data type for the Pedersen commitment scheme.
+
+use {
+    crate::encryption::PEDERSEN_COMMITMENT_LEN,
+    bytemuck::{Pod, Zeroable},
+    std::fmt,
+};
+#[cfg(not(target_os = "solana"))]
+use {
+    crate::{encryption::pedersen::PedersenCommitment, errors::ElGamalError},
+    curve25519_dalek::ristretto::CompressedRistretto,
+};
+
+/// The `PedersenCommitment` type as a `Pod`.
+#[derive(Clone, Copy, Default, Pod, Zeroable, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct PodPedersenCommitment(pub(crate) [u8; PEDERSEN_COMMITMENT_LEN]);
+
+impl fmt::Debug for PodPedersenCommitment {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{:?}", self.0)
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl From<PedersenCommitment> for PodPedersenCommitment {
+    fn from(decoded_commitment: PedersenCommitment) -> Self {
+        Self(decoded_commitment.to_bytes())
+    }
+}
+
+// For proof verification, interpret pod::PedersenCommitment directly as CompressedRistretto
+#[cfg(not(target_os = "solana"))]
+impl From<PodPedersenCommitment> for CompressedRistretto {
+    fn from(pod_commitment: PodPedersenCommitment) -> Self {
+        Self(pod_commitment.0)
+    }
+}
+
+#[cfg(not(target_os = "solana"))]
+impl TryFrom<PodPedersenCommitment> for PedersenCommitment {
+    type Error = ElGamalError;
+
+    fn try_from(pod_commitment: PodPedersenCommitment) -> Result<Self, Self::Error> {
+        Self::from_bytes(&pod_commitment.0).ok_or(ElGamalError::CiphertextDeserialization)
+    }
+}

+ 8 - 0
zk-sdk/src/errors.rs

@@ -36,3 +36,11 @@ pub enum TranscriptError {
     #[error("point is the identity")]
     ValidationError,
 }
+
+#[derive(Error, Debug, Clone, Eq, PartialEq)]
+pub enum ParseError {
+    #[error("String is the wrong size")]
+    WrongSize,
+    #[error("Invalid Base64 string")]
+    Invalid,
+}

+ 1 - 0
zk-sdk/src/lib.rs

@@ -22,6 +22,7 @@
 pub mod elgamal_program;
 pub mod encryption;
 pub mod errors;
+pub mod pod;
 mod range_proof;
 mod sigma_proofs;
 mod transcript;

+ 29 - 0
zk-sdk/src/pod.rs

@@ -0,0 +1,29 @@
+use bytemuck::{Pod, Zeroable};
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Pod, Zeroable)]
+#[repr(transparent)]
+pub struct PodU16([u8; 2]);
+impl From<u16> for PodU16 {
+    fn from(n: u16) -> Self {
+        Self(n.to_le_bytes())
+    }
+}
+impl From<PodU16> for u16 {
+    fn from(pod: PodU16) -> Self {
+        Self::from_le_bytes(pod.0)
+    }
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Pod, Zeroable)]
+#[repr(transparent)]
+pub struct PodU64([u8; 8]);
+impl From<u64> for PodU64 {
+    fn from(n: u64) -> Self {
+        Self(n.to_le_bytes())
+    }
+}
+impl From<PodU64> for u64 {
+    fn from(pod: PodU64) -> Self {
+        Self::from_le_bytes(pod.0)
+    }
+}