From 65f8f43665ff7d11aed2c13f3a046486ece63cc8 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 1 Feb 2022 14:11:28 -0500 Subject: [PATCH] Zk instructions pass (#22851) * zk-token-sdk: re-organize transcript * zk-token-sdk: add pod ElGamal group encryption * zk-token-sdk: add transcript domain separators for sigma proofs * zk-token-sdk: clean up transfer tx decryption * zk-token-sdk: resolve encoding issues for transfer * zk-token-sdk: fix transfer test * zk-token-sdk: clean up transcript for close account and withdraw instructions * zk-token-sdk: add transfer with fee instruction * zk-token-sdk: add transfer with fee instruction * zk-token-sdk: add pod for cryptographic structs needed for fee * zk-token-sdk: add pod for fee sigma proof * zk-token-sdk: fix test for transfer with fee instruction * zk-token-sdk: add range proof verification for transfer with fee * zk-token-sdk: add transfer amount decryption for transfer-with-fee * zk-token-sdk: add proof generation error for instruction * zk-token-sdk: cargo fmt and clippy * zk-token-sdk: fix bpf build --- zk-token-sdk/src/encryption/elgamal.rs | 18 +- zk-token-sdk/src/encryption/pedersen.rs | 7 + zk-token-sdk/src/errors.rs | 6 +- zk-token-sdk/src/instruction/close_account.rs | 123 ++-- zk-token-sdk/src/instruction/mod.rs | 59 +- zk-token-sdk/src/instruction/transfer.rs | 603 +++++++-------- .../src/instruction/transfer_with_fee.rs | 690 ++++++++++++++++++ zk-token-sdk/src/instruction/withdraw.rs | 146 ++-- zk-token-sdk/src/lib.rs | 3 +- zk-token-sdk/src/range_proof/mod.rs | 18 +- .../src/sigma_proofs/equality_proof.rs | 4 + zk-token-sdk/src/sigma_proofs/errors.rs | 4 +- zk-token-sdk/src/sigma_proofs/fee_proof.rs | 114 ++- .../src/sigma_proofs/validity_proof.rs | 10 +- zk-token-sdk/src/transcript.rs | 72 +- zk-token-sdk/src/zk_token_elgamal/convert.rs | 129 +++- zk-token-sdk/src/zk_token_elgamal/pod.rs | 45 ++ 17 files changed, 1514 insertions(+), 537 deletions(-) create mode 100644 zk-token-sdk/src/instruction/transfer_with_fee.rs diff --git a/zk-token-sdk/src/encryption/elgamal.rs b/zk-token-sdk/src/encryption/elgamal.rs index a233c7a2d0..1cf02a6d25 100644 --- a/zk-token-sdk/src/encryption/elgamal.rs +++ b/zk-token-sdk/src/encryption/elgamal.rs @@ -23,6 +23,7 @@ use { curve25519_dalek::{ ristretto::{CompressedRistretto, RistrettoPoint}, scalar::Scalar, + traits::Identity, }, serde::{Deserialize, Serialize}, solana_sdk::{ @@ -49,7 +50,7 @@ use { }; /// Algorithm handle for the twisted ElGamal encryption scheme -struct ElGamal; +pub struct ElGamal; impl ElGamal { /// Generates an ElGamal keypair. /// @@ -93,6 +94,7 @@ impl ElGamal { /// On input a public key, message, and Pedersen opening, the function /// returns the corresponding ElGamal ciphertext. + #[cfg(not(target_arch = "bpf"))] fn encrypt_with>( amount: T, public: &ElGamalPubkey, @@ -104,10 +106,22 @@ impl ElGamal { ElGamalCiphertext { commitment, handle } } + /// On input a message, the function returns a twisted ElGamal ciphertext where the associated + /// Pedersen opening is always zero. Since the opening is zero, any twisted ElGamal ciphertext + /// of this form is a valid ciphertext under any ElGamal public key. + #[cfg(not(target_arch = "bpf"))] + pub fn encode>(amount: T) -> ElGamalCiphertext { + let commitment = Pedersen::encode(amount); + let handle = DecryptHandle(RistrettoPoint::identity()); + + ElGamalCiphertext { commitment, handle } + } + /// On input a secret key and a ciphertext, the function returns the decrypted message. /// /// The output of this function is of type `DiscreteLog`. To recover, the originally encrypted /// message, use `DiscreteLog::decode`. + #[cfg(not(target_arch = "bpf"))] fn decrypt(secret: &ElGamalSecretKey, ciphertext: &ElGamalCiphertext) -> DiscreteLog { DiscreteLog { generator: *G, @@ -117,6 +131,7 @@ impl ElGamal { /// On input a secret key and a ciphertext, the function returns the decrypted message /// interpretted as type `u32`. + #[cfg(not(target_arch = "bpf"))] fn decrypt_u32(secret: &ElGamalSecretKey, ciphertext: &ElGamalCiphertext) -> Option { let discrete_log_instance = Self::decrypt(secret, ciphertext); discrete_log_instance.decode_u32() @@ -124,6 +139,7 @@ impl ElGamal { /// On input a secret key, a ciphertext, and a pre-computed hashmap, the function returns the /// decrypted message interpretted as type `u32`. + #[cfg(not(target_arch = "bpf"))] fn decrypt_u32_online( secret: &ElGamalSecretKey, ciphertext: &ElGamalCiphertext, diff --git a/zk-token-sdk/src/encryption/pedersen.rs b/zk-token-sdk/src/encryption/pedersen.rs index 8f80b5c6a8..694a69e458 100644 --- a/zk-token-sdk/src/encryption/pedersen.rs +++ b/zk-token-sdk/src/encryption/pedersen.rs @@ -52,6 +52,13 @@ impl Pedersen { PedersenCommitment(RistrettoPoint::multiscalar_mul(&[x, *r], &[*G, *H])) } + + /// On input a message, the function returns a Pedersen commitment with zero as the opening. + /// + /// This function is deterministic. + pub fn encode>(amount: T) -> PedersenCommitment { + PedersenCommitment(amount.into() * &(*G)) + } } /// Pedersen opening type. diff --git a/zk-token-sdk/src/errors.rs b/zk-token-sdk/src/errors.rs index 0e8e81d4d7..6e1f1c56ba 100644 --- a/zk-token-sdk/src/errors.rs +++ b/zk-token-sdk/src/errors.rs @@ -5,6 +5,8 @@ use thiserror::Error; // TODO: clean up errors for encryption #[derive(Error, Clone, Debug, Eq, PartialEq)] pub enum ProofError { + #[error("proof generation failed")] + Generation, #[error("proof failed to verify")] Verification, #[error("range proof failed to verify")] @@ -41,8 +43,8 @@ impl From for ProofError { } } -impl From for ProofError { - fn from(_err: FeeProofError) -> Self { +impl From for ProofError { + fn from(_err: FeeSigmaProofError) -> Self { Self::FeeProof } } diff --git a/zk-token-sdk/src/instruction/close_account.rs b/zk-token-sdk/src/instruction/close_account.rs index 9c285e4ea5..0c1e9302aa 100644 --- a/zk-token-sdk/src/instruction/close_account.rs +++ b/zk-token-sdk/src/instruction/close_account.rs @@ -26,10 +26,10 @@ use { #[repr(C)] pub struct CloseAccountData { /// The source account ElGamal pubkey - pub elgamal_pubkey: pod::ElGamalPubkey, // 32 bytes + pub pubkey: pod::ElGamalPubkey, // 32 bytes /// The source account available balance in encrypted form - pub balance: pod::ElGamalCiphertext, // 64 bytes + pub ciphertext: pod::ElGamalCiphertext, // 64 bytes /// Proof that the source account available balance is zero pub proof: CloseAccountProof, // 64 bytes @@ -37,23 +37,33 @@ pub struct CloseAccountData { #[cfg(not(target_arch = "bpf"))] impl CloseAccountData { - pub fn new(source_keypair: &ElGamalKeypair, balance: ElGamalCiphertext) -> Self { - let proof = CloseAccountProof::new(source_keypair, &balance); + pub fn new( + keypair: &ElGamalKeypair, + ciphertext: &ElGamalCiphertext, + ) -> Result { + let pod_pubkey = pod::ElGamalPubkey((&keypair.public).to_bytes()); + let pod_ciphertext = pod::ElGamalCiphertext(ciphertext.to_bytes()); - CloseAccountData { - elgamal_pubkey: source_keypair.public.into(), - balance: balance.into(), + let mut transcript = CloseAccountProof::transcript_new(&pod_pubkey, &pod_ciphertext); + + let proof = CloseAccountProof::new(keypair, ciphertext, &mut transcript); + + Ok(CloseAccountData { + pubkey: pod_pubkey, + ciphertext: pod_ciphertext, proof, - } + }) } } #[cfg(not(target_arch = "bpf"))] impl Verifiable for CloseAccountData { fn verify(&self) -> Result<(), ProofError> { - let elgamal_pubkey = self.elgamal_pubkey.try_into()?; - let balance = self.balance.try_into()?; - self.proof.verify(&elgamal_pubkey, &balance) + let mut transcript = CloseAccountProof::transcript_new(&self.pubkey, &self.ciphertext); + + let pubkey = self.pubkey.try_into()?; + let ciphertext = self.ciphertext.try_into()?; + self.proof.verify(&pubkey, &ciphertext, &mut transcript) } } @@ -69,18 +79,24 @@ pub struct CloseAccountProof { #[allow(non_snake_case)] #[cfg(not(target_arch = "bpf"))] impl CloseAccountProof { - fn transcript_new() -> Transcript { - Transcript::new(b"CloseAccountProof") + fn transcript_new( + pubkey: &pod::ElGamalPubkey, + ciphertext: &pod::ElGamalCiphertext, + ) -> Transcript { + let mut transcript = Transcript::new(b"CloseAccountProof"); + + transcript.append_pubkey(b"pubkey", pubkey); + transcript.append_ciphertext(b"ciphertext", ciphertext); + + transcript } - pub fn new(source_keypair: &ElGamalKeypair, balance: &ElGamalCiphertext) -> Self { - let mut transcript = Self::transcript_new(); - // TODO: Add ciphertext to transcript - - // add a domain separator to record the start of the protocol - transcript.close_account_proof_domain_sep(); - - let proof = ZeroBalanceProof::new(source_keypair, balance, &mut transcript); + pub fn new( + keypair: &ElGamalKeypair, + ciphertext: &ElGamalCiphertext, + transcript: &mut Transcript, + ) -> Self { + let proof = ZeroBalanceProof::new(keypair, ciphertext, transcript); CloseAccountProof { proof: proof.into(), @@ -89,72 +105,33 @@ impl CloseAccountProof { pub fn verify( &self, - elgamal_pubkey: &ElGamalPubkey, - balance: &ElGamalCiphertext, + pubkey: &ElGamalPubkey, + ciphertext: &ElGamalCiphertext, + transcript: &mut Transcript, ) -> Result<(), ProofError> { - let mut transcript = Self::transcript_new(); - - // add a domain separator to record the start of the protocol - transcript.close_account_proof_domain_sep(); - - // verify zero balance proof let proof: ZeroBalanceProof = self.proof.try_into()?; - proof.verify(elgamal_pubkey, balance, &mut transcript)?; + proof.verify(pubkey, ciphertext, transcript)?; + Ok(()) } } #[cfg(test)] mod test { - use { - super::*, - crate::encryption::{ - elgamal::{DecryptHandle, ElGamalKeypair}, - pedersen::{Pedersen, PedersenOpening}, - }, - }; + use super::*; #[test] fn test_close_account_correctness() { - let source_keypair = ElGamalKeypair::new_rand(); + let keypair = ElGamalKeypair::new_rand(); // general case: encryption of 0 - let balance = source_keypair.public.encrypt(0_u64); - let proof = CloseAccountProof::new(&source_keypair, &balance); - assert!(proof.verify(&source_keypair.public, &balance).is_ok()); + let ciphertext = keypair.public.encrypt(0_u64); + let close_account_data = CloseAccountData::new(&keypair, &ciphertext).unwrap(); + assert!(close_account_data.verify().is_ok()); // general case: encryption of > 0 - let balance = source_keypair.public.encrypt(1_u64); - let proof = CloseAccountProof::new(&source_keypair, &balance); - assert!(proof.verify(&source_keypair.public, &balance).is_err()); - - // // edge case: all zero ciphertext - such ciphertext should always be a valid encryption of 0 - let zeroed_ct: ElGamalCiphertext = pod::ElGamalCiphertext::zeroed().try_into().unwrap(); - let proof = CloseAccountProof::new(&source_keypair, &zeroed_ct); - assert!(proof.verify(&source_keypair.public, &zeroed_ct).is_ok()); - - // edge cases: only C or D is zero - such ciphertext is always invalid - let zeroed_comm = Pedersen::with(0_u64, &PedersenOpening::default()); - let handle = balance.handle; - - let zeroed_comm_ciphertext = ElGamalCiphertext { - commitment: zeroed_comm, - handle, - }; - - let proof = CloseAccountProof::new(&source_keypair, &zeroed_comm_ciphertext); - assert!(proof - .verify(&source_keypair.public, &zeroed_comm_ciphertext) - .is_err()); - - let zeroed_handle_ciphertext = ElGamalCiphertext { - commitment: balance.commitment, - handle: DecryptHandle::default(), - }; - - let proof = CloseAccountProof::new(&source_keypair, &zeroed_handle_ciphertext); - assert!(proof - .verify(&source_keypair.public, &zeroed_handle_ciphertext) - .is_err()); + let ciphertext = keypair.public.encrypt(1_u64); + let close_account_data = CloseAccountData::new(&keypair, &ciphertext).unwrap(); + assert!(close_account_data.verify().is_err()); } } diff --git a/zk-token-sdk/src/instruction/mod.rs b/zk-token-sdk/src/instruction/mod.rs index 0e25a917f8..49b0f5f1b4 100644 --- a/zk-token-sdk/src/instruction/mod.rs +++ b/zk-token-sdk/src/instruction/mod.rs @@ -1,14 +1,24 @@ -mod close_account; -mod transfer; -mod withdraw; +pub mod close_account; +pub mod transfer; +pub mod transfer_with_fee; +pub mod withdraw; #[cfg(not(target_arch = "bpf"))] -use crate::errors::ProofError; -pub use { - close_account::CloseAccountData, - transfer::{TransferCommitments, TransferData, TransferPubkeys}, - withdraw::WithdrawData, +use { + crate::{ + encryption::{ + elgamal::ElGamalCiphertext, + pedersen::{PedersenCommitment, PedersenOpening}, + }, + errors::ProofError, + }, + curve25519_dalek::scalar::Scalar, }; +pub use {close_account::CloseAccountData, transfer::TransferData, withdraw::WithdrawData}; + +/// Constant for 2^32 +#[cfg(not(target_arch = "bpf"))] +const TWO_32: u64 = 4294967296; #[cfg(not(target_arch = "bpf"))] pub trait Verifiable { @@ -22,3 +32,36 @@ pub enum Role { Dest, Auditor, } + +/// Split u64 number into two u32 numbers +#[cfg(not(target_arch = "bpf"))] +pub fn split_u64_into_u32(amount: u64) -> (u32, u32) { + let lo = amount as u32; + let hi = (amount >> 32) as u32; + + (lo, hi) +} + +#[cfg(not(target_arch = "bpf"))] +fn combine_u32_ciphertexts( + ciphertext_lo: &ElGamalCiphertext, + ciphertext_hi: &ElGamalCiphertext, +) -> ElGamalCiphertext { + ciphertext_lo + &(ciphertext_hi * &Scalar::from(TWO_32)) +} + +#[cfg(not(target_arch = "bpf"))] +pub fn combine_u32_commitments( + comm_lo: &PedersenCommitment, + comm_hi: &PedersenCommitment, +) -> PedersenCommitment { + comm_lo + comm_hi * &Scalar::from(TWO_32) +} + +#[cfg(not(target_arch = "bpf"))] +pub fn combine_u32_openings( + opening_lo: &PedersenOpening, + opening_hi: &PedersenOpening, +) -> PedersenOpening { + opening_lo + opening_hi * &Scalar::from(TWO_32) +} diff --git a/zk-token-sdk/src/instruction/transfer.rs b/zk-token-sdk/src/instruction/transfer.rs index bda43292c9..832085ebdd 100644 --- a/zk-token-sdk/src/instruction/transfer.rs +++ b/zk-token-sdk/src/instruction/transfer.rs @@ -13,188 +13,197 @@ use { pedersen::{Pedersen, PedersenCommitment, PedersenOpening}, }, errors::ProofError, - instruction::{Role, Verifiable}, + instruction::{combine_u32_ciphertexts, split_u64_into_u32, Role, Verifiable, TWO_32}, range_proof::RangeProof, sigma_proofs::{equality_proof::EqualityProof, validity_proof::AggregatedValidityProof}, transcript::TranscriptProtocol, }, - curve25519_dalek::scalar::Scalar, + arrayref::{array_ref, array_refs}, merlin::Transcript, std::convert::TryInto, }; +#[derive(Clone)] +#[repr(C)] +#[cfg(not(target_arch = "bpf"))] +pub struct TransferAmountEncryption { + pub commitment: PedersenCommitment, + pub source: DecryptHandle, + pub dest: DecryptHandle, + pub auditor: DecryptHandle, +} + +#[cfg(not(target_arch = "bpf"))] +impl TransferAmountEncryption { + pub fn new( + amount: u32, + pubkey_source: &ElGamalPubkey, + pubkey_dest: &ElGamalPubkey, + pubkey_auditor: &ElGamalPubkey, + ) -> (Self, PedersenOpening) { + let (commitment, opening) = Pedersen::new(amount); + let transfer_amount_encryption = Self { + commitment, + source: pubkey_source.decrypt_handle(&opening), + dest: pubkey_dest.decrypt_handle(&opening), + auditor: pubkey_auditor.decrypt_handle(&opening), + }; + + (transfer_amount_encryption, opening) + } + + pub fn to_bytes(&self) -> [u8; 128] { + let mut bytes = [0u8; 128]; + bytes[..32].copy_from_slice(&self.commitment.to_bytes()); + bytes[32..64].copy_from_slice(&self.source.to_bytes()); + bytes[64..96].copy_from_slice(&self.dest.to_bytes()); + bytes[96..128].copy_from_slice(&self.auditor.to_bytes()); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let bytes = array_ref![bytes, 0, 128]; + let (commitment, source, dest, auditor) = array_refs![bytes, 32, 32, 32, 32]; + + let commitment = + PedersenCommitment::from_bytes(commitment).ok_or(ProofError::Verification)?; + let source = DecryptHandle::from_bytes(source).ok_or(ProofError::Verification)?; + let dest = DecryptHandle::from_bytes(dest).ok_or(ProofError::Verification)?; + let auditor = DecryptHandle::from_bytes(auditor).ok_or(ProofError::Verification)?; + + Ok(Self { + commitment, + source, + dest, + auditor, + }) + } +} + #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct TransferData { - /// The encrypted transfer amount - pub encrypted_transfer_amount: EncryptedTransferAmount, + /// Group encryption of the low 32 bits of the transfer amount + pub ciphertext_lo: pod::TransferAmountEncryption, + + /// Group encryption of the high 32 bits of the transfer amount + pub ciphertext_hi: pod::TransferAmountEncryption, /// The public encryption keys associated with the transfer: source, dest, and auditor - pub transfer_public_keys: TransferPubkeys, // 128 bytes + pub transfer_pubkeys: pod::TransferPubkeys, /// The final spendable ciphertext after the transfer - pub new_spendable_ct: pod::ElGamalCiphertext, // 64 bytes + pub ciphertext_new_source: pod::ElGamalCiphertext, - // pub fee: EncryptedTransferFee, /// Zero-knowledge proofs for Transfer pub proof: TransferProof, } -#[derive(Clone, Copy)] -#[repr(C)] -pub struct FeeParameters { - /// Fee rate expressed as basis points of the transfer amount, i.e. increments of 0.01% - pub fee_rate_basis_points: u16, - /// Maximum fee assessed on transfers, expressed as an amount of tokens - pub maximum_fee: u64, -} - -#[allow(dead_code)] -fn calculate_fee(transfer_amount: u64, fee_parameters: FeeParameters) -> u64 { - // TODO: temporary way to calculate fees for now. Should account for overflows/compiler - // optimizations - let fee = (transfer_amount * (fee_parameters.fee_rate_basis_points as u64)) / 10000; - if fee % 10000 > 0 { - fee + 1 - } else { - fee - } -} - #[cfg(not(target_arch = "bpf"))] impl TransferData { #[allow(clippy::too_many_arguments)] pub fn new( - // amount of the transfer transfer_amount: u64, - - // available balance in the source account as u64 - spendable_balance: u64, - - // available balance in the source account as ElGamalCiphertext - spendable_balance_ciphertext: ElGamalCiphertext, - - // source account ElGamal keypair - source_keypair: &ElGamalKeypair, - - // destination account ElGamal pubkey - dest_pk: ElGamalPubkey, - - // auditor ElGamal pubkey - auditor_pk: ElGamalPubkey, - // // fee collector ElGamal pubkey - // fee_collector_pk: ElGamalPubkey, - - // // fee rate and cap value - // fee_parameters: FeeParameters, - ) -> Self { + (spendable_balance, ciphertext_old_source): (u64, &ElGamalCiphertext), + keypair_source: &ElGamalKeypair, + (pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey), + ) -> Result { // split and encrypt transfer amount let (amount_lo, amount_hi) = split_u64_into_u32(transfer_amount); - let (comm_lo, open_lo) = Pedersen::new(amount_lo); - let (comm_hi, open_hi) = Pedersen::new(amount_hi); - - let handle_source_lo = source_keypair.public.decrypt_handle(&open_lo); - let handle_dest_lo = dest_pk.decrypt_handle(&open_lo); - let handle_auditor_lo = auditor_pk.decrypt_handle(&open_lo); - - let handle_source_hi = source_keypair.public.decrypt_handle(&open_hi); - let handle_dest_hi = dest_pk.decrypt_handle(&open_hi); - let handle_auditor_hi = auditor_pk.decrypt_handle(&open_hi); - - // organize transfer amount commitments and decrypt handles - let decrypt_handles_lo = TransferDecryptHandles { - source: handle_source_lo.into(), - dest: handle_dest_lo.into(), - auditor: handle_auditor_lo.into(), - }; - - let decrypt_handles_hi = TransferDecryptHandles { - source: handle_source_hi.into(), - dest: handle_dest_hi.into(), - auditor: handle_auditor_hi.into(), - }; - - let encrypted_transfer_amount = EncryptedTransferAmount { - amount_comm_lo: comm_lo.into(), - amount_comm_hi: comm_hi.into(), - decrypt_handles_lo, - decrypt_handles_hi, - }; - - // group public keys for transfer - let transfer_public_keys = TransferPubkeys { - source_pk: source_keypair.public.into(), - dest_pk: dest_pk.into(), - auditor_pk: auditor_pk.into(), - }; - - // subtract transfer amount from the spendable ciphertext - let spendable_comm = spendable_balance_ciphertext.commitment; - let spendable_handle = spendable_balance_ciphertext.handle; - - let new_spendable_balance = spendable_balance - transfer_amount; - let new_spendable_comm = spendable_comm - combine_u32_comms(comm_lo, comm_hi); - let new_spendable_handle = - spendable_handle - combine_u32_handles(handle_source_lo, handle_source_hi); - - let new_spendable_ct = ElGamalCiphertext { - commitment: new_spendable_comm, - handle: new_spendable_handle, - }; - - // range_proof and validity_proof should be generated together - let proof = TransferProof::new( - source_keypair, - &dest_pk, - &auditor_pk, - (amount_lo as u64, amount_hi as u64), - (&open_lo, &open_hi), - new_spendable_balance, - &new_spendable_ct, + let (ciphertext_lo, opening_lo) = TransferAmountEncryption::new( + amount_lo, + &keypair_source.public, + pubkey_dest, + pubkey_auditor, + ); + let (ciphertext_hi, opening_hi) = TransferAmountEncryption::new( + amount_hi, + &keypair_source.public, + pubkey_dest, + pubkey_auditor, ); - Self { - encrypted_transfer_amount, - new_spendable_ct: new_spendable_ct.into(), - transfer_public_keys, + // subtract transfer amount from the spendable ciphertext + let new_spendable_balance = spendable_balance + .checked_sub(transfer_amount) + .ok_or(ProofError::Generation)?; + + let transfer_amount_lo_source = ElGamalCiphertext { + commitment: ciphertext_lo.commitment, + handle: ciphertext_lo.source, + }; + + let transfer_amount_hi_source = ElGamalCiphertext { + commitment: ciphertext_hi.commitment, + handle: ciphertext_hi.source, + }; + + let ciphertext_new_source = ciphertext_old_source + - combine_u32_ciphertexts(&transfer_amount_lo_source, &transfer_amount_hi_source); + + // generate transcript and append all public inputs + let pod_transfer_pubkeys = + pod::TransferPubkeys::new(&keypair_source.public, pubkey_dest, pubkey_auditor); + let pod_ciphertext_lo: pod::TransferAmountEncryption = ciphertext_lo.into(); + let pod_ciphertext_hi: pod::TransferAmountEncryption = ciphertext_hi.into(); + let pod_ciphertext_new_source: pod::ElGamalCiphertext = ciphertext_new_source.into(); + + let mut transcript = TransferProof::transcript_new( + &pod_transfer_pubkeys, + &pod_ciphertext_lo, + &pod_ciphertext_hi, + &pod_ciphertext_new_source, + ); + + let proof = TransferProof::new( + (amount_lo, amount_hi), + keypair_source, + (pubkey_dest, pubkey_auditor), + &opening_lo, + &opening_hi, + (new_spendable_balance, &ciphertext_new_source), + &mut transcript, + ); + + Ok(Self { + ciphertext_lo: pod_ciphertext_lo, + ciphertext_hi: pod_ciphertext_hi, + transfer_pubkeys: pod_transfer_pubkeys, + ciphertext_new_source: pod_ciphertext_new_source, proof, - } + }) } /// Extracts the lo ciphertexts associated with a transfer data fn ciphertext_lo(&self, role: Role) -> Result { - let transfer_comm_lo: PedersenCommitment = - self.encrypted_transfer_amount.amount_comm_lo.try_into()?; + let ciphertext_lo: TransferAmountEncryption = self.ciphertext_lo.try_into()?; - let decryption_handle_lo = match role { - Role::Source => self.encrypted_transfer_amount.decrypt_handles_lo.source, - Role::Dest => self.encrypted_transfer_amount.decrypt_handles_lo.dest, - Role::Auditor => self.encrypted_transfer_amount.decrypt_handles_lo.auditor, - } - .try_into()?; + let handle_lo = match role { + Role::Source => ciphertext_lo.source, + Role::Dest => ciphertext_lo.dest, + Role::Auditor => ciphertext_lo.auditor, + }; Ok(ElGamalCiphertext { - commitment: transfer_comm_lo, - handle: decryption_handle_lo, + commitment: ciphertext_lo.commitment, + handle: handle_lo, }) } /// Extracts the lo ciphertexts associated with a transfer data fn ciphertext_hi(&self, role: Role) -> Result { - let transfer_comm_hi: PedersenCommitment = - self.encrypted_transfer_amount.amount_comm_hi.try_into()?; + let ciphertext_hi: TransferAmountEncryption = self.ciphertext_hi.try_into()?; - let decryption_handle_hi = match role { - Role::Source => self.encrypted_transfer_amount.decrypt_handles_hi.source, - Role::Dest => self.encrypted_transfer_amount.decrypt_handles_hi.dest, - Role::Auditor => self.encrypted_transfer_amount.decrypt_handles_hi.auditor, - } - .try_into()?; + let handle_hi = match role { + Role::Source => ciphertext_hi.source, + Role::Dest => ciphertext_hi.dest, + Role::Auditor => ciphertext_hi.auditor, + }; Ok(ElGamalCiphertext { - commitment: transfer_comm_hi, - handle: decryption_handle_hi, + commitment: ciphertext_hi.commitment, + handle: handle_hi, }) } @@ -222,17 +231,25 @@ impl TransferData { #[cfg(not(target_arch = "bpf"))] impl Verifiable for TransferData { fn verify(&self) -> Result<(), ProofError> { - let transfer_commitments = TransferCommitments { - lo: self.encrypted_transfer_amount.amount_comm_lo, - hi: self.encrypted_transfer_amount.amount_comm_hi, - }; + // generate transcript and append all public inputs + let mut transcript = TransferProof::transcript_new( + &self.transfer_pubkeys, + &self.ciphertext_lo, + &self.ciphertext_hi, + &self.ciphertext_new_source, + ); + + let ciphertext_lo = self.ciphertext_lo.try_into()?; + let ciphertext_hi = self.ciphertext_hi.try_into()?; + let transfer_pubkeys = self.transfer_pubkeys.try_into()?; + let new_spendable_ciphertext = self.ciphertext_new_source.try_into()?; self.proof.verify( - &transfer_commitments, - &self.encrypted_transfer_amount.decrypt_handles_lo, - &self.encrypted_transfer_amount.decrypt_handles_hi, - &self.new_spendable_ct, - &self.transfer_public_keys, + &ciphertext_lo, + &ciphertext_hi, + &transfer_pubkeys, + &new_spendable_ciphertext, + &mut transcript, ) } } @@ -242,7 +259,7 @@ impl Verifiable for TransferData { #[repr(C)] pub struct TransferProof { /// New Pedersen commitment for the remaining balance in source - pub source_commitment: pod::PedersenCommitment, + pub commitment_new_source: pod::PedersenCommitment, /// Associated equality proof pub equality_proof: pod::EqualityProof, @@ -257,143 +274,118 @@ pub struct TransferProof { #[allow(non_snake_case)] #[cfg(not(target_arch = "bpf"))] impl TransferProof { - fn transcript_new() -> Transcript { - Transcript::new(b"TransferProof") + fn transcript_new( + transfer_pubkeys: &pod::TransferPubkeys, + ciphertext_lo: &pod::TransferAmountEncryption, + ciphertext_hi: &pod::TransferAmountEncryption, + ciphertext_new_source: &pod::ElGamalCiphertext, + ) -> Transcript { + let mut transcript = Transcript::new(b"transfer-proof"); + + transcript.append_message(b"transfer-pubkeys", &transfer_pubkeys.0); + transcript.append_message(b"ciphertext-lo", &ciphertext_lo.0); + transcript.append_message(b"ciphertext-hi", &ciphertext_hi.0); + transcript.append_message(b"ciphertext-new-source", &ciphertext_new_source.0); + + transcript } - #[allow(clippy::too_many_arguments)] - #[allow(clippy::many_single_char_names)] pub fn new( - source_keypair: &ElGamalKeypair, - dest_pk: &ElGamalPubkey, - auditor_pk: &ElGamalPubkey, - transfer_amt: (u64, u64), - openings: (&PedersenOpening, &PedersenOpening), - source_new_balance: u64, - source_new_balance_ct: &ElGamalCiphertext, + (transfer_amount_lo, transfer_amount_hi): (u32, u32), + keypair_source: &ElGamalKeypair, + (pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey), + opening_lo: &PedersenOpening, + opening_hi: &PedersenOpening, + (source_new_balance, ciphertext_new_source): (u64, &ElGamalCiphertext), + transcript: &mut Transcript, ) -> Self { - let mut transcript = Self::transcript_new(); - - // add a domain separator to record the start of the protocol - transcript.transfer_proof_domain_sep(); - // generate a Pedersen commitment for the remaining balance in source - let (source_commitment, source_open) = Pedersen::new(source_new_balance); + let (commitment_new_source, opening_source) = Pedersen::new(source_new_balance); - // extract the relevant scalar and Ristretto points from the inputs - let P_EG = source_keypair.public.get_point(); - let C_EG = source_new_balance_ct.commitment.get_point(); - let D_EG = source_new_balance_ct.handle.get_point(); - let C_Ped = source_commitment.get_point(); - - // append all current state to the transcript - transcript.append_point(b"P_EG", &P_EG.compress()); - transcript.append_point(b"C_EG", &C_EG.compress()); - transcript.append_point(b"D_EG", &D_EG.compress()); - transcript.append_point(b"C_Ped", &C_Ped.compress()); + let pod_commitment_new_source: pod::PedersenCommitment = commitment_new_source.into(); + transcript.append_commitment(b"commitment-new-source", &pod_commitment_new_source); // generate equality_proof let equality_proof = EqualityProof::new( - source_keypair, - source_new_balance_ct, + keypair_source, + ciphertext_new_source, source_new_balance, - &source_open, - &mut transcript, + &opening_source, + transcript, ); // generate ciphertext validity proof let validity_proof = AggregatedValidityProof::new( - (dest_pk, auditor_pk), - transfer_amt, - openings, - &mut transcript, + (pubkey_dest, pubkey_auditor), + (transfer_amount_lo, transfer_amount_hi), + (opening_lo, opening_hi), + transcript, ); // generate the range proof let range_proof = RangeProof::new( - vec![source_new_balance, transfer_amt.0, transfer_amt.1], + vec![ + source_new_balance, + transfer_amount_lo as u64, + transfer_amount_hi as u64, + ], vec![64, 32, 32], - vec![&source_open, openings.0, openings.1], - &mut transcript, + vec![&opening_source, opening_lo, opening_hi], + transcript, ); Self { - source_commitment: source_commitment.into(), - equality_proof: equality_proof.try_into().expect("equality proof"), - validity_proof: validity_proof.try_into().expect("validity proof"), - range_proof: range_proof.try_into().expect("range proof"), + commitment_new_source: pod_commitment_new_source, + equality_proof: equality_proof.into(), + validity_proof: validity_proof.into(), + range_proof: range_proof.try_into().expect("range proof: length error"), } } pub fn verify( - self, - amount_comms: &TransferCommitments, - decryption_handles_lo: &TransferDecryptHandles, - decryption_handles_hi: &TransferDecryptHandles, - new_spendable_ct: &pod::ElGamalCiphertext, - transfer_public_keys: &TransferPubkeys, + &self, + ciphertext_lo: &TransferAmountEncryption, + ciphertext_hi: &TransferAmountEncryption, + transfer_pubkeys: &TransferPubkeys, + new_spendable_ciphertext: &ElGamalCiphertext, + transcript: &mut Transcript, ) -> Result<(), ProofError> { - let mut transcript = Self::transcript_new(); + transcript.append_commitment(b"commitment-new-source", &self.commitment_new_source); - let commitment: PedersenCommitment = self.source_commitment.try_into()?; + let commitment: PedersenCommitment = self.commitment_new_source.try_into()?; let equality_proof: EqualityProof = self.equality_proof.try_into()?; let aggregated_validity_proof: AggregatedValidityProof = self.validity_proof.try_into()?; let range_proof: RangeProof = self.range_proof.try_into()?; - // add a domain separator to record the start of the protocol - transcript.transfer_proof_domain_sep(); - - // extract the relevant scalar and Ristretto points from the inputs - let source_pk: ElGamalPubkey = transfer_public_keys.source_pk.try_into()?; - let new_spendable_ct: ElGamalCiphertext = (*new_spendable_ct).try_into()?; - - let P_EG = source_pk.get_point(); - let C_EG = new_spendable_ct.commitment.get_point(); - let D_EG = new_spendable_ct.handle.get_point(); - let C_Ped = commitment.get_point(); - - // append all current state to the transcript - transcript.append_point(b"P_EG", &P_EG.compress()); - transcript.append_point(b"C_EG", &C_EG.compress()); - transcript.append_point(b"D_EG", &D_EG.compress()); - transcript.append_point(b"C_Ped", &C_Ped.compress()); - // verify equality proof // // TODO: we can also consider verifying equality and range proof in a batch - equality_proof.verify(&source_pk, &new_spendable_ct, &commitment, &mut transcript)?; + equality_proof.verify( + &transfer_pubkeys.source, + new_spendable_ciphertext, + &commitment, + transcript, + )?; - // TODO: record destination and auditor public keys to transcript - let dest_elgamal_pubkey: ElGamalPubkey = transfer_public_keys.dest_pk.try_into()?; - let auditor_elgamal_pubkey: ElGamalPubkey = transfer_public_keys.auditor_pk.try_into()?; - - let amount_comm_lo: PedersenCommitment = amount_comms.lo.try_into()?; - let amount_comm_hi: PedersenCommitment = amount_comms.hi.try_into()?; - - let handle_lo_dest: DecryptHandle = decryption_handles_lo.dest.try_into()?; - let handle_hi_dest: DecryptHandle = decryption_handles_hi.dest.try_into()?; - - let handle_lo_auditor: DecryptHandle = decryption_handles_lo.auditor.try_into()?; - let handle_hi_auditor: DecryptHandle = decryption_handles_hi.auditor.try_into()?; - - // TODO: validity proof + // verify validity proof aggregated_validity_proof.verify( - (&dest_elgamal_pubkey, &auditor_elgamal_pubkey), - (&amount_comm_lo, &amount_comm_hi), - (&handle_lo_dest, &handle_hi_dest), - (&handle_lo_auditor, &handle_hi_auditor), - &mut transcript, + (&transfer_pubkeys.dest, &transfer_pubkeys.auditor), + (&ciphertext_lo.commitment, &ciphertext_hi.commitment), + (&ciphertext_lo.dest, &ciphertext_hi.dest), + (&ciphertext_lo.auditor, &ciphertext_hi.auditor), + transcript, )?; // verify range proof + let commitment_new_source = self.commitment_new_source.try_into()?; range_proof.verify( vec![ - &self.source_commitment.into(), - &amount_comms.lo.into(), - &amount_comms.hi.into(), + &commitment_new_source, + &ciphertext_lo.commitment, + &ciphertext_hi.commitment, ], vec![64_usize, 32_usize, 32_usize], - &mut transcript, + transcript, )?; Ok(()) @@ -401,85 +393,52 @@ impl TransferProof { } /// The ElGamal public keys needed for a transfer -#[derive(Clone, Copy, Pod, Zeroable)] +#[derive(Clone)] #[repr(C)] +#[cfg(not(target_arch = "bpf"))] pub struct TransferPubkeys { - pub source_pk: pod::ElGamalPubkey, // 32 bytes - pub dest_pk: pod::ElGamalPubkey, // 32 bytes - pub auditor_pk: pod::ElGamalPubkey, // 32 bytes -} - -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct EncryptedTransferAmount { - pub amount_comm_lo: pod::PedersenCommitment, - - pub amount_comm_hi: pod::PedersenCommitment, - - /// The decryption handles that allow decryption of the lo-bits of the transfer amount - pub decrypt_handles_lo: TransferDecryptHandles, - - /// The decryption handles that allow decryption of the hi-bits of the transfer amount - pub decrypt_handles_hi: TransferDecryptHandles, -} - -/// The decryption handles needed for a transfer -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct TransferDecryptHandles { - pub source: pod::DecryptHandle, // 32 bytes - pub dest: pod::DecryptHandle, // 32 bytes - pub auditor: pod::DecryptHandle, // 32 bytes -} - -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct TransferCommitments { - pub lo: pod::PedersenCommitment, - pub hi: pod::PedersenCommitment, -} - -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct EncryptedTransferFee { - /// The transfer fee commitment - pub fee_comm: pod::PedersenCommitment, - /// The decryption handle for destination ElGamal pubkey - pub decrypt_handle_dest: pod::DecryptHandle, - /// The decryption handle for fee collector ElGamal pubkey - pub decrypt_handle_fee_collector: pod::DecryptHandle, -} - -/// Split u64 number into two u32 numbers -#[cfg(not(target_arch = "bpf"))] -pub fn split_u64_into_u32(amt: u64) -> (u32, u32) { - let lo = amt as u32; - let hi = (amt >> 32) as u32; - - (lo, hi) -} - -/// Constant for 2^32 -#[cfg(not(target_arch = "bpf"))] -const TWO_32: u64 = 4294967296; - -#[cfg(not(target_arch = "bpf"))] -pub fn combine_u32_comms( - comm_lo: PedersenCommitment, - comm_hi: PedersenCommitment, -) -> PedersenCommitment { - comm_lo + comm_hi * Scalar::from(TWO_32) + pub source: ElGamalPubkey, + pub dest: ElGamalPubkey, + pub auditor: ElGamalPubkey, } #[cfg(not(target_arch = "bpf"))] -pub fn combine_u32_handles(handle_lo: DecryptHandle, handle_hi: DecryptHandle) -> DecryptHandle { - handle_lo + handle_hi * Scalar::from(TWO_32) +impl TransferPubkeys { + // TODO: use constructor instead + pub fn to_bytes(&self) -> [u8; 96] { + let mut bytes = [0u8; 96]; + bytes[..32].copy_from_slice(&self.source.to_bytes()); + bytes[32..64].copy_from_slice(&self.dest.to_bytes()); + bytes[64..96].copy_from_slice(&self.auditor.to_bytes()); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let bytes = array_ref![bytes, 0, 96]; + let (source, dest, auditor) = array_refs![bytes, 32, 32, 32]; + + let source = ElGamalPubkey::from_bytes(source).ok_or(ProofError::Verification)?; + let dest = ElGamalPubkey::from_bytes(dest).ok_or(ProofError::Verification)?; + let auditor = ElGamalPubkey::from_bytes(auditor).ok_or(ProofError::Verification)?; + + Ok(Self { + source, + dest, + auditor, + }) + } } -/* -pub fn combine_u32_ciphertexts(ct_lo: ElGamalCiphertext, ct_hi: ElGamalCiphertext) -> ElGamalCiphertext { - ct_lo + ct_hi * Scalar::from(TWO_32) -}*/ +#[cfg(not(target_arch = "bpf"))] +impl pod::TransferPubkeys { + pub fn new(source: &ElGamalPubkey, dest: &ElGamalPubkey, auditor: &ElGamalPubkey) -> Self { + let mut bytes = [0u8; 96]; + bytes[..32].copy_from_slice(&source.to_bytes()); + bytes[32..64].copy_from_slice(&dest.to_bytes()); + bytes[64..96].copy_from_slice(&auditor.to_bytes()); + Self(bytes) + } +} #[cfg(test)] mod test { @@ -494,7 +453,7 @@ mod test { // create source account spendable ciphertext let spendable_balance: u64 = 77; - let spendable_ct = source_keypair.public.encrypt(spendable_balance); + let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance); // transfer amount let transfer_amount: u64 = 55; @@ -502,12 +461,11 @@ mod test { // create transfer data let transfer_data = TransferData::new( transfer_amount, - spendable_balance, - spendable_ct, + (spendable_balance, &spendable_ciphertext), &source_keypair, - dest_pk, - auditor_pk, - ); + (&dest_pk, &auditor_pk), + ) + .unwrap(); assert!(transfer_data.verify().is_ok()); } @@ -529,7 +487,7 @@ mod test { // create source account spendable ciphertext let spendable_balance: u64 = 77; - let spendable_ct = source_keypair.public.encrypt(spendable_balance); + let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance); // transfer amount let transfer_amount: u64 = 55; @@ -537,12 +495,11 @@ mod test { // create transfer data let transfer_data = TransferData::new( transfer_amount, - spendable_balance, - spendable_ct, + (spendable_balance, &spendable_ciphertext), &source_keypair, - dest_pk, - auditor_pk, - ); + (&dest_pk, &auditor_pk), + ) + .unwrap(); assert_eq!( transfer_data diff --git a/zk-token-sdk/src/instruction/transfer_with_fee.rs b/zk-token-sdk/src/instruction/transfer_with_fee.rs new file mode 100644 index 0000000000..a04b78ed6a --- /dev/null +++ b/zk-token-sdk/src/instruction/transfer_with_fee.rs @@ -0,0 +1,690 @@ +use { + crate::zk_token_elgamal::pod, + bytemuck::{Pod, Zeroable}, +}; +#[cfg(not(target_arch = "bpf"))] +use { + crate::{ + encryption::{ + discrete_log::*, + elgamal::{ + DecryptHandle, ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey, + }, + pedersen::{Pedersen, PedersenCommitment, PedersenOpening}, + }, + errors::ProofError, + instruction::{ + combine_u32_ciphertexts, combine_u32_commitments, combine_u32_openings, + split_u64_into_u32, transfer::TransferAmountEncryption, Role, Verifiable, TWO_32, + }, + range_proof::RangeProof, + sigma_proofs::{ + equality_proof::EqualityProof, + fee_proof::FeeSigmaProof, + validity_proof::{AggregatedValidityProof, ValidityProof}, + }, + transcript::TranscriptProtocol, + }, + arrayref::{array_ref, array_refs}, + curve25519_dalek::scalar::Scalar, + merlin::Transcript, + std::convert::TryInto, + subtle::{ConditionallySelectable, ConstantTimeGreater}, +}; + +#[cfg(not(target_arch = "bpf"))] +const FEE_DENOMINATOR: u64 = 10000; + +#[cfg(not(target_arch = "bpf"))] +lazy_static::lazy_static! { + pub static ref COMMITMENT_FEE_DENOMINATOR: PedersenCommitment = Pedersen::encode(FEE_DENOMINATOR); +} + +// #[derive(Clone, Copy, Pod, Zeroable)] +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct TransferWithFeeData { + /// Group encryption of the low 32 bites of the transfer amount + pub ciphertext_lo: pod::TransferAmountEncryption, + + /// Group encryption of the high 32 bits of the transfer amount + pub ciphertext_hi: pod::TransferAmountEncryption, + + /// The public encryption keys associated with the transfer: source, dest, and auditor + pub transfer_with_fee_pubkeys: pod::TransferWithFeePubkeys, + + /// The final spendable ciphertext after the transfer, + pub ciphertext_new_source: pod::ElGamalCiphertext, + + // transfer fee encryption + pub ciphertext_fee: pod::FeeEncryption, + + // fee parameters + pub fee_parameters: pod::FeeParameters, + + // transfer fee proof + pub proof: TransferWithFeeProof, +} + +#[cfg(not(target_arch = "bpf"))] +impl TransferWithFeeData { + pub fn new( + transfer_amount: u64, + (spendable_balance, ciphertext_old_source): (u64, &ElGamalCiphertext), + keypair_source: &ElGamalKeypair, + (pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey), + fee_parameters: FeeParameters, + pubkey_fee_collector: &ElGamalPubkey, + ) -> Result { + // split and encrypt transfer amount + let (amount_lo, amount_hi) = split_u64_into_u32(transfer_amount); + + let (ciphertext_lo, opening_lo) = TransferAmountEncryption::new( + amount_lo, + &keypair_source.public, + pubkey_dest, + pubkey_auditor, + ); + let (ciphertext_hi, opening_hi) = TransferAmountEncryption::new( + amount_hi, + &keypair_source.public, + pubkey_dest, + pubkey_auditor, + ); + + // subtract transfer amount from the spendable ciphertext + let new_spendable_balance = spendable_balance + .checked_sub(transfer_amount) + .ok_or(ProofError::Generation)?; + + let transfer_amount_lo_source = ElGamalCiphertext { + commitment: ciphertext_lo.commitment, + handle: ciphertext_lo.source, + }; + + let transfer_amount_hi_source = ElGamalCiphertext { + commitment: ciphertext_hi.commitment, + handle: ciphertext_hi.source, + }; + + let ciphertext_new_source = ciphertext_old_source + - combine_u32_ciphertexts(&transfer_amount_lo_source, &transfer_amount_hi_source); + + // calculate and encrypt fee + let (fee_amount, delta_fee) = + calculate_fee(transfer_amount, fee_parameters.fee_rate_basis_points); + + let below_max = u64::ct_gt(&fee_parameters.maximum_fee, &fee_amount); + let fee_to_encrypt = + u64::conditional_select(&fee_parameters.maximum_fee, &fee_amount, below_max); + // u64::conditional_select(&fee_amount, &fee_parameters.maximum_fee, below_max); + + let (ciphertext_fee, opening_fee) = + FeeEncryption::new(fee_to_encrypt, pubkey_dest, pubkey_fee_collector); + + // generate transcript and append all public inputs + let pod_transfer_with_fee_pubkeys = pod::TransferWithFeePubkeys::new( + &keypair_source.public, + pubkey_dest, + pubkey_auditor, + pubkey_fee_collector, + ); + let pod_ciphertext_lo = pod::TransferAmountEncryption(ciphertext_lo.to_bytes()); + let pod_ciphertext_hi = pod::TransferAmountEncryption(ciphertext_hi.to_bytes()); + let pod_ciphertext_new_source: pod::ElGamalCiphertext = ciphertext_new_source.into(); + let pod_ciphertext_fee = pod::FeeEncryption(ciphertext_fee.to_bytes()); + + let mut transcript = TransferWithFeeProof::transcript_new( + &pod_transfer_with_fee_pubkeys, + &pod_ciphertext_lo, + &pod_ciphertext_hi, + &pod_ciphertext_fee, + ); + + let proof = TransferWithFeeProof::new( + (amount_lo, &ciphertext_lo, &opening_lo), + (amount_hi, &ciphertext_hi, &opening_hi), + keypair_source, + (pubkey_dest, pubkey_auditor), + (new_spendable_balance, &ciphertext_new_source), + (fee_amount, &ciphertext_fee, &opening_fee), + delta_fee, + pubkey_fee_collector, + fee_parameters, + &mut transcript, + ); + + Ok(Self { + ciphertext_lo: pod_ciphertext_lo, + ciphertext_hi: pod_ciphertext_hi, + transfer_with_fee_pubkeys: pod_transfer_with_fee_pubkeys, + ciphertext_new_source: pod_ciphertext_new_source, + ciphertext_fee: pod_ciphertext_fee, + fee_parameters: fee_parameters.into(), + proof, + }) + } + + /// Extracts the lo ciphertexts associated with a transfer-with-fee data + fn ciphertext_lo(&self, role: Role) -> Result { + let ciphertext_lo: TransferAmountEncryption = self.ciphertext_lo.try_into()?; + + let handle_lo = match role { + Role::Source => ciphertext_lo.source, + Role::Dest => ciphertext_lo.dest, + Role::Auditor => ciphertext_lo.auditor, + }; + + Ok(ElGamalCiphertext { + commitment: ciphertext_lo.commitment, + handle: handle_lo, + }) + } + + /// Extracts the lo ciphertexts associated with a transfer-with-fee data + fn ciphertext_hi(&self, role: Role) -> Result { + let ciphertext_hi: TransferAmountEncryption = self.ciphertext_hi.try_into()?; + + let handle_hi = match role { + Role::Source => ciphertext_hi.source, + Role::Dest => ciphertext_hi.dest, + Role::Auditor => ciphertext_hi.auditor, + }; + + Ok(ElGamalCiphertext { + commitment: ciphertext_hi.commitment, + handle: handle_hi, + }) + } + + /// Decrypts transfer amount from transfer-with-fee data + /// + /// TODO: This function should run in constant time. Use `subtle::Choice` for the if statement + /// and make sure that the function does not terminate prematurely due to errors + /// + /// TODO: Define specific error type for decryption error + pub fn decrypt_amount(&self, role: Role, sk: &ElGamalSecretKey) -> Result { + let ciphertext_lo = self.ciphertext_lo(role)?; + let ciphertext_hi = self.ciphertext_hi(role)?; + + let amount_lo = ciphertext_lo.decrypt_u32_online(sk, &DECODE_U32_PRECOMPUTATION_FOR_G); + let amount_hi = ciphertext_hi.decrypt_u32_online(sk, &DECODE_U32_PRECOMPUTATION_FOR_G); + + if let (Some(amount_lo), Some(amount_hi)) = (amount_lo, amount_hi) { + Ok((amount_lo as u64) + (TWO_32 * amount_hi as u64)) + } else { + Err(ProofError::Verification) + } + } +} + +#[cfg(not(target_arch = "bpf"))] +impl Verifiable for TransferWithFeeData { + fn verify(&self) -> Result<(), ProofError> { + let mut transcript = TransferWithFeeProof::transcript_new( + &self.transfer_with_fee_pubkeys, + &self.ciphertext_lo, + &self.ciphertext_hi, + &self.ciphertext_fee, + ); + + let ciphertext_lo = self.ciphertext_lo.try_into()?; + let ciphertext_hi = self.ciphertext_hi.try_into()?; + let transfer_with_fee_pubkeys = self.transfer_with_fee_pubkeys.try_into()?; + let new_spendable_ciphertext = self.ciphertext_new_source.try_into()?; + + let ciphertext_fee = self.ciphertext_fee.try_into()?; + let fee_parameters = self.fee_parameters.into(); + + self.proof.verify( + &ciphertext_lo, + &ciphertext_hi, + &transfer_with_fee_pubkeys, + &new_spendable_ciphertext, + &ciphertext_fee, + fee_parameters, + &mut transcript, + ) + } +} + +// #[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +pub struct TransferWithFeeProof { + pub commitment_new_source: pod::PedersenCommitment, + pub commitment_claimed: pod::PedersenCommitment, + pub equality_proof: pod::EqualityProof, + pub ciphertext_amount_validity_proof: pod::AggregatedValidityProof, + pub fee_sigma_proof: pod::FeeSigmaProof, + pub ciphertext_fee_validity_proof: pod::ValidityProof, + pub range_proof: pod::RangeProof256, +} + +#[allow(non_snake_case)] +#[cfg(not(target_arch = "bpf"))] +impl TransferWithFeeProof { + fn transcript_new( + transfer_with_fee_pubkeys: &pod::TransferWithFeePubkeys, + ciphertext_lo: &pod::TransferAmountEncryption, + ciphertext_hi: &pod::TransferAmountEncryption, + ciphertext_fee: &pod::FeeEncryption, + ) -> Transcript { + let mut transcript = Transcript::new(b"FeeProof"); + + transcript.append_message(b"transfer-with-fee-pubkeys", &transfer_with_fee_pubkeys.0); + transcript.append_message(b"ciphertext-lo", &ciphertext_lo.0); + transcript.append_message(b"ciphertext-hi", &ciphertext_hi.0); + transcript.append_message(b"ciphertext-fee", &ciphertext_fee.0); + + transcript + } + + #[allow(clippy::too_many_arguments)] + #[allow(clippy::many_single_char_names)] + pub fn new( + transfer_amount_lo_data: (u32, &TransferAmountEncryption, &PedersenOpening), + transfer_amount_hi_data: (u32, &TransferAmountEncryption, &PedersenOpening), + keypair_source: &ElGamalKeypair, + (pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey), + (source_new_balance, ciphertext_new_source): (u64, &ElGamalCiphertext), + + (fee_amount, ciphertext_fee, opening_fee): (u64, &FeeEncryption, &PedersenOpening), + delta_fee: u64, + pubkey_fee_collector: &ElGamalPubkey, + fee_parameters: FeeParameters, + transcript: &mut Transcript, + ) -> Self { + let (transfer_amount_lo, ciphertext_lo, opening_lo) = transfer_amount_lo_data; + let (transfer_amount_hi, ciphertext_hi, opening_hi) = transfer_amount_hi_data; + + // generate a Pedersen commitment for the remaining balance in source + let (commitment_new_source, opening_source) = Pedersen::new(source_new_balance); + let (commitment_claimed, opening_claimed) = Pedersen::new(delta_fee); + + let pod_commitment_new_source: pod::PedersenCommitment = commitment_new_source.into(); + let pod_commitment_claimed: pod::PedersenCommitment = commitment_claimed.into(); + + transcript.append_commitment(b"commitment-new-source", &pod_commitment_new_source); + transcript.append_commitment(b"commitment-claimed", &pod_commitment_claimed); + + // generate equality_proof + let equality_proof = EqualityProof::new( + keypair_source, + ciphertext_new_source, + source_new_balance, + &opening_source, + transcript, + ); + + // generate ciphertext validity proof + let ciphertext_amount_validity_proof = AggregatedValidityProof::new( + (pubkey_dest, pubkey_auditor), + (transfer_amount_lo, transfer_amount_hi), + (opening_lo, opening_hi), + transcript, + ); + + let (commitment_delta, opening_delta) = compute_delta_commitment_and_opening( + (&ciphertext_lo.commitment, opening_lo), + (&ciphertext_hi.commitment, opening_hi), + (&ciphertext_fee.commitment, opening_fee), + fee_parameters.fee_rate_basis_points, + ); + + let fee_sigma_proof = FeeSigmaProof::new( + (fee_amount, &ciphertext_fee.commitment, opening_fee), + (delta_fee, &commitment_delta, &opening_delta), + (&commitment_claimed, &opening_claimed), + fee_parameters.maximum_fee, + transcript, + ); + + let ciphertext_fee_validity_proof = ValidityProof::new( + (pubkey_dest, pubkey_fee_collector), + fee_amount, + opening_fee, + transcript, + ); + + let opening_claimed_negated = &PedersenOpening::default() - &opening_claimed; + let range_proof = RangeProof::new( + vec![ + source_new_balance, + transfer_amount_lo as u64, + transfer_amount_hi as u64, + delta_fee, + FEE_DENOMINATOR - delta_fee, + ], + vec![ + 64, 32, 32, 64, // double check + 64, + ], + vec![ + &opening_source, + opening_lo, + opening_hi, + &opening_claimed, + &opening_claimed_negated, + ], + transcript, + ); + + Self { + commitment_new_source: pod_commitment_new_source, + commitment_claimed: pod_commitment_claimed, + equality_proof: equality_proof.into(), + ciphertext_amount_validity_proof: ciphertext_amount_validity_proof.into(), + fee_sigma_proof: fee_sigma_proof.into(), + ciphertext_fee_validity_proof: ciphertext_fee_validity_proof.into(), + range_proof: range_proof.try_into().expect("range proof: length error"), + } + } + + pub fn verify( + &self, + ciphertext_lo: &TransferAmountEncryption, + ciphertext_hi: &TransferAmountEncryption, + transfer_with_fee_pubkeys: &TransferWithFeePubkeys, + new_spendable_ciphertext: &ElGamalCiphertext, + + ciphertext_fee: &FeeEncryption, + fee_parameters: FeeParameters, + transcript: &mut Transcript, + ) -> Result<(), ProofError> { + transcript.append_commitment(b"commitment-new-source", &self.commitment_new_source); + transcript.append_commitment(b"commitment-claimed", &self.commitment_claimed); + + let commitment_new_source: PedersenCommitment = self.commitment_new_source.try_into()?; + let commitment_claimed: PedersenCommitment = self.commitment_claimed.try_into()?; + + let equality_proof: EqualityProof = self.equality_proof.try_into()?; + let ciphertext_amount_validity_proof: AggregatedValidityProof = + self.ciphertext_amount_validity_proof.try_into()?; + let fee_sigma_proof: FeeSigmaProof = self.fee_sigma_proof.try_into()?; + let ciphertext_fee_validity_proof: ValidityProof = + self.ciphertext_fee_validity_proof.try_into()?; + let range_proof: RangeProof = self.range_proof.try_into()?; + + // verify equality proof + equality_proof.verify( + &transfer_with_fee_pubkeys.source, + new_spendable_ciphertext, + &commitment_new_source, + transcript, + )?; + + // verify that the transfer amount is encrypted correctly + ciphertext_amount_validity_proof.verify( + ( + &transfer_with_fee_pubkeys.dest, + &transfer_with_fee_pubkeys.auditor, + ), + (&ciphertext_lo.commitment, &ciphertext_hi.commitment), + (&ciphertext_lo.dest, &ciphertext_hi.dest), + (&ciphertext_lo.auditor, &ciphertext_hi.auditor), + transcript, + )?; + + // verify fee sigma proof + let commitment_delta = compute_delta_commitment( + &ciphertext_lo.commitment, + &ciphertext_hi.commitment, + &ciphertext_fee.commitment, + fee_parameters.fee_rate_basis_points, + ); + + fee_sigma_proof.verify( + &ciphertext_fee.commitment, + &commitment_delta, + &commitment_claimed, + fee_parameters.maximum_fee, + transcript, + )?; + + ciphertext_fee_validity_proof.verify( + &ciphertext_fee.commitment, + ( + &transfer_with_fee_pubkeys.dest, + &transfer_with_fee_pubkeys.fee_collector, + ), + (&ciphertext_fee.dest, &ciphertext_fee.fee_collector), + transcript, + )?; + + let commitment_claimed_negated = &(*COMMITMENT_FEE_DENOMINATOR) - &commitment_claimed; + range_proof.verify( + vec![ + &commitment_new_source, + &ciphertext_lo.commitment, + &ciphertext_hi.commitment, + &commitment_claimed, + &commitment_claimed_negated, + ], + vec![64, 32, 32, 64, 64], + transcript, + )?; + + Ok(()) + } +} + +/// The ElGamal public keys needed for a transfer with fee +#[derive(Clone)] +#[repr(C)] +#[cfg(not(target_arch = "bpf"))] +pub struct TransferWithFeePubkeys { + pub source: ElGamalPubkey, + pub dest: ElGamalPubkey, + pub auditor: ElGamalPubkey, + pub fee_collector: ElGamalPubkey, +} + +#[cfg(not(target_arch = "bpf"))] +impl TransferWithFeePubkeys { + pub fn to_bytes(&self) -> [u8; 128] { + let mut bytes = [0u8; 128]; + bytes[..32].copy_from_slice(&self.source.to_bytes()); + bytes[32..64].copy_from_slice(&self.dest.to_bytes()); + bytes[64..96].copy_from_slice(&self.auditor.to_bytes()); + bytes[96..128].copy_from_slice(&self.fee_collector.to_bytes()); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let bytes = array_ref![bytes, 0, 128]; + let (source, dest, auditor, fee_collector) = array_refs![bytes, 32, 32, 32, 32]; + + let source = ElGamalPubkey::from_bytes(source).ok_or(ProofError::Verification)?; + let dest = ElGamalPubkey::from_bytes(dest).ok_or(ProofError::Verification)?; + let auditor = ElGamalPubkey::from_bytes(auditor).ok_or(ProofError::Verification)?; + let fee_collector = + ElGamalPubkey::from_bytes(fee_collector).ok_or(ProofError::Verification)?; + + Ok(Self { + source, + dest, + auditor, + fee_collector, + }) + } +} + +#[cfg(not(target_arch = "bpf"))] +impl pod::TransferWithFeePubkeys { + pub fn new( + source: &ElGamalPubkey, + dest: &ElGamalPubkey, + auditor: &ElGamalPubkey, + fee_collector: &ElGamalPubkey, + ) -> Self { + let mut bytes = [0u8; 128]; + bytes[..32].copy_from_slice(&source.to_bytes()); + bytes[32..64].copy_from_slice(&dest.to_bytes()); + bytes[64..96].copy_from_slice(&auditor.to_bytes()); + bytes[96..128].copy_from_slice(&fee_collector.to_bytes()); + Self(bytes) + } +} + +#[derive(Clone)] +#[repr(C)] +#[cfg(not(target_arch = "bpf"))] +pub struct FeeEncryption { + pub commitment: PedersenCommitment, + pub dest: DecryptHandle, + pub fee_collector: DecryptHandle, +} + +#[cfg(not(target_arch = "bpf"))] +impl FeeEncryption { + pub fn new( + amount: u64, + pubkey_dest: &ElGamalPubkey, + pubkey_fee_collector: &ElGamalPubkey, + ) -> (Self, PedersenOpening) { + let (commitment, opening) = Pedersen::new(amount); + let fee_encryption = Self { + commitment, + dest: pubkey_dest.decrypt_handle(&opening), + fee_collector: pubkey_fee_collector.decrypt_handle(&opening), + }; + + (fee_encryption, opening) + } + + pub fn to_bytes(&self) -> [u8; 96] { + let mut bytes = [0u8; 96]; + bytes[..32].copy_from_slice(&self.commitment.to_bytes()); + bytes[32..64].copy_from_slice(&self.dest.to_bytes()); + bytes[64..96].copy_from_slice(&self.fee_collector.to_bytes()); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let bytes = array_ref![bytes, 0, 96]; + let (commitment, dest, fee_collector) = array_refs![bytes, 32, 32, 32]; + + let commitment = + PedersenCommitment::from_bytes(commitment).ok_or(ProofError::Verification)?; + let dest = DecryptHandle::from_bytes(dest).ok_or(ProofError::Verification)?; + let fee_collector = + DecryptHandle::from_bytes(fee_collector).ok_or(ProofError::Verification)?; + + Ok(Self { + commitment, + dest, + fee_collector, + }) + } +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct FeeParameters { + /// Fee rate expressed as basis points of the transfer amount, i.e. increments of 0.01% + pub fee_rate_basis_points: u16, + /// Maximum fee assessed on transfers, expressed as an amount of tokens + pub maximum_fee: u64, +} + +#[cfg(not(target_arch = "bpf"))] +impl FeeParameters { + pub fn to_bytes(&self) -> [u8; 10] { + let mut bytes = [0u8; 10]; + bytes[..2].copy_from_slice(&self.fee_rate_basis_points.to_le_bytes()); + bytes[2..10].copy_from_slice(&self.maximum_fee.to_le_bytes()); + + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Self { + let bytes = array_ref![bytes, 0, 10]; + let (fee_rate_basis_points, maximum_fee) = array_refs![bytes, 2, 8]; + + Self { + fee_rate_basis_points: u16::from_le_bytes(*fee_rate_basis_points), + maximum_fee: u64::from_le_bytes(*maximum_fee), + } + } +} + +#[cfg(not(target_arch = "bpf"))] +fn calculate_fee(transfer_amount: u64, fee_rate_basis_points: u16) -> (u64, u64) { + let fee_scaled = (transfer_amount as u128) * (fee_rate_basis_points as u128); + + let fee = (fee_scaled / FEE_DENOMINATOR as u128) as u64; + let rem = (fee_scaled % FEE_DENOMINATOR as u128) as u64; + + if rem == 0 { + (fee, rem) + } else { + (fee + 1, rem) + } +} + +#[cfg(not(target_arch = "bpf"))] +fn compute_delta_commitment_and_opening( + (commitment_lo, opening_lo): (&PedersenCommitment, &PedersenOpening), + (commitment_hi, opening_hi): (&PedersenCommitment, &PedersenOpening), + (commitment_fee, opening_fee): (&PedersenCommitment, &PedersenOpening), + fee_rate_basis_points: u16, +) -> (PedersenCommitment, PedersenOpening) { + let fee_rate_scalar = Scalar::from(fee_rate_basis_points); + + let commitment_delta = commitment_fee * Scalar::from(FEE_DENOMINATOR) + - &(&combine_u32_commitments(commitment_lo, commitment_hi) * &fee_rate_scalar); + + let opening_delta = opening_fee * Scalar::from(FEE_DENOMINATOR) + - &(&combine_u32_openings(opening_lo, opening_hi) * &fee_rate_scalar); + + (commitment_delta, opening_delta) +} + +#[cfg(not(target_arch = "bpf"))] +fn compute_delta_commitment( + commitment_lo: &PedersenCommitment, + commitment_hi: &PedersenCommitment, + commitment_fee: &PedersenCommitment, + fee_rate_basis_points: u16, +) -> PedersenCommitment { + let fee_rate_scalar = Scalar::from(fee_rate_basis_points); + + commitment_fee * Scalar::from(FEE_DENOMINATOR) + - &(&combine_u32_commitments(commitment_lo, commitment_hi) * &fee_rate_scalar) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_fee_correctness() { + let keypair_source = ElGamalKeypair::new_rand(); + let pubkey_dest = ElGamalKeypair::new_rand().public; + let pubkey_auditor = ElGamalKeypair::new_rand().public; + let pubkey_fee_collector = ElGamalKeypair::new_rand().public; + + let spendable_balance: u64 = 120; + let spendable_ciphertext = keypair_source.public.encrypt(spendable_balance); + + let transfer_amount: u64 = 100; + + let fee_parameters = FeeParameters { + fee_rate_basis_points: 100, + maximum_fee: 3, + }; + + let fee_data = TransferWithFeeData::new( + transfer_amount, + (spendable_balance, &spendable_ciphertext), + &keypair_source, + (&pubkey_dest, &pubkey_auditor), + fee_parameters, + &pubkey_fee_collector, + ) + .unwrap(); + + assert!(fee_data.verify().is_ok()); + } +} diff --git a/zk-token-sdk/src/instruction/withdraw.rs b/zk-token-sdk/src/instruction/withdraw.rs index def1a69300..2feee099e0 100644 --- a/zk-token-sdk/src/instruction/withdraw.rs +++ b/zk-token-sdk/src/instruction/withdraw.rs @@ -6,8 +6,8 @@ use { use { crate::{ encryption::{ - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, - pedersen::{Pedersen, PedersenCommitment, PedersenOpening}, + elgamal::{ElGamal, ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, + pedersen::{Pedersen, PedersenCommitment}, }, errors::ProofError, instruction::Verifiable, @@ -30,11 +30,11 @@ use { #[repr(C)] pub struct WithdrawData { /// The source account ElGamal pubkey - pub elgamal_pubkey: pod::ElGamalPubkey, // 32 bytes + pub pubkey: pod::ElGamalPubkey, // 32 bytes /// The source account available balance *after* the withdraw (encrypted by /// `source_pk` - pub final_balance_ct: pod::ElGamalCiphertext, // 64 bytes + pub final_ciphertext: pod::ElGamalCiphertext, // 64 bytes /// Range proof pub proof: WithdrawProof, // 736 bytes @@ -44,38 +44,43 @@ impl WithdrawData { #[cfg(not(target_arch = "bpf"))] pub fn new( amount: u64, - source_keypair: &ElGamalKeypair, + keypair: &ElGamalKeypair, current_balance: u64, - current_balance_ct: ElGamalCiphertext, - ) -> Self { + current_ciphertext: &ElGamalCiphertext, + ) -> Result { // subtract withdraw amount from current balance // - // panics if current_balance < amount - let final_balance = current_balance - amount; + // errors if current_balance < amount + let final_balance = current_balance + .checked_sub(amount) + .ok_or(ProofError::Generation)?; // encode withdraw amount as an ElGamal ciphertext and subtract it from // current source balance - let amount_encoded = source_keypair - .public - .encrypt_with(amount, &PedersenOpening::default()); - let final_balance_ct = current_balance_ct - amount_encoded; + let final_ciphertext = current_ciphertext - &ElGamal::encode(amount); - let proof = WithdrawProof::new(source_keypair, final_balance, &final_balance_ct); + let pod_pubkey = pod::ElGamalPubkey((&keypair.public).to_bytes()); + let pod_final_ciphertext: pod::ElGamalCiphertext = final_ciphertext.into(); + let mut transcript = WithdrawProof::transcript_new(&pod_pubkey, &pod_final_ciphertext); + let proof = WithdrawProof::new(keypair, final_balance, &final_ciphertext, &mut transcript); - Self { - elgamal_pubkey: source_keypair.public.into(), - final_balance_ct: final_balance_ct.into(), + Ok(Self { + pubkey: pod_pubkey, + final_ciphertext: pod_final_ciphertext, proof, - } + }) } } #[cfg(not(target_arch = "bpf"))] impl Verifiable for WithdrawData { fn verify(&self) -> Result<(), ProofError> { - let elgamal_pubkey = self.elgamal_pubkey.try_into()?; - let final_balance_ct = self.final_balance_ct.try_into()?; - self.proof.verify(&elgamal_pubkey, &final_balance_ct) + let mut transcript = WithdrawProof::transcript_new(&self.pubkey, &self.final_ciphertext); + + let elgamal_pubkey = self.pubkey.try_into()?; + let final_balance_ciphertext = self.final_ciphertext.try_into()?; + self.proof + .verify(&elgamal_pubkey, &final_balance_ciphertext, &mut transcript) } } @@ -98,53 +103,44 @@ pub struct WithdrawProof { #[allow(non_snake_case)] #[cfg(not(target_arch = "bpf"))] impl WithdrawProof { - fn transcript_new() -> Transcript { - Transcript::new(b"WithdrawProof") + fn transcript_new( + pubkey: &pod::ElGamalPubkey, + ciphertext: &pod::ElGamalCiphertext, + ) -> Transcript { + let mut transcript = Transcript::new(b"WithdrawProof"); + + transcript.append_pubkey(b"pubkey", pubkey); + transcript.append_ciphertext(b"ciphertext", ciphertext); + + transcript } pub fn new( - source_keypair: &ElGamalKeypair, + keypair: &ElGamalKeypair, final_balance: u64, - final_balance_ct: &ElGamalCiphertext, + final_ciphertext: &ElGamalCiphertext, + transcript: &mut Transcript, ) -> Self { - let mut transcript = Self::transcript_new(); - - // add a domain separator to record the start of the protocol - transcript.withdraw_proof_domain_sep(); - // generate a Pedersen commitment for `final_balance` let (commitment, opening) = Pedersen::new(final_balance); + let pod_commitment: pod::PedersenCommitment = commitment.into(); - // extract the relevant scalar and Ristretto points from the inputs - let P_EG = source_keypair.public.get_point(); - let C_EG = final_balance_ct.commitment.get_point(); - let D_EG = final_balance_ct.handle.get_point(); - let C_Ped = commitment.get_point(); - - // append all current state to the transcript - transcript.append_point(b"P_EG", &P_EG.compress()); - transcript.append_point(b"C_EG", &C_EG.compress()); - transcript.append_point(b"D_EG", &D_EG.compress()); - transcript.append_point(b"C_Ped", &C_Ped.compress()); + transcript.append_commitment(b"commitment", &pod_commitment); // generate equality_proof let equality_proof = EqualityProof::new( - source_keypair, - final_balance_ct, + keypair, + final_ciphertext, final_balance, &opening, - &mut transcript, + transcript, ); - let range_proof = RangeProof::new( - vec![final_balance], - vec![64], - vec![&opening], - &mut transcript, - ); + let range_proof = + RangeProof::new(vec![final_balance], vec![64], vec![&opening], transcript); WithdrawProof { - commitment: commitment.into(), + commitment: pod_commitment, equality_proof: equality_proof.try_into().expect("equality proof"), range_proof: range_proof.try_into().expect("range proof"), } @@ -152,43 +148,25 @@ impl WithdrawProof { pub fn verify( &self, - source_pk: &ElGamalPubkey, - final_balance_ct: &ElGamalCiphertext, + pubkey: &ElGamalPubkey, + final_ciphertext: &ElGamalCiphertext, + transcript: &mut Transcript, ) -> Result<(), ProofError> { - let mut transcript = Self::transcript_new(); + transcript.append_commitment(b"commitment", &self.commitment); let commitment: PedersenCommitment = self.commitment.try_into()?; let equality_proof: EqualityProof = self.equality_proof.try_into()?; let range_proof: RangeProof = self.range_proof.try_into()?; - // add a domain separator to record the start of the protocol - transcript.withdraw_proof_domain_sep(); - - // extract the relevant scalar and Ristretto points from the inputs - let P_EG = source_pk.get_point(); - let C_EG = final_balance_ct.commitment.get_point(); - let D_EG = final_balance_ct.handle.get_point(); - let C_Ped = commitment.get_point(); - - // append all current state to the transcript - transcript.append_point(b"P_EG", &P_EG.compress()); - transcript.append_point(b"C_EG", &C_EG.compress()); - transcript.append_point(b"D_EG", &D_EG.compress()); - transcript.append_point(b"C_Ped", &C_Ped.compress()); - // verify equality proof // // TODO: we can also consider verifying equality and range proof in a batch - equality_proof.verify(source_pk, final_balance_ct, &commitment, &mut transcript)?; + equality_proof.verify(pubkey, final_ciphertext, &commitment, transcript)?; // verify range proof // // TODO: double compressing here - consider modifying range proof input type to `PedersenCommitment` - range_proof.verify( - vec![&commitment.get_point().compress()], - vec![64_usize], - &mut transcript, - )?; + range_proof.verify(vec![&commitment], vec![64_usize], transcript)?; Ok(()) } @@ -201,29 +179,31 @@ mod test { #[test] fn test_withdraw_correctness() { // generate and verify proof for the proper setting - let elgamal_keypair = ElGamalKeypair::new_rand(); + let keypair = ElGamalKeypair::new_rand(); let current_balance: u64 = 77; - let current_balance_ct = elgamal_keypair.public.encrypt(current_balance); + let current_ciphertext = keypair.public.encrypt(current_balance); let withdraw_amount: u64 = 55; let data = WithdrawData::new( withdraw_amount, - &elgamal_keypair, + &keypair, current_balance, - current_balance_ct, - ); + ¤t_ciphertext, + ) + .unwrap(); assert!(data.verify().is_ok()); // generate and verify proof with wrong balance let wrong_balance: u64 = 99; let data = WithdrawData::new( withdraw_amount, - &elgamal_keypair, + &keypair, wrong_balance, - current_balance_ct, - ); + ¤t_ciphertext, + ) + .unwrap(); assert!(data.verify().is_err()); } } diff --git a/zk-token-sdk/src/lib.rs b/zk-token-sdk/src/lib.rs index ab623efa27..20d44f3e35 100644 --- a/zk-token-sdk/src/lib.rs +++ b/zk-token-sdk/src/lib.rs @@ -31,7 +31,8 @@ mod sigma_proofs; #[cfg(not(target_arch = "bpf"))] mod transcript; -mod instruction; +// TODO: re-organize visibility +pub mod instruction; pub mod zk_token_elgamal; pub mod zk_token_proof_instruction; pub mod zk_token_proof_program; diff --git a/zk-token-sdk/src/range_proof/mod.rs b/zk-token-sdk/src/range_proof/mod.rs index 68edfa2e54..9e27be27af 100644 --- a/zk-token-sdk/src/range_proof/mod.rs +++ b/zk-token-sdk/src/range_proof/mod.rs @@ -1,6 +1,6 @@ #[cfg(not(target_arch = "bpf"))] use { - crate::encryption::pedersen::{Pedersen, PedersenOpening}, + crate::encryption::pedersen::{Pedersen, PedersenCommitment, PedersenOpening}, curve25519_dalek::traits::MultiscalarMul, rand::rngs::OsRng, subtle::{Choice, ConditionallySelectable}, @@ -220,7 +220,7 @@ impl RangeProof { #[allow(clippy::many_single_char_names)] pub fn verify( &self, - comms: Vec<&CompressedRistretto>, + comms: Vec<&PedersenCommitment>, bit_lengths: Vec, transcript: &mut Transcript, ) -> Result<(), RangeProofError> { @@ -308,7 +308,7 @@ impl RangeProof { .chain(self.ipp_proof.R_vec.iter().map(|R| R.decompress())) .chain(bp_gens.G(nm).map(|&x| Some(x))) .chain(bp_gens.H(nm).map(|&x| Some(x))) - .chain(comms.iter().map(|V| V.decompress())), + .chain(comms.iter().map(|V| Some(*V.get_point()))), ) .ok_or(RangeProofError::MultiscalarMul)?; @@ -403,11 +403,7 @@ mod tests { let proof = RangeProof::new(vec![55], vec![32], vec![&open], &mut transcript_create); assert!(proof - .verify( - vec![&comm.get_point().compress()], - vec![32], - &mut transcript_verify - ) + .verify(vec![&comm], vec![32], &mut transcript_verify) .is_ok()); } @@ -427,13 +423,9 @@ mod tests { &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(); - assert!(proof .verify( - vec![&comm_1_point, &comm_2_point, &comm_3_point], + vec![&comm_1, &comm_2, &comm_3], vec![64, 32, 32], &mut transcript_verify, ) diff --git a/zk-token-sdk/src/sigma_proofs/equality_proof.rs b/zk-token-sdk/src/sigma_proofs/equality_proof.rs index 3a25e5f3e9..3b5fbeeef1 100644 --- a/zk-token-sdk/src/sigma_proofs/equality_proof.rs +++ b/zk-token-sdk/src/sigma_proofs/equality_proof.rs @@ -69,6 +69,8 @@ impl EqualityProof { opening: &PedersenOpening, transcript: &mut Transcript, ) -> Self { + transcript.equality_proof_domain_sep(); + // extract the relevant scalar and Ristretto points from the inputs let P_EG = elgamal_keypair.public.get_point(); let D_EG = ciphertext.handle.get_point(); @@ -127,6 +129,8 @@ impl EqualityProof { commitment: &PedersenCommitment, transcript: &mut Transcript, ) -> Result<(), EqualityProofError> { + transcript.equality_proof_domain_sep(); + // extract the relevant scalar and Ristretto points from the inputs let P_EG = elgamal_pubkey.get_point(); let C_EG = ciphertext.commitment.get_point(); diff --git a/zk-token-sdk/src/sigma_proofs/errors.rs b/zk-token-sdk/src/sigma_proofs/errors.rs index 973ba15d6d..e1bf03eae4 100644 --- a/zk-token-sdk/src/sigma_proofs/errors.rs +++ b/zk-token-sdk/src/sigma_proofs/errors.rs @@ -57,7 +57,7 @@ impl From for ZeroBalanceProofError { } #[derive(Error, Clone, Debug, Eq, PartialEq)] -pub enum FeeProofError { +pub enum FeeSigmaProofError { #[error("the required algebraic relation does not hold")] AlgebraicRelation, #[error("malformed proof")] @@ -68,7 +68,7 @@ pub enum FeeProofError { Transcript, } -impl From for FeeProofError { +impl From for FeeSigmaProofError { fn from(_err: TranscriptError) -> Self { Self::Transcript } diff --git a/zk-token-sdk/src/sigma_proofs/fee_proof.rs b/zk-token-sdk/src/sigma_proofs/fee_proof.rs index bf952e70a7..767b240369 100644 --- a/zk-token-sdk/src/sigma_proofs/fee_proof.rs +++ b/zk-token-sdk/src/sigma_proofs/fee_proof.rs @@ -8,7 +8,8 @@ use { rand::rngs::OsRng, }; use { - crate::{sigma_proofs::errors::FeeProofError, transcript::TranscriptProtocol}, + crate::{sigma_proofs::errors::FeeSigmaProofError, transcript::TranscriptProtocol}, + arrayref::{array_ref, array_refs}, curve25519_dalek::{ ristretto::{CompressedRistretto, RistrettoPoint}, scalar::Scalar, @@ -73,25 +74,25 @@ impl FeeSigmaProof { &mut transcript_fee_below_max, ); - let above_max = u64::ct_gt(&max_fee, &fee_amount); + let below_max = u64::ct_gt(&max_fee, &fee_amount); // conditionally assign transcript; transcript is not conditionally selectable - if bool::from(above_max) { - *transcript = transcript_fee_above_max; - } else { + if bool::from(below_max) { *transcript = transcript_fee_below_max; + } else { + *transcript = transcript_fee_above_max; } Self { fee_max_proof: FeeMaxProof::conditional_select( &proof_fee_above_max.fee_max_proof, &proof_fee_below_max.fee_max_proof, - above_max, + below_max, ), fee_equality_proof: FeeEqualityProof::conditional_select( &proof_fee_above_max.fee_equality_proof, &proof_fee_below_max.fee_equality_proof, - above_max, + below_max, ), } } @@ -259,7 +260,7 @@ impl FeeSigmaProof { commitment_claimed: &PedersenCommitment, max_fee: u64, transcript: &mut Transcript, - ) -> Result<(), FeeProofError> { + ) -> Result<(), FeeSigmaProofError> { // extract the relevant scalar and Ristretto points from the input let m = Scalar::from(max_fee); @@ -275,19 +276,19 @@ impl FeeSigmaProof { .fee_max_proof .Y_max_proof .decompress() - .ok_or(FeeProofError::Format)?; + .ok_or(FeeSigmaProofError::Format)?; let z_max = self.fee_max_proof.z_max_proof; let Y_delta_real = self .fee_equality_proof .Y_delta .decompress() - .ok_or(FeeProofError::Format)?; + .ok_or(FeeSigmaProofError::Format)?; let Y_claimed = self .fee_equality_proof .Y_claimed .decompress() - .ok_or(FeeProofError::Format)?; + .ok_or(FeeSigmaProofError::Format)?; let z_x = self.fee_equality_proof.z_x; let z_delta_real = self.fee_equality_proof.z_delta; let z_claimed = self.fee_equality_proof.z_claimed; @@ -333,9 +334,56 @@ impl FeeSigmaProof { if check.is_identity() { Ok(()) } else { - Err(FeeProofError::AlgebraicRelation) + Err(FeeSigmaProofError::AlgebraicRelation) } } + + pub fn to_bytes(&self) -> [u8; 256] { + let mut buf = [0_u8; 256]; + buf[..32].copy_from_slice(self.fee_max_proof.Y_max_proof.as_bytes()); + buf[32..64].copy_from_slice(self.fee_max_proof.z_max_proof.as_bytes()); + buf[64..96].copy_from_slice(self.fee_max_proof.c_max_proof.as_bytes()); + buf[96..128].copy_from_slice(self.fee_equality_proof.Y_delta.as_bytes()); + buf[128..160].copy_from_slice(self.fee_equality_proof.Y_claimed.as_bytes()); + buf[160..192].copy_from_slice(self.fee_equality_proof.z_x.as_bytes()); + buf[192..224].copy_from_slice(self.fee_equality_proof.z_delta.as_bytes()); + buf[224..256].copy_from_slice(self.fee_equality_proof.z_claimed.as_bytes()); + buf + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let bytes = array_ref![bytes, 0, 256]; + let (Y_max_proof, z_max_proof, c_max_proof, Y_delta, Y_claimed, z_x, z_delta, z_claimed) = + array_refs![bytes, 32, 32, 32, 32, 32, 32, 32, 32]; + + let Y_max_proof = CompressedRistretto::from_slice(Y_max_proof); + let z_max_proof = + Scalar::from_canonical_bytes(*z_max_proof).ok_or(FeeSigmaProofError::Format)?; + let c_max_proof = + Scalar::from_canonical_bytes(*c_max_proof).ok_or(FeeSigmaProofError::Format)?; + + let Y_delta = CompressedRistretto::from_slice(Y_delta); + let Y_claimed = CompressedRistretto::from_slice(Y_claimed); + let z_x = Scalar::from_canonical_bytes(*z_x).ok_or(FeeSigmaProofError::Format)?; + let z_delta = Scalar::from_canonical_bytes(*z_delta).ok_or(FeeSigmaProofError::Format)?; + let z_claimed = + Scalar::from_canonical_bytes(*z_claimed).ok_or(FeeSigmaProofError::Format)?; + + Ok(Self { + fee_max_proof: FeeMaxProof { + Y_max_proof, + z_max_proof, + c_max_proof, + }, + fee_equality_proof: FeeEqualityProof { + Y_delta, + Y_claimed, + z_x, + z_delta, + z_claimed, + }, + }) + } } /// The fee max proof. @@ -491,4 +539,46 @@ mod test { ) .is_ok()); } + + #[test] + fn test_fee_delta_is_zero() { + let transfer_amount: u64 = 100; + let max_fee: u64 = 3; + + let rate_fee: u16 = 100; // 1.00% + let amount_fee: u64 = 1; + let delta: u64 = 0; // 1*10000 - 100*100 + + let (commitment_transfer, opening_transfer) = Pedersen::new(transfer_amount); + let (commitment_fee, opening_fee) = Pedersen::new(amount_fee); + + let scalar_rate = Scalar::from(rate_fee); + let commitment_delta = + &(&commitment_fee * &Scalar::from(10000_u64)) - &(&commitment_transfer * &scalar_rate); + let opening_delta = + &(&opening_fee * &Scalar::from(10000_u64)) - &(&opening_transfer * &scalar_rate); + + let (commitment_claimed, opening_claimed) = Pedersen::new(delta); + + let mut transcript_prover = Transcript::new(b"test"); + let mut transcript_verifier = Transcript::new(b"test"); + + let proof = FeeSigmaProof::new( + (amount_fee, &commitment_fee, &opening_fee), + (delta, &commitment_delta, &opening_delta), + (&commitment_claimed, &opening_claimed), + max_fee, + &mut transcript_prover, + ); + + assert!(proof + .verify( + &commitment_fee, + &commitment_delta, + &commitment_claimed, + max_fee, + &mut transcript_verifier, + ) + .is_ok()); + } } diff --git a/zk-token-sdk/src/sigma_proofs/validity_proof.rs b/zk-token-sdk/src/sigma_proofs/validity_proof.rs index ea3c7dd8cf..8eb32d2dfb 100644 --- a/zk-token-sdk/src/sigma_proofs/validity_proof.rs +++ b/zk-token-sdk/src/sigma_proofs/validity_proof.rs @@ -62,11 +62,13 @@ impl ValidityProof { /// * `opening` - The opening associated with the Pedersen commitment /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic pub fn new>( - (pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey), + (pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey), // TODO: rename pubkey_auditor amount: T, opening: &PedersenOpening, transcript: &mut Transcript, ) -> Self { + transcript.validity_proof_domain_sep(); + // extract the relevant scalar and Ristretto points from the inputs let P_dest = pubkey_dest.get_point(); let P_auditor = pubkey_auditor.get_point(); @@ -120,6 +122,8 @@ impl ValidityProof { (handle_dest, handle_auditor): (&DecryptHandle, &DecryptHandle), transcript: &mut Transcript, ) -> Result<(), ValidityProofError> { + transcript.validity_proof_domain_sep(); + // include Y_0, Y_1, Y_2 to transcript and extract challenges transcript.validate_and_append_point(b"Y_0", &self.Y_0)?; transcript.validate_and_append_point(b"Y_1", &self.Y_1)?; @@ -235,6 +239,8 @@ impl AggregatedValidityProof { (opening_lo, opening_hi): (&PedersenOpening, &PedersenOpening), transcript: &mut Transcript, ) -> Self { + transcript.aggregated_validity_proof_domain_sep(); + let t = transcript.challenge_scalar(b"t"); let aggregated_message = amount_lo.into() + amount_hi.into() * t; @@ -263,6 +269,8 @@ impl AggregatedValidityProof { (handle_lo_auditor, handle_hi_auditor): (&DecryptHandle, &DecryptHandle), transcript: &mut Transcript, ) -> Result<(), ValidityProofError> { + transcript.aggregated_validity_proof_domain_sep(); + let t = transcript.challenge_scalar(b"t"); let aggregated_commitment = commitment_lo + commitment_hi * t; diff --git a/zk-token-sdk/src/transcript.rs b/zk-token-sdk/src/transcript.rs index d989e2c449..d0dfda9e42 100644 --- a/zk-token-sdk/src/transcript.rs +++ b/zk-token-sdk/src/transcript.rs @@ -1,5 +1,5 @@ use { - crate::errors::TranscriptError, + crate::{errors::TranscriptError, zk_token_elgamal::pod}, curve25519_dalek::{ristretto::CompressedRistretto, scalar::Scalar, traits::IsIdentity}, merlin::Transcript, }; @@ -19,9 +19,6 @@ pub trait TranscriptProtocol { /// Append a domain separator for close account proof. fn close_account_proof_domain_sep(&mut self); - /// Append a domain separator for update account public key proof. - fn update_account_public_key_proof_domain_sep(&mut self); - /// Append a domain separator for withdraw proof. fn withdraw_proof_domain_sep(&mut self); @@ -34,6 +31,33 @@ pub trait TranscriptProtocol { /// Append a `point` with the given `label`. fn append_point(&mut self, label: &'static [u8], point: &CompressedRistretto); + /// Append an ElGamal pubkey with the given `label`. + fn append_pubkey(&mut self, label: &'static [u8], point: &pod::ElGamalPubkey); + + /// Append an ElGamal ciphertext with the given `label`. + fn append_ciphertext(&mut self, label: &'static [u8], point: &pod::ElGamalCiphertext); + + /// Append a Pedersen commitment with the given `label`. + fn append_commitment(&mut self, label: &'static [u8], point: &pod::PedersenCommitment); + + /// Append an ElGamal decryption handle with the given `label`. + fn append_handle(&mut self, label: &'static [u8], point: &pod::DecryptHandle); + + /// Append a domain separator for equality proof. + fn equality_proof_domain_sep(&mut self); + + /// Append a domain separator for zero-balance proof. + fn zero_balance_proof_domain_sep(&mut self); + + /// Append a domain separator for validity proof. + fn validity_proof_domain_sep(&mut self); + + /// Append a domain separator for aggregated validity proof. + fn aggregated_validity_proof_domain_sep(&mut self); + + /// Append a domain separator for fee sigma proof. + fn fee_sigma_proof_domain_sep(&mut self); + /// Check that a point is not the identity, then append it to the /// transcript. Otherwise, return an error. fn validate_and_append_point( @@ -66,10 +90,6 @@ impl TranscriptProtocol for Transcript { self.append_message(b"dom-sep", b"CloseAccountProof"); } - fn update_account_public_key_proof_domain_sep(&mut self) { - self.append_message(b"dom-sep", b"UpdateAccountPublicKeyProof"); - } - fn withdraw_proof_domain_sep(&mut self) { self.append_message(b"dom-sep", b"WithdrawProof"); } @@ -105,4 +125,40 @@ impl TranscriptProtocol for Transcript { Scalar::from_bytes_mod_order_wide(&buf) } + + fn append_pubkey(&mut self, label: &'static [u8], pubkey: &pod::ElGamalPubkey) { + self.append_message(label, &pubkey.0); + } + + fn append_ciphertext(&mut self, label: &'static [u8], ciphertext: &pod::ElGamalCiphertext) { + self.append_message(label, &ciphertext.0); + } + + fn append_commitment(&mut self, label: &'static [u8], commitment: &pod::PedersenCommitment) { + self.append_message(label, &commitment.0); + } + + fn append_handle(&mut self, label: &'static [u8], handle: &pod::DecryptHandle) { + self.append_message(label, &handle.0); + } + + fn equality_proof_domain_sep(&mut self) { + self.append_message(b"dom-sep", b"equality-proof") + } + + fn zero_balance_proof_domain_sep(&mut self) { + self.append_message(b"dom-sep", b"zero-balance-proof") + } + + fn validity_proof_domain_sep(&mut self) { + self.append_message(b"dom-sep", b"validity-proof") + } + + fn aggregated_validity_proof_domain_sep(&mut self) { + self.append_message(b"dom-sep", b"aggregated-validity-proof") + } + + fn fee_sigma_proof_domain_sep(&mut self) { + self.append_message(b"dom-sep", b"fee-sigma-proof") + } } diff --git a/zk-token-sdk/src/zk_token_elgamal/convert.rs b/zk-token-sdk/src/zk_token_elgamal/convert.rs index c63ebd71e0..73577854b5 100644 --- a/zk-token-sdk/src/zk_token_elgamal/convert.rs +++ b/zk-token-sdk/src/zk_token_elgamal/convert.rs @@ -21,10 +21,15 @@ mod target_arch { pedersen::PedersenCommitment, }, errors::ProofError, + instruction::{ + transfer::{TransferAmountEncryption, TransferPubkeys}, + transfer_with_fee::{FeeEncryption, FeeParameters, TransferWithFeePubkeys}, + }, range_proof::{errors::RangeProofError, RangeProof}, sigma_proofs::{ equality_proof::EqualityProof, errors::*, + fee_proof::FeeSigmaProof, validity_proof::{AggregatedValidityProof, ValidityProof}, zero_balance_proof::ZeroBalanceProof, }, @@ -202,6 +207,20 @@ mod target_arch { } } + impl From for pod::FeeSigmaProof { + fn from(proof: FeeSigmaProof) -> Self { + Self(proof.to_bytes()) + } + } + + impl TryFrom for FeeSigmaProof { + type Error = FeeSigmaProofError; + + fn try_from(pod: pod::FeeSigmaProof) -> Result { + Self::from_bytes(&pod.0) + } + } + impl TryFrom for pod::RangeProof64 { type Error = RangeProofError; @@ -260,6 +279,104 @@ mod target_arch { Self::from_bytes(&pod.0) } } + + #[cfg(not(target_arch = "bpf"))] + impl TryFrom for pod::RangeProof256 { + type Error = RangeProofError; + + fn try_from(proof: RangeProof) -> Result { + if proof.ipp_proof.serialized_size() != 576 { + return Err(RangeProofError::Format); + } + + let mut buf = [0_u8; 800]; + 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..800].copy_from_slice(&proof.ipp_proof.to_bytes()); + Ok(pod::RangeProof256(buf)) + } + } + + impl TryFrom for RangeProof { + type Error = RangeProofError; + + fn try_from(pod: pod::RangeProof256) -> Result { + Self::from_bytes(&pod.0) + } + } + + impl From for pod::TransferPubkeys { + fn from(keys: TransferPubkeys) -> Self { + Self(keys.to_bytes()) + } + } + + impl TryFrom for TransferPubkeys { + type Error = ProofError; + + fn try_from(pod: pod::TransferPubkeys) -> Result { + Self::from_bytes(&pod.0) + } + } + + impl From for pod::TransferWithFeePubkeys { + fn from(keys: TransferWithFeePubkeys) -> Self { + Self(keys.to_bytes()) + } + } + + impl TryFrom for TransferWithFeePubkeys { + type Error = ProofError; + + fn try_from(pod: pod::TransferWithFeePubkeys) -> Result { + Self::from_bytes(&pod.0) + } + } + + impl From for pod::TransferAmountEncryption { + fn from(ciphertext: TransferAmountEncryption) -> Self { + Self(ciphertext.to_bytes()) + } + } + + impl TryFrom for TransferAmountEncryption { + type Error = ProofError; + + fn try_from(pod: pod::TransferAmountEncryption) -> Result { + Self::from_bytes(&pod.0) + } + } + + impl From for pod::FeeEncryption { + fn from(ciphertext: FeeEncryption) -> Self { + Self(ciphertext.to_bytes()) + } + } + + impl TryFrom for FeeEncryption { + type Error = ProofError; + + fn try_from(pod: pod::FeeEncryption) -> Result { + Self::from_bytes(&pod.0) + } + } + + impl From for pod::FeeParameters { + fn from(parameters: FeeParameters) -> Self { + Self(parameters.to_bytes()) + } + } + + impl From for FeeParameters { + fn from(pod: pod::FeeParameters) -> Self { + Self::from_bytes(&pod.0) + } + } } #[cfg(target_arch = "bpf")] @@ -288,11 +405,7 @@ mod tests { let proof_deserialized: RangeProof = proof_serialized.try_into().unwrap(); assert!(proof_deserialized - .verify( - vec![&comm.get_point().compress()], - vec![64], - &mut transcript_verify - ) + .verify(vec![&comm], vec![64], &mut transcript_verify) .is_ok()); // should fail to serialize to pod::RangeProof128 @@ -317,16 +430,12 @@ mod tests { &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![&comm_1, &comm_2, &comm_3], vec![64, 32, 32], &mut transcript_verify, ) diff --git a/zk-token-sdk/src/zk_token_elgamal/pod.rs b/zk-token-sdk/src/zk_token_elgamal/pod.rs index 0d820f8593..29f152cfcd 100644 --- a/zk-token-sdk/src/zk_token_elgamal/pod.rs +++ b/zk-token-sdk/src/zk_token_elgamal/pod.rs @@ -95,6 +95,11 @@ pub struct ZeroBalanceProof(pub [u8; 96]); unsafe impl Zeroable for ZeroBalanceProof {} unsafe impl Pod for ZeroBalanceProof {} +/// Serialization of fee sigma proof +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(transparent)] +pub struct FeeSigmaProof(pub [u8; 256]); + /// Serialization of range proofs for 64-bit numbers (for `Withdraw` instruction) #[derive(Clone, Copy)] #[repr(transparent)] @@ -115,6 +120,16 @@ pub struct RangeProof128(pub [u8; 736]); unsafe impl Zeroable for RangeProof128 {} unsafe impl Pod for RangeProof128 {} +/// Serialization of range proofs for 128-bit numbers (for `TransferRangeProof` instruction) +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct RangeProof256(pub [u8; 800]); + +// `PodRangeProof256` is a Pod and Zeroable. +// Add the marker traits manually because `bytemuck` only adds them for some `u8` arrays +unsafe impl Zeroable for RangeProof256 {} +unsafe impl Pod for RangeProof256 {} + /// Serialization for AeCiphertext #[derive(Clone, Copy, PartialEq)] #[repr(transparent)] @@ -136,3 +151,33 @@ impl Default for AeCiphertext { Self::zeroed() } } + +// TODO: refactor this code into the instruction module +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct TransferPubkeys(pub [u8; 96]); + +unsafe impl Zeroable for TransferPubkeys {} +unsafe impl Pod for TransferPubkeys {} + +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(transparent)] +pub struct TransferWithFeePubkeys(pub [u8; 128]); + +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(transparent)] +pub struct TransferAmountEncryption(pub [u8; 128]); + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct FeeEncryption(pub [u8; 96]); + +unsafe impl Zeroable for FeeEncryption {} +unsafe impl Pod for FeeEncryption {} + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct FeeParameters(pub [u8; 10]); + +unsafe impl Zeroable for FeeParameters {} +unsafe impl Pod for FeeParameters {}