use {
curve25519_dalek::scalar::Scalar,
spl_token_2022::{
error::TokenError,
extension::confidential_transfer::{
ciphertext_extraction::{transfer_amount_source_ciphertext, SourceDecryptHandles},
processor::verify_and_split_deposit_amount,
},
solana_zk_token_sdk::{
encryption::{
auth_encryption::{AeCiphertext, AeKey},
elgamal::{DecryptHandle, ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
grouped_elgamal::GroupedElGamal,
pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
},
instruction::{
transfer::{
combine_lo_hi_commitments, combine_lo_hi_openings, FeeEncryption,
FeeParameters, TransferAmountCiphertext,
},
BatchedGroupedCiphertext2HandlesValidityProofData, BatchedRangeProofU256Data,
CiphertextCommitmentEqualityProofData, FeeSigmaProofData,
},
zk_token_elgamal::ops::subtract_with_lo_hi,
},
},
};
#[allow(clippy::too_many_arguments)]
pub fn transfer_with_fee_split_proof_data(
current_available_balance: &ElGamalCiphertext,
current_decryptable_available_balance: &AeCiphertext,
transfer_amount: u64,
source_elgamal_keypair: &ElGamalKeypair,
aes_key: &AeKey,
destination_elgamal_pubkey: &ElGamalPubkey,
auditor_elgamal_pubkey: Option<&ElGamalPubkey>,
withdraw_withheld_authority_elgamal_pubkey: &ElGamalPubkey,
transfer_fee_parameters: &FeeParameters,
) -> Result<
(
CiphertextCommitmentEqualityProofData,
BatchedGroupedCiphertext2HandlesValidityProofData,
FeeSigmaProofData,
BatchedGroupedCiphertext2HandlesValidityProofData,
BatchedRangeProofU256Data,
SourceDecryptHandles,
),
TokenError,
> {
let default_auditor_pubkey = ElGamalPubkey::default();
let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey);
let (transfer_amount_lo, transfer_amount_hi) =
verify_and_split_deposit_amount(transfer_amount)?;
let (transfer_amount_grouped_ciphertext_lo, transfer_amount_opening_lo) =
TransferAmountCiphertext::new(
transfer_amount_lo,
source_elgamal_keypair.pubkey(),
destination_elgamal_pubkey,
auditor_elgamal_pubkey,
);
let (transfer_amount_grouped_ciphertext_hi, transfer_amount_opening_hi) =
TransferAmountCiphertext::new(
transfer_amount_hi,
source_elgamal_keypair.pubkey(),
destination_elgamal_pubkey,
auditor_elgamal_pubkey,
);
let current_decrypted_available_balance = current_decryptable_available_balance
.decrypt(aes_key)
.ok_or(TokenError::AccountDecryption)?;
let new_decrypted_available_balance = current_decrypted_available_balance
.checked_sub(transfer_amount)
.ok_or(TokenError::InsufficientFunds)?;
let (new_available_balance_commitment, new_source_opening) =
Pedersen::new(new_decrypted_available_balance);
let transfer_amount_source_ciphertext_lo =
transfer_amount_source_ciphertext(&transfer_amount_grouped_ciphertext_lo.into());
let transfer_amount_source_ciphertext_hi =
transfer_amount_source_ciphertext(&transfer_amount_grouped_ciphertext_hi.into());
let current_available_balance = (*current_available_balance).into();
let new_available_balance_ciphertext = subtract_with_lo_hi(
¤t_available_balance,
&transfer_amount_source_ciphertext_lo,
&transfer_amount_source_ciphertext_hi,
)
.ok_or(TokenError::CiphertextArithmeticFailed)?;
let new_available_balance_ciphertext: ElGamalCiphertext = new_available_balance_ciphertext
.try_into()
.map_err(|_| TokenError::MalformedCiphertext)?;
let equality_proof_data = CiphertextCommitmentEqualityProofData::new(
source_elgamal_keypair,
&new_available_balance_ciphertext,
&new_available_balance_commitment,
&new_source_opening,
new_decrypted_available_balance,
)
.map_err(|_| TokenError::ProofGeneration)?;
let source_decrypt_handle_lo =
DecryptHandle::new(source_elgamal_keypair.pubkey(), &transfer_amount_opening_lo);
let source_decrypt_handle_hi =
DecryptHandle::new(source_elgamal_keypair.pubkey(), &transfer_amount_opening_hi);
let source_decrypt_handles = SourceDecryptHandles {
lo: source_decrypt_handle_lo.into(),
hi: source_decrypt_handle_hi.into(),
};
let transfer_amount_destination_auditor_ciphertext_lo = GroupedElGamal::encrypt_with(
[destination_elgamal_pubkey, auditor_elgamal_pubkey],
transfer_amount_lo,
&transfer_amount_opening_lo,
);
let transfer_amount_destination_auditor_ciphertext_hi = GroupedElGamal::encrypt_with(
[destination_elgamal_pubkey, auditor_elgamal_pubkey],
transfer_amount_hi,
&transfer_amount_opening_hi,
);
let transfer_amount_ciphertext_validity_proof_data =
BatchedGroupedCiphertext2HandlesValidityProofData::new(
destination_elgamal_pubkey,
auditor_elgamal_pubkey,
&transfer_amount_destination_auditor_ciphertext_lo,
&transfer_amount_destination_auditor_ciphertext_hi,
transfer_amount_lo,
transfer_amount_hi,
&transfer_amount_opening_lo,
&transfer_amount_opening_hi,
)
.map_err(|_| TokenError::ProofGeneration)?;
let transfer_fee_basis_points = transfer_fee_parameters.fee_rate_basis_points;
let transfer_fee_maximum_fee = transfer_fee_parameters.maximum_fee;
let (raw_fee_amount, delta_fee) =
calculate_raw_fee_and_delta(transfer_amount, transfer_fee_basis_points)
.ok_or(TokenError::Overflow)?;
let fee_amount = std::cmp::min(transfer_fee_maximum_fee, raw_fee_amount);
let (fee_amount_lo, fee_amount_hi) = verify_and_split_deposit_amount(fee_amount)?;
let (fee_ciphertext_lo, fee_opening_lo) = FeeEncryption::new(
fee_amount_lo,
destination_elgamal_pubkey,
withdraw_withheld_authority_elgamal_pubkey,
);
let (fee_ciphertext_hi, fee_opening_hi) = FeeEncryption::new(
fee_amount_hi,
destination_elgamal_pubkey,
withdraw_withheld_authority_elgamal_pubkey,
);
const TRANSFER_AMOUNT_LO_BIT_LENGTH: usize = 16;
let combined_transfer_amount_commitment = combine_lo_hi_commitments(
transfer_amount_grouped_ciphertext_lo.get_commitment(),
transfer_amount_grouped_ciphertext_hi.get_commitment(),
TRANSFER_AMOUNT_LO_BIT_LENGTH,
);
let combined_transfer_amount_opening = combine_lo_hi_openings(
&transfer_amount_opening_lo,
&transfer_amount_opening_hi,
TRANSFER_AMOUNT_LO_BIT_LENGTH,
);
const FEE_AMOUNT_LO_BIT_LENGTH: usize = 16;
let combined_fee_commitment = combine_lo_hi_commitments(
fee_ciphertext_lo.get_commitment(),
fee_ciphertext_hi.get_commitment(),
FEE_AMOUNT_LO_BIT_LENGTH,
);
let combined_fee_opening =
combine_lo_hi_openings(&fee_opening_lo, &fee_opening_hi, FEE_AMOUNT_LO_BIT_LENGTH);
let (claimed_commitment, claimed_opening) = Pedersen::new(delta_fee);
let (delta_commitment, delta_opening) = compute_delta_commitment_and_opening(
(
&combined_transfer_amount_commitment,
&combined_transfer_amount_opening,
),
(&combined_fee_commitment, &combined_fee_opening),
transfer_fee_basis_points,
);
let fee_sigma_proof_data = FeeSigmaProofData::new(
&combined_fee_commitment,
&delta_commitment,
&claimed_commitment,
&combined_fee_opening,
&delta_opening,
&claimed_opening,
fee_amount,
delta_fee,
transfer_fee_maximum_fee,
)
.map_err(|_| TokenError::ProofGeneration)?;
let fee_destination_withdraw_withheld_authority_ciphertext_lo = GroupedElGamal::encrypt_with(
[
destination_elgamal_pubkey,
withdraw_withheld_authority_elgamal_pubkey,
],
fee_amount_lo,
&fee_opening_lo,
);
let fee_destination_withdraw_withheld_authority_ciphertext_hi = GroupedElGamal::encrypt_with(
[
destination_elgamal_pubkey,
withdraw_withheld_authority_elgamal_pubkey,
],
fee_amount_hi,
&fee_opening_hi,
);
let fee_ciphertext_validity_proof_data =
BatchedGroupedCiphertext2HandlesValidityProofData::new(
destination_elgamal_pubkey,
withdraw_withheld_authority_elgamal_pubkey,
&fee_destination_withdraw_withheld_authority_ciphertext_lo,
&fee_destination_withdraw_withheld_authority_ciphertext_hi,
fee_amount_lo,
fee_amount_hi,
&fee_opening_lo,
&fee_opening_hi,
)
.map_err(|_| TokenError::ProofGeneration)?;
const REMAINING_BALANCE_BIT_LENGTH: usize = 64;
const TRANSFER_AMOUNT_HI_BIT_LENGTH: usize = 32;
const DELTA_BIT_LENGTH: usize = 48;
const FEE_AMOUNT_HI_BIT_LENGTH: usize = 32;
const MAX_FEE_BASIS_POINTS: u64 = 10_000;
let delta_fee_complement = MAX_FEE_BASIS_POINTS - delta_fee;
let max_fee_basis_points_commitment =
Pedersen::with(MAX_FEE_BASIS_POINTS, &PedersenOpening::default());
let claimed_complement_commitment = max_fee_basis_points_commitment - claimed_commitment;
let claimed_complement_opening = PedersenOpening::default() - &claimed_opening;
let range_proof_data = BatchedRangeProofU256Data::new(
vec![
&new_available_balance_commitment,
transfer_amount_grouped_ciphertext_lo.get_commitment(),
transfer_amount_grouped_ciphertext_hi.get_commitment(),
&claimed_commitment,
&claimed_complement_commitment,
fee_ciphertext_lo.get_commitment(),
fee_ciphertext_hi.get_commitment(),
],
vec![
new_decrypted_available_balance,
transfer_amount_lo,
transfer_amount_hi,
delta_fee,
delta_fee_complement,
fee_amount_lo,
fee_amount_hi,
],
vec![
REMAINING_BALANCE_BIT_LENGTH,
TRANSFER_AMOUNT_LO_BIT_LENGTH,
TRANSFER_AMOUNT_HI_BIT_LENGTH,
DELTA_BIT_LENGTH,
DELTA_BIT_LENGTH,
FEE_AMOUNT_LO_BIT_LENGTH,
FEE_AMOUNT_HI_BIT_LENGTH,
],
vec![
&new_source_opening,
&transfer_amount_opening_lo,
&transfer_amount_opening_hi,
&claimed_opening,
&claimed_complement_opening,
&fee_opening_lo,
&fee_opening_hi,
],
)
.map_err(|_| TokenError::ProofGeneration)?;
Ok((
equality_proof_data,
transfer_amount_ciphertext_validity_proof_data,
fee_sigma_proof_data,
fee_ciphertext_validity_proof_data,
range_proof_data,
source_decrypt_handles,
))
}
fn calculate_raw_fee_and_delta(
transfer_amount: u64,
fee_rate_basis_points: u16,
) -> Option<(u64, u64)> {
const ONE_IN_BASIS_POINTS: u128 = 10_000_u128;
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))
}
fn compute_delta_commitment_and_opening(
(transfer_amount_commitment, transfer_amount_opening): (&PedersenCommitment, &PedersenOpening),
(fee_commitment, fee_opening): (&PedersenCommitment, &PedersenOpening),
fee_rate_basis_points: u16,
) -> (PedersenCommitment, PedersenOpening) {
const ONE_IN_BASIS_POINTS: u128 = 10_000_u128;
let one_in_basis_points_scalar = Scalar::from(ONE_IN_BASIS_POINTS);
let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
let delta_commitment =
fee_commitment * one_in_basis_points_scalar - transfer_amount_commitment * fee_rate_scalar;
let delta_opening =
fee_opening * one_in_basis_points_scalar - transfer_amount_opening * fee_rate_scalar;
(delta_commitment, delta_opening)
}