use { crate::zk_token_elgamal::pod, bytemuck::{Pod, Zeroable}, }; #[cfg(not(target_os = "solana"))] use { crate::{ encryption::{ elgamal::{ DecryptHandle, ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey, }, pedersen::{Pedersen, PedersenCommitment, PedersenOpening}, }, errors::ProofError, instruction::{ combine_lo_hi_ciphertexts, combine_lo_hi_commitments, combine_lo_hi_openings, split_u64, transfer::TransferAmountEncryption, Role, Verifiable, }, range_proof::RangeProof, sigma_proofs::{ equality_proof::CtxtCommEqualityProof, 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_os = "solana"))] const MAX_FEE_BASIS_POINTS: u64 = 10_000; #[cfg(not(target_os = "solana"))] const ONE_IN_BASIS_POINTS: u128 = MAX_FEE_BASIS_POINTS as u128; #[cfg(not(target_os = "solana"))] const TRANSFER_SOURCE_AMOUNT_BITS: usize = 64; #[cfg(not(target_os = "solana"))] const TRANSFER_AMOUNT_LO_BITS: usize = 16; #[cfg(not(target_os = "solana"))] const TRANSFER_AMOUNT_LO_NEGATED_BITS: usize = 16; #[cfg(not(target_os = "solana"))] const TRANSFER_AMOUNT_HI_BITS: usize = 32; #[cfg(not(target_os = "solana"))] const TRANSFER_DELTA_BITS: usize = 64; #[cfg(not(target_os = "solana"))] lazy_static::lazy_static! { pub static ref COMMITMENT_MAX: PedersenCommitment = Pedersen::encode((1_u64 << TRANSFER_AMOUNT_LO_NEGATED_BITS) - 1); pub static ref COMMITMENT_MAX_FEE_BASIS_POINTS: PedersenCommitment = Pedersen::encode(MAX_FEE_BASIS_POINTS); } // #[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 new_source_ciphertext: pod::ElGamalCiphertext, // transfer fee encryption pub fee_ciphertext: pod::FeeEncryption, // fee parameters pub fee_parameters: pod::FeeParameters, // transfer fee proof pub proof: TransferWithFeeProof, } #[cfg(not(target_os = "solana"))] impl TransferWithFeeData { pub fn new( transfer_amount: u64, (spendable_balance, old_source_ciphertext): (u64, &ElGamalCiphertext), source_keypair: &ElGamalKeypair, (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey), fee_parameters: FeeParameters, withdraw_withheld_authority_pubkey: &ElGamalPubkey, ) -> Result { // split and encrypt transfer amount let (amount_lo, amount_hi) = split_u64( transfer_amount, TRANSFER_AMOUNT_LO_BITS, TRANSFER_AMOUNT_HI_BITS, )?; let (ciphertext_lo, opening_lo) = TransferAmountEncryption::new( amount_lo, &source_keypair.public, destination_pubkey, auditor_pubkey, ); let (ciphertext_hi, opening_hi) = TransferAmountEncryption::new( amount_hi, &source_keypair.public, destination_pubkey, auditor_pubkey, ); // 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_handle, }; let transfer_amount_hi_source = ElGamalCiphertext { commitment: ciphertext_hi.commitment, handle: ciphertext_hi.source_handle, }; let new_source_ciphertext = old_source_ciphertext - combine_lo_hi_ciphertexts( &transfer_amount_lo_source, &transfer_amount_hi_source, TRANSFER_AMOUNT_LO_BITS, ); // calculate and encrypt fee let (fee_amount, delta_fee) = calculate_fee(transfer_amount, fee_parameters.fee_rate_basis_points) .ok_or(ProofError::Generation)?; 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); let (fee_ciphertext, opening_fee) = FeeEncryption::new( fee_to_encrypt, destination_pubkey, withdraw_withheld_authority_pubkey, ); // generate transcript and append all public inputs let pod_transfer_with_fee_pubkeys = pod::TransferWithFeePubkeys { source_pubkey: source_keypair.public.into(), destination_pubkey: (*destination_pubkey).into(), auditor_pubkey: (*auditor_pubkey).into(), withdraw_withheld_authority_pubkey: (*withdraw_withheld_authority_pubkey).into(), }; let pod_ciphertext_lo: pod::TransferAmountEncryption = ciphertext_lo.to_pod(); let pod_ciphertext_hi: pod::TransferAmountEncryption = ciphertext_hi.to_pod(); let pod_new_source_ciphertext: pod::ElGamalCiphertext = new_source_ciphertext.into(); let pod_fee_ciphertext: pod::FeeEncryption = fee_ciphertext.to_pod(); let mut transcript = TransferWithFeeProof::transcript_new( &pod_transfer_with_fee_pubkeys, &pod_ciphertext_lo, &pod_ciphertext_hi, &pod_new_source_ciphertext, &pod_fee_ciphertext, ); let proof = TransferWithFeeProof::new( (amount_lo, &ciphertext_lo, &opening_lo), (amount_hi, &ciphertext_hi, &opening_hi), source_keypair, (destination_pubkey, auditor_pubkey), (new_spendable_balance, &new_source_ciphertext), (fee_to_encrypt, &fee_ciphertext, &opening_fee), delta_fee, withdraw_withheld_authority_pubkey, 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, new_source_ciphertext: pod_new_source_ciphertext, fee_ciphertext: pod_fee_ciphertext, 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_handle, Role::Dest => ciphertext_lo.destination_handle, Role::Auditor => ciphertext_lo.auditor_handle, }; 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_handle, Role::Dest => ciphertext_hi.destination_handle, Role::Auditor => ciphertext_hi.auditor_handle, }; Ok(ElGamalCiphertext { commitment: ciphertext_hi.commitment, handle: handle_hi, }) } /// Decrypts transfer amount from transfer-with-fee data 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(sk); let amount_hi = ciphertext_hi.decrypt_u32(sk); if let (Some(amount_lo), Some(amount_hi)) = (amount_lo, amount_hi) { let two_power = 1 << TRANSFER_AMOUNT_LO_BITS; Ok(amount_lo + two_power * amount_hi) } else { Err(ProofError::Verification) } } } #[cfg(not(target_os = "solana"))] 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.new_source_ciphertext, &self.fee_ciphertext, ); let ciphertext_lo = self.ciphertext_lo.try_into()?; let ciphertext_hi = self.ciphertext_hi.try_into()?; let pubkeys_transfer_with_fee = self.transfer_with_fee_pubkeys.try_into()?; let new_source_ciphertext = self.new_source_ciphertext.try_into()?; let fee_ciphertext = self.fee_ciphertext.try_into()?; let fee_parameters = self.fee_parameters.into(); self.proof.verify( &ciphertext_lo, &ciphertext_hi, &pubkeys_transfer_with_fee, &new_source_ciphertext, &fee_ciphertext, fee_parameters, &mut transcript, ) } } // #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] pub struct TransferWithFeeProof { pub new_source_commitment: pod::PedersenCommitment, pub claimed_commitment: pod::PedersenCommitment, pub equality_proof: pod::CtxtCommEqualityProof, pub ciphertext_amount_validity_proof: pod::AggregatedValidityProof, pub fee_sigma_proof: pod::FeeSigmaProof, pub fee_ciphertext_validity_proof: pod::ValidityProof, pub range_proof: pod::RangeProof256, } #[allow(non_snake_case)] #[cfg(not(target_os = "solana"))] impl TransferWithFeeProof { fn transcript_new( transfer_with_fee_pubkeys: &pod::TransferWithFeePubkeys, ciphertext_lo: &pod::TransferAmountEncryption, ciphertext_hi: &pod::TransferAmountEncryption, new_source_ciphertext: &pod::ElGamalCiphertext, fee_ciphertext: &pod::FeeEncryption, ) -> Transcript { let mut transcript = Transcript::new(b"FeeProof"); transcript.append_pubkey(b"pubkey-source", &transfer_with_fee_pubkeys.source_pubkey); transcript.append_pubkey( b"pubkey-dest", &transfer_with_fee_pubkeys.destination_pubkey, ); transcript.append_pubkey(b"pubkey-auditor", &transfer_with_fee_pubkeys.auditor_pubkey); transcript.append_pubkey( b"withdraw_withheld_authority_pubkey", &transfer_with_fee_pubkeys.withdraw_withheld_authority_pubkey, ); transcript.append_commitment(b"comm-lo-amount", &ciphertext_lo.commitment); transcript.append_handle(b"handle-lo-source", &ciphertext_lo.source_handle); transcript.append_handle(b"handle-lo-dest", &ciphertext_lo.destination_handle); transcript.append_handle(b"handle-lo-auditor", &ciphertext_lo.auditor_handle); transcript.append_commitment(b"comm-hi-amount", &ciphertext_hi.commitment); transcript.append_handle(b"handle-hi-source", &ciphertext_hi.source_handle); transcript.append_handle(b"handle-hi-dest", &ciphertext_hi.destination_handle); transcript.append_handle(b"handle-hi-auditor", &ciphertext_hi.auditor_handle); transcript.append_ciphertext(b"ctxt-new-source", new_source_ciphertext); transcript.append_commitment(b"comm-fee", &fee_ciphertext.commitment); transcript.append_handle(b"fee-dest-handle", &fee_ciphertext.destination_handle); transcript.append_handle( b"handle-fee-auditor", &fee_ciphertext.withdraw_withheld_authority_handle, ); transcript } #[allow(clippy::too_many_arguments)] #[allow(clippy::many_single_char_names)] pub fn new( transfer_amount_lo_data: (u64, &TransferAmountEncryption, &PedersenOpening), transfer_amount_hi_data: (u64, &TransferAmountEncryption, &PedersenOpening), source_keypair: &ElGamalKeypair, (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey), (source_new_balance, new_source_ciphertext): (u64, &ElGamalCiphertext), (fee_amount, fee_ciphertext, opening_fee): (u64, &FeeEncryption, &PedersenOpening), delta_fee: u64, withdraw_withheld_authority_pubkey: &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 (new_source_commitment, opening_source) = Pedersen::new(source_new_balance); let (claimed_commitment, opening_claimed) = Pedersen::new(delta_fee); let pod_new_source_commitment: pod::PedersenCommitment = new_source_commitment.into(); let pod_claimed_commitment: pod::PedersenCommitment = claimed_commitment.into(); transcript.append_commitment(b"commitment-new-source", &pod_new_source_commitment); transcript.append_commitment(b"commitment-claimed", &pod_claimed_commitment); // generate equality_proof let equality_proof = CtxtCommEqualityProof::new( source_keypair, new_source_ciphertext, source_new_balance, &opening_source, transcript, ); // generate ciphertext validity proof let ciphertext_amount_validity_proof = AggregatedValidityProof::new( (destination_pubkey, auditor_pubkey), (transfer_amount_lo, transfer_amount_hi), (opening_lo, opening_hi), transcript, ); let (delta_commitment, opening_delta) = compute_delta_commitment_and_opening( (&ciphertext_lo.commitment, opening_lo), (&ciphertext_hi.commitment, opening_hi), (&fee_ciphertext.commitment, opening_fee), fee_parameters.fee_rate_basis_points, ); let fee_sigma_proof = FeeSigmaProof::new( (fee_amount, &fee_ciphertext.commitment, opening_fee), (delta_fee, &delta_commitment, &opening_delta), (&claimed_commitment, &opening_claimed), fee_parameters.maximum_fee, transcript, ); let fee_ciphertext_validity_proof = ValidityProof::new( (destination_pubkey, withdraw_withheld_authority_pubkey), fee_amount, opening_fee, transcript, ); // generate the range proof let opening_claimed_negated = &PedersenOpening::default() - &opening_claimed; let range_proof = if TRANSFER_AMOUNT_LO_BITS == 32 { RangeProof::new( vec![ source_new_balance, transfer_amount_lo, transfer_amount_hi, delta_fee, MAX_FEE_BASIS_POINTS - delta_fee, ], vec![ TRANSFER_SOURCE_AMOUNT_BITS, TRANSFER_AMOUNT_LO_BITS, TRANSFER_AMOUNT_HI_BITS, TRANSFER_DELTA_BITS, TRANSFER_DELTA_BITS, ], vec![ &opening_source, opening_lo, opening_hi, &opening_claimed, &opening_claimed_negated, ], transcript, ) } else { let transfer_amount_lo_negated = ((1 << TRANSFER_AMOUNT_LO_NEGATED_BITS) - 1) - transfer_amount_lo; let opening_lo_negated = &PedersenOpening::default() - opening_lo; RangeProof::new( vec![ source_new_balance, transfer_amount_lo, transfer_amount_lo_negated, transfer_amount_hi, delta_fee, MAX_FEE_BASIS_POINTS - delta_fee, ], vec![ TRANSFER_SOURCE_AMOUNT_BITS, TRANSFER_AMOUNT_LO_BITS, TRANSFER_AMOUNT_LO_NEGATED_BITS, TRANSFER_AMOUNT_HI_BITS, TRANSFER_DELTA_BITS, TRANSFER_DELTA_BITS, ], vec![ &opening_source, opening_lo, &opening_lo_negated, opening_hi, &opening_claimed, &opening_claimed_negated, ], transcript, ) }; Self { new_source_commitment: pod_new_source_commitment, claimed_commitment: pod_claimed_commitment, equality_proof: equality_proof.into(), ciphertext_amount_validity_proof: ciphertext_amount_validity_proof.into(), fee_sigma_proof: fee_sigma_proof.into(), fee_ciphertext_validity_proof: fee_ciphertext_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, fee_ciphertext: &FeeEncryption, fee_parameters: FeeParameters, transcript: &mut Transcript, ) -> Result<(), ProofError> { transcript.append_commitment(b"commitment-new-source", &self.new_source_commitment); transcript.append_commitment(b"commitment-claimed", &self.claimed_commitment); let new_source_commitment: PedersenCommitment = self.new_source_commitment.try_into()?; let claimed_commitment: PedersenCommitment = self.claimed_commitment.try_into()?; let equality_proof: CtxtCommEqualityProof = 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 fee_ciphertext_validity_proof: ValidityProof = self.fee_ciphertext_validity_proof.try_into()?; let range_proof: RangeProof = self.range_proof.try_into()?; // verify equality proof equality_proof.verify( &transfer_with_fee_pubkeys.source_pubkey, new_spendable_ciphertext, &new_source_commitment, transcript, )?; // verify that the transfer amount is encrypted correctly ciphertext_amount_validity_proof.verify( ( &transfer_with_fee_pubkeys.destination_pubkey, &transfer_with_fee_pubkeys.auditor_pubkey, ), (&ciphertext_lo.commitment, &ciphertext_hi.commitment), ( &ciphertext_lo.destination_handle, &ciphertext_hi.destination_handle, ), (&ciphertext_lo.auditor_handle, &ciphertext_hi.auditor_handle), transcript, )?; // verify fee sigma proof let delta_commitment = compute_delta_commitment( &ciphertext_lo.commitment, &ciphertext_hi.commitment, &fee_ciphertext.commitment, fee_parameters.fee_rate_basis_points, ); fee_sigma_proof.verify( &fee_ciphertext.commitment, &delta_commitment, &claimed_commitment, fee_parameters.maximum_fee, transcript, )?; fee_ciphertext_validity_proof.verify( &fee_ciphertext.commitment, ( &transfer_with_fee_pubkeys.destination_pubkey, &transfer_with_fee_pubkeys.withdraw_withheld_authority_pubkey, ), ( &fee_ciphertext.destination_handle, &fee_ciphertext.withdraw_withheld_authority_handle, ), transcript, )?; // verify range proof let new_source_commitment = self.new_source_commitment.try_into()?; let claimed_commitment_negated = &(*COMMITMENT_MAX_FEE_BASIS_POINTS) - &claimed_commitment; if TRANSFER_AMOUNT_LO_BITS == 32 { range_proof.verify( vec![ &new_source_commitment, &ciphertext_lo.commitment, &ciphertext_hi.commitment, &claimed_commitment, &claimed_commitment_negated, ], vec![64, 32, 32, 64, 64], transcript, )?; } else { let commitment_lo_negated = &(*COMMITMENT_MAX) - &ciphertext_lo.commitment; range_proof.verify( vec![ &new_source_commitment, &ciphertext_lo.commitment, &commitment_lo_negated, &ciphertext_hi.commitment, &claimed_commitment, &claimed_commitment_negated, ], vec![64, 16, 16, 32, 64, 64], transcript, )?; } Ok(()) } } /// The ElGamal public keys needed for a transfer with fee #[derive(Clone)] #[repr(C)] #[cfg(not(target_os = "solana"))] pub struct TransferWithFeePubkeys { pub source_pubkey: ElGamalPubkey, pub destination_pubkey: ElGamalPubkey, pub auditor_pubkey: ElGamalPubkey, pub withdraw_withheld_authority_pubkey: ElGamalPubkey, } #[cfg(not(target_os = "solana"))] impl TransferWithFeePubkeys { pub fn to_bytes(&self) -> [u8; 128] { let mut bytes = [0u8; 128]; bytes[..32].copy_from_slice(&self.source_pubkey.to_bytes()); bytes[32..64].copy_from_slice(&self.destination_pubkey.to_bytes()); bytes[64..96].copy_from_slice(&self.auditor_pubkey.to_bytes()); bytes[96..128].copy_from_slice(&self.withdraw_withheld_authority_pubkey.to_bytes()); bytes } pub fn from_bytes(bytes: &[u8]) -> Result { let bytes = array_ref![bytes, 0, 128]; let (source_pubkey, destination_pubkey, auditor_pubkey, withdraw_withheld_authority_pubkey) = array_refs![bytes, 32, 32, 32, 32]; let source_pubkey = ElGamalPubkey::from_bytes(source_pubkey).ok_or(ProofError::Verification)?; let destination_pubkey = ElGamalPubkey::from_bytes(destination_pubkey).ok_or(ProofError::Verification)?; let auditor_pubkey = ElGamalPubkey::from_bytes(auditor_pubkey).ok_or(ProofError::Verification)?; let withdraw_withheld_authority_pubkey = ElGamalPubkey::from_bytes(withdraw_withheld_authority_pubkey) .ok_or(ProofError::Verification)?; Ok(Self { source_pubkey, destination_pubkey, auditor_pubkey, withdraw_withheld_authority_pubkey, }) } } #[derive(Clone)] #[repr(C)] #[cfg(not(target_os = "solana"))] pub struct FeeEncryption { pub commitment: PedersenCommitment, pub destination_handle: DecryptHandle, pub withdraw_withheld_authority_handle: DecryptHandle, } #[cfg(not(target_os = "solana"))] impl FeeEncryption { pub fn new( amount: u64, destination_pubkey: &ElGamalPubkey, withdraw_withheld_authority_pubkey: &ElGamalPubkey, ) -> (Self, PedersenOpening) { let (commitment, opening) = Pedersen::new(amount); let fee_encryption = Self { commitment, destination_handle: destination_pubkey.decrypt_handle(&opening), withdraw_withheld_authority_handle: withdraw_withheld_authority_pubkey .decrypt_handle(&opening), }; (fee_encryption, opening) } pub fn to_pod(&self) -> pod::FeeEncryption { pod::FeeEncryption { commitment: self.commitment.into(), destination_handle: self.destination_handle.into(), withdraw_withheld_authority_handle: self.withdraw_withheld_authority_handle.into(), } } } #[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_os = "solana"))] 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_os = "solana"))] fn calculate_fee(transfer_amount: u64, fee_rate_basis_points: u16) -> Option<(u64, u64)> { let numerator = (transfer_amount as u128).checked_mul(fee_rate_basis_points as u128)?; // Warning: Division may involve CPU opcodes that have variable execution times. This // non-constant-time execution of the fee calculation can theoretically reveal information // about the transfer amount. For transfers that invole extremely sensitive data, additional // care should be put into how the fees are calculated. let fee = numerator .checked_add(ONE_IN_BASIS_POINTS)? .checked_sub(1)? .checked_div(ONE_IN_BASIS_POINTS)?; let delta_fee = fee .checked_mul(ONE_IN_BASIS_POINTS)? .checked_sub(numerator)?; Some((fee as u64, delta_fee as u64)) } #[cfg(not(target_os = "solana"))] fn compute_delta_commitment_and_opening( (commitment_lo, opening_lo): (&PedersenCommitment, &PedersenOpening), (commitment_hi, opening_hi): (&PedersenCommitment, &PedersenOpening), (fee_commitment, opening_fee): (&PedersenCommitment, &PedersenOpening), fee_rate_basis_points: u16, ) -> (PedersenCommitment, PedersenOpening) { let fee_rate_scalar = Scalar::from(fee_rate_basis_points); let delta_commitment = fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS) - &(&combine_lo_hi_commitments(commitment_lo, commitment_hi, TRANSFER_AMOUNT_LO_BITS) * &fee_rate_scalar); let opening_delta = opening_fee * Scalar::from(MAX_FEE_BASIS_POINTS) - &(&combine_lo_hi_openings(opening_lo, opening_hi, TRANSFER_AMOUNT_LO_BITS) * &fee_rate_scalar); (delta_commitment, opening_delta) } #[cfg(not(target_os = "solana"))] fn compute_delta_commitment( commitment_lo: &PedersenCommitment, commitment_hi: &PedersenCommitment, fee_commitment: &PedersenCommitment, fee_rate_basis_points: u16, ) -> PedersenCommitment { let fee_rate_scalar = Scalar::from(fee_rate_basis_points); fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS) - &(&combine_lo_hi_commitments(commitment_lo, commitment_hi, TRANSFER_AMOUNT_LO_BITS) * &fee_rate_scalar) } #[cfg(test)] mod test { use super::*; #[test] fn test_fee_correctness() { let source_keypair = ElGamalKeypair::new_rand(); let destination_pubkey = ElGamalKeypair::new_rand().public; let auditor_pubkey = ElGamalKeypair::new_rand().public; let withdraw_withheld_authority_pubkey = ElGamalKeypair::new_rand().public; // Case 1: transfer 0 amount let spendable_balance: u64 = 120; let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance); let transfer_amount: u64 = 0; let fee_parameters = FeeParameters { fee_rate_basis_points: 400, maximum_fee: 3, }; let fee_data = TransferWithFeeData::new( transfer_amount, (spendable_balance, &spendable_ciphertext), &source_keypair, (&destination_pubkey, &auditor_pubkey), fee_parameters, &withdraw_withheld_authority_pubkey, ) .unwrap(); assert!(fee_data.verify().is_ok()); // Case 2: transfer max amount let spendable_balance: u64 = u64::max_value(); let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance); let transfer_amount: u64 = (1u64 << (TRANSFER_AMOUNT_LO_BITS + TRANSFER_AMOUNT_HI_BITS)) - 1; let fee_parameters = FeeParameters { fee_rate_basis_points: 400, maximum_fee: 3, }; let fee_data = TransferWithFeeData::new( transfer_amount, (spendable_balance, &spendable_ciphertext), &source_keypair, (&destination_pubkey, &auditor_pubkey), fee_parameters, &withdraw_withheld_authority_pubkey, ) .unwrap(); assert!(fee_data.verify().is_ok()); // Case 3: general success case let spendable_balance: u64 = 120; let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance); let transfer_amount: u64 = 100; let fee_parameters = FeeParameters { fee_rate_basis_points: 400, maximum_fee: 3, }; let fee_data = TransferWithFeeData::new( transfer_amount, (spendable_balance, &spendable_ciphertext), &source_keypair, (&destination_pubkey, &auditor_pubkey), fee_parameters, &withdraw_withheld_authority_pubkey, ) .unwrap(); assert!(fee_data.verify().is_ok()); // Case 4: transfer amount too big let spendable_balance: u64 = u64::max_value(); let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance); let transfer_amount: u64 = 1u64 << (TRANSFER_AMOUNT_LO_BITS + TRANSFER_AMOUNT_HI_BITS); let fee_parameters = FeeParameters { fee_rate_basis_points: 400, maximum_fee: 3, }; let fee_data = TransferWithFeeData::new( transfer_amount, (spendable_balance, &spendable_ciphertext), &source_keypair, (&destination_pubkey, &auditor_pubkey), fee_parameters, &withdraw_withheld_authority_pubkey, ); assert!(fee_data.is_err()); // Case 5: invalid destination, auditor, or withdraw authority pubkeys let spendable_balance: u64 = 120; let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance); let transfer_amount: u64 = 0; let fee_parameters = FeeParameters { fee_rate_basis_points: 400, maximum_fee: 3, }; // destination pubkey invalid let destination_pubkey: ElGamalPubkey = pod::ElGamalPubkey::zeroed().try_into().unwrap(); let auditor_pubkey = ElGamalKeypair::new_rand().public; let withdraw_withheld_authority_pubkey = ElGamalKeypair::new_rand().public; let fee_data = TransferWithFeeData::new( transfer_amount, (spendable_balance, &spendable_ciphertext), &source_keypair, (&destination_pubkey, &auditor_pubkey), fee_parameters, &withdraw_withheld_authority_pubkey, ) .unwrap(); assert!(fee_data.verify().is_err()); // auditor pubkey invalid let destination_pubkey: ElGamalPubkey = ElGamalKeypair::new_rand().public; let auditor_pubkey = pod::ElGamalPubkey::zeroed().try_into().unwrap(); let withdraw_withheld_authority_pubkey = ElGamalKeypair::new_rand().public; let fee_data = TransferWithFeeData::new( transfer_amount, (spendable_balance, &spendable_ciphertext), &source_keypair, (&destination_pubkey, &auditor_pubkey), fee_parameters, &withdraw_withheld_authority_pubkey, ) .unwrap(); assert!(fee_data.verify().is_err()); // withdraw authority invalid let destination_pubkey: ElGamalPubkey = ElGamalKeypair::new_rand().public; let auditor_pubkey = ElGamalKeypair::new_rand().public; let withdraw_withheld_authority_pubkey = pod::ElGamalPubkey::zeroed().try_into().unwrap(); let fee_data = TransferWithFeeData::new( transfer_amount, (spendable_balance, &spendable_ciphertext), &source_keypair, (&destination_pubkey, &auditor_pubkey), fee_parameters, &withdraw_withheld_authority_pubkey, ) .unwrap(); assert!(fee_data.verify().is_err()); } }