use crate::{
instruction::{ProofType, ZkProofData},
zk_token_elgamal::pod,
};
#[cfg(not(target_os = "solana"))]
use {
crate::{
encryption::{
elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey},
pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
},
errors::{ProofGenerationError, ProofVerificationError},
instruction::{
errors::InstructionError,
transfer::{
encryption::{FeeEncryption, TransferAmountCiphertext},
try_combine_lo_hi_ciphertexts, try_combine_lo_hi_commitments,
try_combine_lo_hi_openings, try_combine_lo_hi_u64, try_split_u64, FeeParameters,
Role,
},
},
range_proof::RangeProof,
sigma_proofs::{
batched_grouped_ciphertext_validity_proof::BatchedGroupedCiphertext2HandlesValidityProof,
ciphertext_commitment_equality_proof::CiphertextCommitmentEqualityProof,
fee_proof::FeeSigmaProof,
},
transcript::TranscriptProtocol,
},
bytemuck::bytes_of,
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 MAX_DELTA_RANGE: u64 = MAX_FEE_BASIS_POINTS - 1;
#[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 = 16;
#[cfg(not(target_os = "solana"))]
const FEE_AMOUNT_LO_BITS: usize = 16;
#[cfg(not(target_os = "solana"))]
const FEE_AMOUNT_HI_BITS: usize = 32;
#[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);
pub static ref COMMITMENT_MAX_DELTA_RANGE: PedersenCommitment = Pedersen::encode(MAX_DELTA_RANGE);
}
#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
#[repr(C)]
pub struct TransferWithFeeData {
pub context: TransferWithFeeProofContext,
pub proof: TransferWithFeeProof,
}
#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
#[repr(C)]
pub struct TransferWithFeeProofContext {
pub ciphertext_lo: pod::TransferAmountCiphertext, pub ciphertext_hi: pod::TransferAmountCiphertext, pub transfer_with_fee_pubkeys: TransferWithFeePubkeys, pub new_source_ciphertext: pod::ElGamalCiphertext, pub fee_ciphertext_lo: pod::FeeEncryption, pub fee_ciphertext_hi: pod::FeeEncryption, pub fee_parameters: pod::FeeParameters, }
#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
#[repr(C)]
pub struct TransferWithFeePubkeys {
pub source: pod::ElGamalPubkey,
pub destination: pod::ElGamalPubkey,
pub auditor: pod::ElGamalPubkey,
pub withdraw_withheld_authority: pod::ElGamalPubkey,
}
#[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<Self, ProofGenerationError> {
let (amount_lo, amount_hi) = try_split_u64(transfer_amount, TRANSFER_AMOUNT_LO_BITS)
.map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
let (ciphertext_lo, opening_lo) = TransferAmountCiphertext::new(
amount_lo,
source_keypair.pubkey(),
destination_pubkey,
auditor_pubkey,
);
let (ciphertext_hi, opening_hi) = TransferAmountCiphertext::new(
amount_hi,
source_keypair.pubkey(),
destination_pubkey,
auditor_pubkey,
);
let new_spendable_balance = spendable_balance
.checked_sub(transfer_amount)
.ok_or(ProofGenerationError::NotEnoughFunds)?;
let transfer_amount_lo_source = ElGamalCiphertext {
commitment: *ciphertext_lo.get_commitment(),
handle: *ciphertext_lo.get_source_handle(),
};
let transfer_amount_hi_source = ElGamalCiphertext {
commitment: *ciphertext_hi.get_commitment(),
handle: *ciphertext_hi.get_source_handle(),
};
let new_source_ciphertext = old_source_ciphertext
- try_combine_lo_hi_ciphertexts(
&transfer_amount_lo_source,
&transfer_amount_hi_source,
TRANSFER_AMOUNT_LO_BITS,
)
.map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
let (fee_amount, delta_fee) =
calculate_fee(transfer_amount, fee_parameters.fee_rate_basis_points)
.ok_or(ProofGenerationError::FeeCalculation)?;
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_to_encrypt_lo, fee_to_encrypt_hi) =
try_split_u64(fee_to_encrypt, FEE_AMOUNT_LO_BITS)
.map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
let (fee_ciphertext_lo, opening_fee_lo) = FeeEncryption::new(
fee_to_encrypt_lo,
destination_pubkey,
withdraw_withheld_authority_pubkey,
);
let (fee_ciphertext_hi, opening_fee_hi) = FeeEncryption::new(
fee_to_encrypt_hi,
destination_pubkey,
withdraw_withheld_authority_pubkey,
);
let pod_transfer_with_fee_pubkeys = TransferWithFeePubkeys {
source: (*source_keypair.pubkey()).into(),
destination: (*destination_pubkey).into(),
auditor: (*auditor_pubkey).into(),
withdraw_withheld_authority: (*withdraw_withheld_authority_pubkey).into(),
};
let pod_ciphertext_lo: pod::TransferAmountCiphertext = ciphertext_lo.into();
let pod_ciphertext_hi: pod::TransferAmountCiphertext = ciphertext_hi.into();
let pod_new_source_ciphertext: pod::ElGamalCiphertext = new_source_ciphertext.into();
let pod_fee_ciphertext_lo: pod::FeeEncryption = fee_ciphertext_lo.into();
let pod_fee_ciphertext_hi: pod::FeeEncryption = fee_ciphertext_hi.into();
let context = TransferWithFeeProofContext {
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_lo: pod_fee_ciphertext_lo,
fee_ciphertext_hi: pod_fee_ciphertext_hi,
fee_parameters: fee_parameters.into(),
};
let mut transcript = context.new_transcript();
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_lo, &fee_ciphertext_lo, &opening_fee_lo),
(fee_to_encrypt_hi, &fee_ciphertext_hi, &opening_fee_hi),
delta_fee,
withdraw_withheld_authority_pubkey,
fee_parameters,
&mut transcript,
)?;
Ok(Self { context, proof })
}
fn ciphertext_lo(&self, role: Role) -> Result<ElGamalCiphertext, InstructionError> {
let ciphertext_lo: TransferAmountCiphertext = self
.context
.ciphertext_lo
.try_into()
.map_err(|_| InstructionError::Decryption)?;
let handle_lo = match role {
Role::Source => Some(ciphertext_lo.get_source_handle()),
Role::Destination => Some(ciphertext_lo.get_destination_handle()),
Role::Auditor => Some(ciphertext_lo.get_auditor_handle()),
Role::WithdrawWithheldAuthority => None,
};
if let Some(handle) = handle_lo {
Ok(ElGamalCiphertext {
commitment: *ciphertext_lo.get_commitment(),
handle: *handle,
})
} else {
Err(InstructionError::MissingCiphertext)
}
}
fn ciphertext_hi(&self, role: Role) -> Result<ElGamalCiphertext, InstructionError> {
let ciphertext_hi: TransferAmountCiphertext = self
.context
.ciphertext_hi
.try_into()
.map_err(|_| InstructionError::Decryption)?;
let handle_hi = match role {
Role::Source => Some(ciphertext_hi.get_source_handle()),
Role::Destination => Some(ciphertext_hi.get_destination_handle()),
Role::Auditor => Some(ciphertext_hi.get_auditor_handle()),
Role::WithdrawWithheldAuthority => None,
};
if let Some(handle) = handle_hi {
Ok(ElGamalCiphertext {
commitment: *ciphertext_hi.get_commitment(),
handle: *handle,
})
} else {
Err(InstructionError::MissingCiphertext)
}
}
fn fee_ciphertext_lo(&self, role: Role) -> Result<ElGamalCiphertext, InstructionError> {
let fee_ciphertext_lo: FeeEncryption = self
.context
.fee_ciphertext_lo
.try_into()
.map_err(|_| InstructionError::Decryption)?;
let fee_handle_lo = match role {
Role::Source => None,
Role::Destination => Some(fee_ciphertext_lo.get_destination_handle()),
Role::Auditor => None,
Role::WithdrawWithheldAuthority => {
Some(fee_ciphertext_lo.get_withdraw_withheld_authority_handle())
}
};
if let Some(handle) = fee_handle_lo {
Ok(ElGamalCiphertext {
commitment: *fee_ciphertext_lo.get_commitment(),
handle: *handle,
})
} else {
Err(InstructionError::MissingCiphertext)
}
}
fn fee_ciphertext_hi(&self, role: Role) -> Result<ElGamalCiphertext, InstructionError> {
let fee_ciphertext_hi: FeeEncryption = self
.context
.fee_ciphertext_hi
.try_into()
.map_err(|_| InstructionError::Decryption)?;
let fee_handle_hi = match role {
Role::Source => None,
Role::Destination => Some(fee_ciphertext_hi.get_destination_handle()),
Role::Auditor => None,
Role::WithdrawWithheldAuthority => {
Some(fee_ciphertext_hi.get_withdraw_withheld_authority_handle())
}
};
if let Some(handle) = fee_handle_hi {
Ok(ElGamalCiphertext {
commitment: *fee_ciphertext_hi.get_commitment(),
handle: *handle,
})
} else {
Err(InstructionError::MissingCiphertext)
}
}
pub fn decrypt_amount(
&self,
role: Role,
sk: &ElGamalSecretKey,
) -> Result<u64, InstructionError> {
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 shifted_amount_hi = amount_hi << TRANSFER_AMOUNT_LO_BITS;
Ok(amount_lo + shifted_amount_hi)
} else {
Err(InstructionError::Decryption)
}
}
pub fn decrypt_fee_amount(
&self,
role: Role,
sk: &ElGamalSecretKey,
) -> Result<u64, InstructionError> {
let ciphertext_lo = self.fee_ciphertext_lo(role)?;
let ciphertext_hi = self.fee_ciphertext_hi(role)?;
let fee_amount_lo = ciphertext_lo.decrypt_u32(sk);
let fee_amount_hi = ciphertext_hi.decrypt_u32(sk);
if let (Some(fee_amount_lo), Some(fee_amount_hi)) = (fee_amount_lo, fee_amount_hi) {
let shifted_fee_amount_hi = fee_amount_hi << FEE_AMOUNT_LO_BITS;
Ok(fee_amount_lo + shifted_fee_amount_hi)
} else {
Err(InstructionError::Decryption)
}
}
}
impl ZkProofData<TransferWithFeeProofContext> for TransferWithFeeData {
const PROOF_TYPE: ProofType = ProofType::TransferWithFee;
fn context_data(&self) -> &TransferWithFeeProofContext {
&self.context
}
#[cfg(not(target_os = "solana"))]
fn verify_proof(&self) -> Result<(), ProofVerificationError> {
let mut transcript = self.context.new_transcript();
let source_pubkey = self.context.transfer_with_fee_pubkeys.source.try_into()?;
let destination_pubkey = self
.context
.transfer_with_fee_pubkeys
.destination
.try_into()?;
let auditor_pubkey = self.context.transfer_with_fee_pubkeys.auditor.try_into()?;
let withdraw_withheld_authority_pubkey = self
.context
.transfer_with_fee_pubkeys
.withdraw_withheld_authority
.try_into()?;
let ciphertext_lo = self.context.ciphertext_lo.try_into()?;
let ciphertext_hi = self.context.ciphertext_hi.try_into()?;
let new_source_ciphertext = self.context.new_source_ciphertext.try_into()?;
let fee_ciphertext_lo = self.context.fee_ciphertext_lo.try_into()?;
let fee_ciphertext_hi = self.context.fee_ciphertext_hi.try_into()?;
let fee_parameters = self.context.fee_parameters.into();
self.proof.verify(
&source_pubkey,
&destination_pubkey,
&auditor_pubkey,
&withdraw_withheld_authority_pubkey,
&ciphertext_lo,
&ciphertext_hi,
&new_source_ciphertext,
&fee_ciphertext_lo,
&fee_ciphertext_hi,
fee_parameters,
&mut transcript,
)
}
}
#[allow(non_snake_case)]
#[cfg(not(target_os = "solana"))]
impl TransferWithFeeProofContext {
fn new_transcript(&self) -> Transcript {
let mut transcript = Transcript::new(b"transfer-with-fee-proof");
transcript.append_message(b"ciphertext-lo", bytes_of(&self.ciphertext_lo));
transcript.append_message(b"ciphertext-hi", bytes_of(&self.ciphertext_hi));
transcript.append_message(
b"transfer-with-fee-pubkeys",
bytes_of(&self.transfer_with_fee_pubkeys),
);
transcript.append_message(
b"new-source-ciphertext",
bytes_of(&self.new_source_ciphertext),
);
transcript.append_message(b"fee-ciphertext-lo", bytes_of(&self.fee_ciphertext_lo));
transcript.append_message(b"fee-ciphertext-hi", bytes_of(&self.fee_ciphertext_hi));
transcript.append_message(b"fee-parameters", bytes_of(&self.fee_parameters));
transcript
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
pub struct TransferWithFeeProof {
pub new_source_commitment: pod::PedersenCommitment,
pub claimed_commitment: pod::PedersenCommitment,
pub equality_proof: pod::CiphertextCommitmentEqualityProof,
pub ciphertext_amount_validity_proof: pod::BatchedGroupedCiphertext2HandlesValidityProof,
pub fee_sigma_proof: pod::FeeSigmaProof,
pub fee_ciphertext_validity_proof: pod::BatchedGroupedCiphertext2HandlesValidityProof,
pub range_proof: pod::RangeProofU256,
}
#[allow(non_snake_case)]
#[cfg(not(target_os = "solana"))]
impl TransferWithFeeProof {
#[allow(clippy::too_many_arguments)]
#[allow(clippy::many_single_char_names)]
pub fn new(
transfer_amount_lo_data: (u64, &TransferAmountCiphertext, &PedersenOpening),
transfer_amount_hi_data: (u64, &TransferAmountCiphertext, &PedersenOpening),
source_keypair: &ElGamalKeypair,
(destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey),
(source_new_balance, new_source_ciphertext): (u64, &ElGamalCiphertext),
(fee_amount_lo, fee_ciphertext_lo, opening_fee_lo): (u64, &FeeEncryption, &PedersenOpening),
(fee_amount_hi, fee_ciphertext_hi, opening_fee_hi): (u64, &FeeEncryption, &PedersenOpening),
delta_fee: u64,
withdraw_withheld_authority_pubkey: &ElGamalPubkey,
fee_parameters: FeeParameters,
transcript: &mut Transcript,
) -> Result<Self, ProofGenerationError> {
let (transfer_amount_lo, ciphertext_lo, opening_lo) = transfer_amount_lo_data;
let (transfer_amount_hi, ciphertext_hi, opening_hi) = transfer_amount_hi_data;
let (new_source_commitment, opening_source) = Pedersen::new(source_new_balance);
let pod_new_source_commitment: pod::PedersenCommitment = new_source_commitment.into();
transcript.append_commitment(b"commitment-new-source", &pod_new_source_commitment);
let equality_proof = CiphertextCommitmentEqualityProof::new(
source_keypair,
new_source_ciphertext,
&opening_source,
source_new_balance,
transcript,
);
let ciphertext_amount_validity_proof = BatchedGroupedCiphertext2HandlesValidityProof::new(
(destination_pubkey, auditor_pubkey),
(transfer_amount_lo, transfer_amount_hi),
(opening_lo, opening_hi),
transcript,
);
let (claimed_commitment, opening_claimed) = Pedersen::new(delta_fee);
let pod_claimed_commitment: pod::PedersenCommitment = claimed_commitment.into();
transcript.append_commitment(b"commitment-claimed", &pod_claimed_commitment);
let combined_commitment = try_combine_lo_hi_commitments(
ciphertext_lo.get_commitment(),
ciphertext_hi.get_commitment(),
TRANSFER_AMOUNT_LO_BITS,
)
.map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
let combined_opening =
try_combine_lo_hi_openings(opening_lo, opening_hi, TRANSFER_AMOUNT_LO_BITS)
.map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
let combined_fee_amount =
try_combine_lo_hi_u64(fee_amount_lo, fee_amount_hi, TRANSFER_AMOUNT_LO_BITS)
.map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
let combined_fee_commitment = try_combine_lo_hi_commitments(
fee_ciphertext_lo.get_commitment(),
fee_ciphertext_hi.get_commitment(),
TRANSFER_AMOUNT_LO_BITS,
)
.map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
let combined_fee_opening =
try_combine_lo_hi_openings(opening_fee_lo, opening_fee_hi, TRANSFER_AMOUNT_LO_BITS)
.map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
let (delta_commitment, opening_delta) = compute_delta_commitment_and_opening(
(&combined_commitment, &combined_opening),
(&combined_fee_commitment, &combined_fee_opening),
fee_parameters.fee_rate_basis_points,
);
let pod_delta_commitment: pod::PedersenCommitment = delta_commitment.into();
transcript.append_commitment(b"commitment-delta", &pod_delta_commitment);
let fee_sigma_proof = FeeSigmaProof::new(
(
combined_fee_amount,
&combined_fee_commitment,
&combined_fee_opening,
),
(delta_fee, &delta_commitment, &opening_delta),
(&claimed_commitment, &opening_claimed),
fee_parameters.maximum_fee,
transcript,
);
let fee_ciphertext_validity_proof = BatchedGroupedCiphertext2HandlesValidityProof::new(
(destination_pubkey, withdraw_withheld_authority_pubkey),
(fee_amount_lo, fee_amount_hi),
(opening_fee_lo, opening_fee_hi),
transcript,
);
let opening_claimed_negated = &PedersenOpening::default() - &opening_claimed;
let combined_amount = try_combine_lo_hi_u64(
transfer_amount_lo,
transfer_amount_hi,
TRANSFER_AMOUNT_LO_BITS,
)
.map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
let amount_sub_fee = combined_amount
.checked_sub(combined_fee_amount)
.ok_or(ProofGenerationError::FeeCalculation)?;
let amount_sub_fee_opening = combined_opening - combined_fee_opening;
let delta_negated = MAX_DELTA_RANGE
.checked_sub(delta_fee)
.ok_or(ProofGenerationError::FeeCalculation)?;
let range_proof = RangeProof::new(
vec![
source_new_balance,
transfer_amount_lo,
transfer_amount_hi,
delta_fee,
delta_negated,
fee_amount_lo,
fee_amount_hi,
amount_sub_fee,
],
vec![
TRANSFER_SOURCE_AMOUNT_BITS, TRANSFER_AMOUNT_LO_BITS, TRANSFER_AMOUNT_HI_BITS, TRANSFER_DELTA_BITS, TRANSFER_DELTA_BITS, FEE_AMOUNT_LO_BITS, FEE_AMOUNT_HI_BITS, TRANSFER_SOURCE_AMOUNT_BITS, ],
vec![
&opening_source,
opening_lo,
opening_hi,
&opening_claimed,
&opening_claimed_negated,
opening_fee_lo,
opening_fee_hi,
&amount_sub_fee_opening,
],
transcript,
)?;
Ok(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()
.map_err(|_| ProofGenerationError::ProofLength)?,
})
}
#[allow(clippy::too_many_arguments)]
pub fn verify(
&self,
source_pubkey: &ElGamalPubkey,
destination_pubkey: &ElGamalPubkey,
auditor_pubkey: &ElGamalPubkey,
withdraw_withheld_authority_pubkey: &ElGamalPubkey,
ciphertext_lo: &TransferAmountCiphertext,
ciphertext_hi: &TransferAmountCiphertext,
new_spendable_ciphertext: &ElGamalCiphertext,
fee_ciphertext_lo: &FeeEncryption,
fee_ciphertext_hi: &FeeEncryption,
fee_parameters: FeeParameters,
transcript: &mut Transcript,
) -> Result<(), ProofVerificationError> {
transcript.append_commitment(b"commitment-new-source", &self.new_source_commitment);
let new_source_commitment: PedersenCommitment = self.new_source_commitment.try_into()?;
let claimed_commitment: PedersenCommitment = self.claimed_commitment.try_into()?;
let equality_proof: CiphertextCommitmentEqualityProof = self.equality_proof.try_into()?;
let ciphertext_amount_validity_proof: BatchedGroupedCiphertext2HandlesValidityProof =
self.ciphertext_amount_validity_proof.try_into()?;
let fee_sigma_proof: FeeSigmaProof = self.fee_sigma_proof.try_into()?;
let fee_ciphertext_validity_proof: BatchedGroupedCiphertext2HandlesValidityProof =
self.fee_ciphertext_validity_proof.try_into()?;
let range_proof: RangeProof = self.range_proof.try_into()?;
equality_proof.verify(
source_pubkey,
new_spendable_ciphertext,
&new_source_commitment,
transcript,
)?;
ciphertext_amount_validity_proof.verify(
(destination_pubkey, auditor_pubkey),
(
ciphertext_lo.get_commitment(),
ciphertext_hi.get_commitment(),
),
(
ciphertext_lo.get_destination_handle(),
ciphertext_hi.get_destination_handle(),
),
(
ciphertext_lo.get_auditor_handle(),
ciphertext_hi.get_auditor_handle(),
),
transcript,
)?;
transcript.append_commitment(b"commitment-claimed", &self.claimed_commitment);
let combined_commitment = try_combine_lo_hi_commitments(
ciphertext_lo.get_commitment(),
ciphertext_hi.get_commitment(),
TRANSFER_AMOUNT_LO_BITS,
)
.map_err(|_| ProofVerificationError::IllegalAmountBitLength)?;
let combined_fee_commitment = try_combine_lo_hi_commitments(
fee_ciphertext_lo.get_commitment(),
fee_ciphertext_hi.get_commitment(),
TRANSFER_AMOUNT_LO_BITS,
)
.map_err(|_| ProofVerificationError::IllegalAmountBitLength)?;
let delta_commitment = compute_delta_commitment(
&combined_commitment,
&combined_fee_commitment,
fee_parameters.fee_rate_basis_points,
);
let pod_delta_commitment: pod::PedersenCommitment = delta_commitment.into();
transcript.append_commitment(b"commitment-delta", &pod_delta_commitment);
fee_sigma_proof.verify(
&combined_fee_commitment,
&delta_commitment,
&claimed_commitment,
fee_parameters.maximum_fee,
transcript,
)?;
fee_ciphertext_validity_proof.verify(
(destination_pubkey, withdraw_withheld_authority_pubkey),
(
fee_ciphertext_lo.get_commitment(),
fee_ciphertext_hi.get_commitment(),
),
(
fee_ciphertext_lo.get_destination_handle(),
fee_ciphertext_hi.get_destination_handle(),
),
(
fee_ciphertext_lo.get_withdraw_withheld_authority_handle(),
fee_ciphertext_hi.get_withdraw_withheld_authority_handle(),
),
transcript,
)?;
let new_source_commitment = self.new_source_commitment.try_into()?;
let claimed_commitment_negated = &(*COMMITMENT_MAX_DELTA_RANGE) - &claimed_commitment;
let amount_sub_fee_commitment = combined_commitment - combined_fee_commitment;
range_proof.verify(
vec![
&new_source_commitment,
ciphertext_lo.get_commitment(),
ciphertext_hi.get_commitment(),
&claimed_commitment,
&claimed_commitment_negated,
fee_ciphertext_lo.get_commitment(),
fee_ciphertext_hi.get_commitment(),
&amount_sub_fee_commitment,
],
vec![
TRANSFER_SOURCE_AMOUNT_BITS, TRANSFER_AMOUNT_LO_BITS, TRANSFER_AMOUNT_HI_BITS, TRANSFER_DELTA_BITS, TRANSFER_DELTA_BITS, FEE_AMOUNT_LO_BITS, FEE_AMOUNT_HI_BITS, TRANSFER_SOURCE_AMOUNT_BITS, ],
transcript,
)?;
Ok(())
}
}
#[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)?;
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(
(combined_commitment, combined_opening): (&PedersenCommitment, &PedersenOpening),
(combined_fee_commitment, combined_fee_opening): (&PedersenCommitment, &PedersenOpening),
fee_rate_basis_points: u16,
) -> (PedersenCommitment, PedersenOpening) {
let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
let delta_commitment = combined_fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS)
- combined_commitment * &fee_rate_scalar;
let delta_opening = combined_fee_opening * Scalar::from(MAX_FEE_BASIS_POINTS)
- combined_opening * &fee_rate_scalar;
(delta_commitment, delta_opening)
}
#[cfg(not(target_os = "solana"))]
fn compute_delta_commitment(
combined_commitment: &PedersenCommitment,
combined_fee_commitment: &PedersenCommitment,
fee_rate_basis_points: u16,
) -> PedersenCommitment {
let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
combined_fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS)
- combined_commitment * &fee_rate_scalar
}
#[cfg(test)]
mod test {
use {super::*, bytemuck::Zeroable};
#[test]
fn test_fee_correctness() {
let source_keypair = ElGamalKeypair::new_rand();
let destination_keypair = ElGamalKeypair::new_rand();
let destination_pubkey = destination_keypair.pubkey();
let auditor_keypair = ElGamalKeypair::new_rand();
let auditor_pubkey = auditor_keypair.pubkey();
let withdraw_withheld_authority_keypair = ElGamalKeypair::new_rand();
let withdraw_withheld_authority_pubkey = withdraw_withheld_authority_keypair.pubkey();
let spendable_balance: u64 = 120;
let spendable_ciphertext = source_keypair.pubkey().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_proof().is_ok());
let spendable_balance: u64 = u64::MAX;
let spendable_ciphertext = source_keypair.pubkey().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_proof().is_ok());
let spendable_balance: u64 = 120;
let spendable_ciphertext = source_keypair.pubkey().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_proof().is_ok());
let spendable_balance: u64 = 120;
let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
let transfer_amount: u64 = 0;
let fee_parameters = FeeParameters {
fee_rate_basis_points: 400,
maximum_fee: 3,
};
let destination_pubkey: ElGamalPubkey = pod::ElGamalPubkey::zeroed().try_into().unwrap();
let auditor_keypair = ElGamalKeypair::new_rand();
let auditor_pubkey = auditor_keypair.pubkey();
let withdraw_withheld_authority_keypair = ElGamalKeypair::new_rand();
let withdraw_withheld_authority_pubkey = withdraw_withheld_authority_keypair.pubkey();
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_proof().is_err());
}
}