[zk-token-sdk] Add `GroupedElGamalCiphertext` type (#31849)

* add `GroupedElGamalCiphertext` type

* add `GroupedElGamalCiphertext` type in `zk_token_elgamal::pod`

* cargo fmt

* Apply suggestions from code review

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

* cargo fmt

* add explanation on `expected_byte_length`

* use `checked_add` and `checked_mul`

---------

Co-authored-by: Tyera <teulberg@gmail.com>
This commit is contained in:
samkim-crypto 2023-05-31 10:01:44 +09:00 committed by GitHub
parent 1af1d42b9a
commit 502f118931
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 366 additions and 0 deletions

View File

@ -0,0 +1,290 @@
//! The twisted ElGamal group encryption implementation.
//!
//! The message space consists of any number that is representable as a scalar (a.k.a. "exponent")
//! for Curve25519.
//!
//! A regular twisted ElGamal ciphertext consists of two components:
//! - A Pedersen commitment that encodes a message to be encrypted
//! - A "decryption handle" that binds the Pedersen opening to a specific public key
//! The ciphertext can be generalized to hold not a single decryption handle, but multiple handles
//! pertaining to multiple ElGamal public keys. These ciphertexts are referred to as a "grouped"
//! ElGamal ciphertext.
//!
use {
crate::encryption::{
discrete_log::DiscreteLog,
elgamal::{DecryptHandle, ElGamalCiphertext, ElGamalPubkey, ElGamalSecretKey},
pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
},
curve25519_dalek::scalar::Scalar,
thiserror::Error,
};
#[derive(Error, Clone, Debug, Eq, PartialEq)]
pub enum GroupedElGamalError {
#[error("index out of bounds")]
IndexOutOfBounds,
}
/// Algorithm handle for the grouped ElGamal encryption
pub struct GroupedElGamal<const N: usize>;
impl<const N: usize> GroupedElGamal<N> {
/// Encrypts an amount under an array of ElGamal public keys.
///
/// This function is randomized. It internally samples a scalar element using `OsRng`.
pub fn encrypt<T: Into<Scalar>>(
pubkeys: [&ElGamalPubkey; N],
amount: T,
) -> GroupedElGamalCiphertext<N> {
let (commitment, opening) = Pedersen::new(amount);
let handles: [DecryptHandle; N] = pubkeys
.iter()
.map(|handle| handle.decrypt_handle(&opening))
.collect::<Vec<DecryptHandle>>()
.try_into()
.unwrap();
GroupedElGamalCiphertext {
commitment,
handles,
}
}
/// Encrypts an amount under an array of ElGamal public keys using a specified Pedersen
/// opening.
pub fn encrypt_with<T: Into<Scalar>>(
pubkeys: [&ElGamalPubkey; N],
amount: T,
opening: &PedersenOpening,
) -> GroupedElGamalCiphertext<N> {
let commitment = Pedersen::with(amount, opening);
let handles: [DecryptHandle; N] = pubkeys
.iter()
.map(|handle| handle.decrypt_handle(opening))
.collect::<Vec<DecryptHandle>>()
.try_into()
.unwrap();
GroupedElGamalCiphertext {
commitment,
handles,
}
}
/// Converts a grouped ElGamal ciphertext into a regular ElGamal ciphertext using the decrypt
/// handle at a specified index.
fn to_elgamal_ciphertext(
grouped_ciphertext: &GroupedElGamalCiphertext<N>,
index: usize,
) -> Result<ElGamalCiphertext, GroupedElGamalError> {
let handle = grouped_ciphertext
.handles
.get(index)
.ok_or(GroupedElGamalError::IndexOutOfBounds)?;
Ok(ElGamalCiphertext {
commitment: grouped_ciphertext.commitment,
handle: *handle,
})
}
/// Decrypts a grouped ElGamal ciphertext using an ElGamal secret key pertaining to a
/// decryption handle at a specified index.
///
/// The output of this function is of type `DiscreteLog`. To recover the originally encrypted
/// amount, use `DiscreteLog::decode`.
fn decrypt(
grouped_ciphertext: &GroupedElGamalCiphertext<N>,
secret: &ElGamalSecretKey,
index: usize,
) -> Result<DiscreteLog, GroupedElGamalError> {
Self::to_elgamal_ciphertext(grouped_ciphertext, index)
.map(|ciphertext| ciphertext.decrypt(secret))
}
/// Decrypts a grouped ElGamal ciphertext to a number that is interpreted as a positive 32-bit
/// number (but still of type `u64`).
///
/// If the originally encrypted amount is not a positive 32-bit number, then the function
/// Result contains `None`.
fn decrypt_u32(
grouped_ciphertext: &GroupedElGamalCiphertext<N>,
secret: &ElGamalSecretKey,
index: usize,
) -> Result<Option<u64>, GroupedElGamalError> {
Self::to_elgamal_ciphertext(grouped_ciphertext, index)
.map(|ciphertext| ciphertext.decrypt_u32(secret))
}
}
/// A grouped ElGamal ciphertext.
///
/// The type is defined with a generic constant parameter that specifies the number of
/// decryption handles that the ciphertext holds.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GroupedElGamalCiphertext<const N: usize> {
pub commitment: PedersenCommitment,
pub handles: [DecryptHandle; N],
}
impl<const N: usize> GroupedElGamalCiphertext<N> {
/// Decrypts the grouped ElGamal ciphertext using an ElGamal secret key pertaining to a
/// specified index.
///
/// The output of this function is of type `DiscreteLog`. To recover the originally encrypted
/// amount, use `DiscreteLog::decode`.
pub fn decrypt(
&self,
secret: &ElGamalSecretKey,
index: usize,
) -> Result<DiscreteLog, GroupedElGamalError> {
GroupedElGamal::decrypt(self, secret, index)
}
/// Decrypts the grouped ElGamal ciphertext to a number that is interpreted as a positive 32-bit
/// number (but still of type `u64`).
///
/// If the originally encrypted amount is not a positive 32-bit number, then the function
/// returns `None`.
pub fn decrypt_u32(
&self,
secret: &ElGamalSecretKey,
index: usize,
) -> Result<Option<u64>, GroupedElGamalError> {
GroupedElGamal::decrypt_u32(self, secret, index)
}
/// The expected length of a serialized grouped ElGamal ciphertext.
///
/// A grouped ElGamal ciphertext consists of a Pedersen commitment and an array of decryption
/// handles. The commitment and decryption handles are each a single Curve25519 group element
/// that is serialized as 32 bytes. Therefore, the total byte length of a grouped ciphertext is
/// `(N+1) * 32`.
fn expected_byte_length() -> usize {
N.checked_add(1)
.and_then(|length| length.checked_mul(32))
.unwrap()
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(Self::expected_byte_length());
buf.extend_from_slice(&self.commitment.to_bytes());
self.handles
.iter()
.for_each(|handle| buf.extend_from_slice(&handle.to_bytes()));
buf
}
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() != Self::expected_byte_length() {
return None;
}
let mut iter = bytes.chunks(32);
let commitment = PedersenCommitment::from_bytes(iter.next()?)?;
let mut handles = Vec::with_capacity(N);
for handle_bytes in iter {
handles.push(DecryptHandle::from_bytes(handle_bytes)?);
}
Some(Self {
commitment,
handles: handles.try_into().unwrap(),
})
}
}
#[cfg(test)]
mod tests {
use {super::*, crate::encryption::elgamal::ElGamalKeypair};
#[test]
fn test_grouped_elgamal_encrypt_decrypt_correctness() {
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 grouped_ciphertext = GroupedElGamal::encrypt(
[
&elgamal_keypair_0.public,
&elgamal_keypair_1.public,
&elgamal_keypair_2.public,
],
amount,
);
assert_eq!(
Some(amount),
grouped_ciphertext
.decrypt_u32(&elgamal_keypair_0.secret, 0)
.unwrap()
);
assert_eq!(
Some(amount),
grouped_ciphertext
.decrypt_u32(&elgamal_keypair_1.secret, 1)
.unwrap()
);
assert_eq!(
Some(amount),
grouped_ciphertext
.decrypt_u32(&elgamal_keypair_2.secret, 2)
.unwrap()
);
assert_eq!(
GroupedElGamalError::IndexOutOfBounds,
grouped_ciphertext
.decrypt_u32(&elgamal_keypair_0.secret, 3)
.unwrap_err()
);
}
#[test]
fn test_grouped_ciphertext_bytes() {
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 grouped_ciphertext = GroupedElGamal::encrypt(
[
&elgamal_keypair_0.public,
&elgamal_keypair_1.public,
&elgamal_keypair_2.public,
],
amount,
);
let produced_bytes = grouped_ciphertext.to_bytes();
assert_eq!(produced_bytes.len(), 128);
let decoded_grouped_ciphertext =
GroupedElGamalCiphertext::<3>::from_bytes(&produced_bytes).unwrap();
assert_eq!(
Some(amount),
decoded_grouped_ciphertext
.decrypt_u32(&elgamal_keypair_0.secret, 0)
.unwrap()
);
assert_eq!(
Some(amount),
decoded_grouped_ciphertext
.decrypt_u32(&elgamal_keypair_1.secret, 1)
.unwrap()
);
assert_eq!(
Some(amount),
decoded_grouped_ciphertext
.decrypt_u32(&elgamal_keypair_2.secret, 2)
.unwrap()
);
}
}

View File

@ -13,4 +13,5 @@
pub mod auth_encryption;
pub mod discrete_log;
pub mod elgamal;
pub mod grouped_elgamal;
pub mod pedersen;

View File

@ -0,0 +1,73 @@
//! Plain Old Data types for the Grouped ElGamal encryption scheme.
#[cfg(not(target_os = "solana"))]
use crate::{encryption::grouped_elgamal::GroupedElGamalCiphertext, errors::ProofError};
use {
crate::zk_token_elgamal::pod::{Pod, Zeroable},
std::fmt,
};
/// The `GroupedElGamalCiphertext` type with two decryption handles as a `Pod`
#[derive(Clone, Copy, Pod, Zeroable, PartialEq, Eq)]
#[repr(transparent)]
pub struct GroupedElGamalCiphertext2Handles(pub [u8; 96]);
impl fmt::Debug for GroupedElGamalCiphertext2Handles {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl Default for GroupedElGamalCiphertext2Handles {
fn default() -> Self {
Self::zeroed()
}
}
#[cfg(not(target_os = "solana"))]
impl From<GroupedElGamalCiphertext<2>> for GroupedElGamalCiphertext2Handles {
fn from(decoded_ciphertext: GroupedElGamalCiphertext<2>) -> Self {
Self(decoded_ciphertext.to_bytes().try_into().unwrap())
}
}
#[cfg(not(target_os = "solana"))]
impl TryFrom<GroupedElGamalCiphertext2Handles> for GroupedElGamalCiphertext<2> {
type Error = ProofError;
fn try_from(pod_ciphertext: GroupedElGamalCiphertext2Handles) -> Result<Self, Self::Error> {
Self::from_bytes(&pod_ciphertext.0).ok_or(ProofError::CiphertextDeserialization)
}
}
/// The `GroupedElGamalCiphertext` type with three decryption handles as a `Pod`
#[derive(Clone, Copy, Pod, Zeroable, PartialEq, Eq)]
#[repr(transparent)]
pub struct GroupedElGamalCiphertext3Handles(pub [u8; 128]);
impl fmt::Debug for GroupedElGamalCiphertext3Handles {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl Default for GroupedElGamalCiphertext3Handles {
fn default() -> Self {
Self::zeroed()
}
}
#[cfg(not(target_os = "solana"))]
impl From<GroupedElGamalCiphertext<3>> for GroupedElGamalCiphertext2Handles {
fn from(decoded_ciphertext: GroupedElGamalCiphertext<3>) -> Self {
Self(decoded_ciphertext.to_bytes().try_into().unwrap())
}
}
#[cfg(not(target_os = "solana"))]
impl TryFrom<GroupedElGamalCiphertext3Handles> for GroupedElGamalCiphertext<3> {
type Error = ProofError;
fn try_from(pod_ciphertext: GroupedElGamalCiphertext3Handles) -> Result<Self, Self::Error> {
Self::from_bytes(&pod_ciphertext.0).ok_or(ProofError::CiphertextDeserialization)
}
}

View File

@ -1,5 +1,6 @@
mod auth_encryption;
mod elgamal;
mod grouped_elgamal;
mod instruction;
mod pedersen;
mod range_proof;
@ -14,6 +15,7 @@ pub use {
auth_encryption::AeCiphertext,
bytemuck::{Pod, Zeroable},
elgamal::{DecryptHandle, ElGamalCiphertext, ElGamalPubkey},
grouped_elgamal::{GroupedElGamalCiphertext2Handles, GroupedElGamalCiphertext3Handles},
instruction::{
FeeEncryption, FeeParameters, TransferAmountEncryption, TransferPubkeys,
TransferWithFeePubkeys,