use crate::{
extension::{
confidential_transfer::*, confidential_transfer_fee::EncryptedFee,
transfer_fee::TransferFee,
},
solana_program::program_error::ProgramError,
solana_zk_token_sdk::{
curve25519::{
ristretto::{self, PodRistrettoPoint},
scalar::PodScalar,
},
instruction::{
transfer::{TransferProofContext, TransferWithFeeProofContext},
BatchedGroupedCiphertext2HandlesValidityProofContext, BatchedRangeProofContext,
CiphertextCommitmentEqualityProofContext, FeeSigmaProofContext,
},
zk_token_elgamal::pod::{
DecryptHandle, FeeEncryption, GroupedElGamalCiphertext2Handles,
GroupedElGamalCiphertext3Handles, PedersenCommitment, TransferAmountCiphertext,
},
},
};
#[cfg(feature = "serde-traits")]
use {
crate::serialization::decrypthandle_fromstr,
serde::{Deserialize, Serialize},
};
pub(crate) fn extract_commitment_from_grouped_ciphertext(
transfer_amount_ciphertext: &GroupedElGamalCiphertext2Handles,
) -> PedersenCommitment {
let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext);
let transfer_amount_commitment_bytes =
transfer_amount_ciphertext_bytes[..32].try_into().unwrap();
PedersenCommitment(transfer_amount_commitment_bytes)
}
pub fn transfer_amount_source_ciphertext(
transfer_amount_ciphertext: &TransferAmountCiphertext,
) -> ElGamalCiphertext {
let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext);
let mut source_ciphertext_bytes = [0u8; 64];
source_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]);
source_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[32..64]);
ElGamalCiphertext(source_ciphertext_bytes)
}
#[cfg(feature = "zk-ops")]
pub(crate) fn transfer_amount_destination_ciphertext(
transfer_amount_ciphertext: &TransferAmountCiphertext,
) -> ElGamalCiphertext {
let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext);
let mut destination_ciphertext_bytes = [0u8; 64];
destination_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]);
destination_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[64..96]);
ElGamalCiphertext(destination_ciphertext_bytes)
}
#[cfg(feature = "zk-ops")]
pub(crate) fn fee_amount_destination_ciphertext(
transfer_amount_ciphertext: &EncryptedFee,
) -> ElGamalCiphertext {
let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext);
let mut source_ciphertext_bytes = [0u8; 64];
source_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]);
source_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[32..64]);
ElGamalCiphertext(source_ciphertext_bytes)
}
#[cfg(feature = "zk-ops")]
pub(crate) fn fee_amount_withdraw_withheld_authority_ciphertext(
transfer_amount_ciphertext: &EncryptedFee,
) -> ElGamalCiphertext {
let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext);
let mut destination_ciphertext_bytes = [0u8; 64];
destination_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]);
destination_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[64..96]);
ElGamalCiphertext(destination_ciphertext_bytes)
}
#[cfg(feature = "zk-ops")]
pub(crate) fn transfer_amount_encryption_from_decrypt_handle(
source_decrypt_handle: &DecryptHandle,
grouped_ciphertext: &GroupedElGamalCiphertext2Handles,
) -> TransferAmountCiphertext {
let source_decrypt_handle_bytes = bytemuck::bytes_of(source_decrypt_handle);
let grouped_ciphertext_bytes = bytemuck::bytes_of(grouped_ciphertext);
let mut transfer_amount_ciphertext_bytes = [0u8; 128];
transfer_amount_ciphertext_bytes[..32].copy_from_slice(&grouped_ciphertext_bytes[..32]);
transfer_amount_ciphertext_bytes[32..64].copy_from_slice(source_decrypt_handle_bytes);
transfer_amount_ciphertext_bytes[64..128].copy_from_slice(&grouped_ciphertext_bytes[32..96]);
TransferAmountCiphertext(GroupedElGamalCiphertext3Handles(
transfer_amount_ciphertext_bytes,
))
}
#[cfg(feature = "zk-ops")]
pub struct TransferPubkeysInfo {
pub source: ElGamalPubkey,
pub destination: ElGamalPubkey,
pub auditor: ElGamalPubkey,
}
#[cfg(feature = "zk-ops")]
pub struct TransferProofContextInfo {
pub ciphertext_lo: TransferAmountCiphertext,
pub ciphertext_hi: TransferAmountCiphertext,
pub transfer_pubkeys: TransferPubkeysInfo,
pub new_source_ciphertext: ElGamalCiphertext,
}
#[cfg(feature = "zk-ops")]
impl From<TransferProofContext> for TransferProofContextInfo {
fn from(context: TransferProofContext) -> Self {
let transfer_pubkeys = TransferPubkeysInfo {
source: context.transfer_pubkeys.source,
destination: context.transfer_pubkeys.destination,
auditor: context.transfer_pubkeys.auditor,
};
TransferProofContextInfo {
ciphertext_lo: context.ciphertext_lo,
ciphertext_hi: context.ciphertext_hi,
transfer_pubkeys,
new_source_ciphertext: context.new_source_ciphertext,
}
}
}
#[cfg(feature = "zk-ops")]
impl TransferProofContextInfo {
pub fn verify_and_extract(
equality_proof_context: &CiphertextCommitmentEqualityProofContext,
ciphertext_validity_proof_context: &BatchedGroupedCiphertext2HandlesValidityProofContext,
range_proof_context: &BatchedRangeProofContext,
source_decrypt_handles: &SourceDecryptHandles,
) -> Result<Self, ProgramError> {
let CiphertextCommitmentEqualityProofContext {
pubkey: source_pubkey,
ciphertext: new_source_ciphertext,
commitment: new_source_commitment,
} = equality_proof_context;
let BatchedGroupedCiphertext2HandlesValidityProofContext {
destination_pubkey,
auditor_pubkey,
grouped_ciphertext_lo: transfer_amount_ciphertext_lo,
grouped_ciphertext_hi: transfer_amount_ciphertext_hi,
} = ciphertext_validity_proof_context;
let BatchedRangeProofContext {
commitments: range_proof_commitments,
bit_lengths: range_proof_bit_lengths,
} = range_proof_context;
let transfer_amount_commitment_lo =
extract_commitment_from_grouped_ciphertext(transfer_amount_ciphertext_lo);
let transfer_amount_commitment_hi =
extract_commitment_from_grouped_ciphertext(transfer_amount_ciphertext_hi);
let expected_commitments = [
*new_source_commitment,
transfer_amount_commitment_lo,
transfer_amount_commitment_hi,
];
if !range_proof_commitments
.iter()
.zip(expected_commitments.iter())
.all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment)
{
return Err(ProgramError::InvalidInstructionData);
}
const REMAINING_BALANCE_BIT_LENGTH: u8 = 64;
const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16;
const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32;
const PADDING_BIT_LENGTH: u8 = 16;
let expected_bit_lengths = [
REMAINING_BALANCE_BIT_LENGTH,
TRANSFER_AMOUNT_LO_BIT_LENGTH,
TRANSFER_AMOUNT_HI_BIT_LENGTH,
PADDING_BIT_LENGTH,
]
.iter();
if !range_proof_bit_lengths
.iter()
.zip(expected_bit_lengths)
.all(|(proof_len, expected_len)| proof_len == expected_len)
{
return Err(ProgramError::InvalidInstructionData);
}
let transfer_pubkeys = TransferPubkeysInfo {
source: *source_pubkey,
destination: *destination_pubkey,
auditor: *auditor_pubkey,
};
let transfer_amount_ciphertext_lo = transfer_amount_encryption_from_decrypt_handle(
&source_decrypt_handles.lo,
transfer_amount_ciphertext_lo,
);
let transfer_amount_ciphertext_hi = transfer_amount_encryption_from_decrypt_handle(
&source_decrypt_handles.hi,
transfer_amount_ciphertext_hi,
);
Ok(Self {
ciphertext_lo: transfer_amount_ciphertext_lo,
ciphertext_hi: transfer_amount_ciphertext_hi,
transfer_pubkeys,
new_source_ciphertext: *new_source_ciphertext,
})
}
}
#[cfg(feature = "zk-ops")]
pub struct TransferWithFeePubkeysInfo {
pub source: ElGamalPubkey,
pub destination: ElGamalPubkey,
pub auditor: ElGamalPubkey,
pub withdraw_withheld_authority: ElGamalPubkey,
}
#[cfg(feature = "zk-ops")]
pub struct TransferWithFeeProofContextInfo {
pub ciphertext_lo: TransferAmountCiphertext,
pub ciphertext_hi: TransferAmountCiphertext,
pub transfer_with_fee_pubkeys: TransferWithFeePubkeysInfo,
pub new_source_ciphertext: ElGamalCiphertext,
pub fee_ciphertext_lo: EncryptedFee,
pub fee_ciphertext_hi: EncryptedFee,
}
#[cfg(feature = "zk-ops")]
impl From<TransferWithFeeProofContext> for TransferWithFeeProofContextInfo {
fn from(context: TransferWithFeeProofContext) -> Self {
let transfer_with_fee_pubkeys = TransferWithFeePubkeysInfo {
source: context.transfer_with_fee_pubkeys.source,
destination: context.transfer_with_fee_pubkeys.destination,
auditor: context.transfer_with_fee_pubkeys.auditor,
withdraw_withheld_authority: context
.transfer_with_fee_pubkeys
.withdraw_withheld_authority,
};
TransferWithFeeProofContextInfo {
ciphertext_lo: context.ciphertext_lo,
ciphertext_hi: context.ciphertext_hi,
transfer_with_fee_pubkeys,
new_source_ciphertext: context.new_source_ciphertext,
fee_ciphertext_lo: context.fee_ciphertext_lo,
fee_ciphertext_hi: context.fee_ciphertext_hi,
}
}
}
#[cfg(feature = "zk-ops")]
impl TransferWithFeeProofContextInfo {
pub fn verify_and_extract(
equality_proof_context: &CiphertextCommitmentEqualityProofContext,
transfer_amount_ciphertext_validity_proof_context: &BatchedGroupedCiphertext2HandlesValidityProofContext,
fee_sigma_proof_context: &FeeSigmaProofContext,
fee_ciphertext_validity_proof_context: &BatchedGroupedCiphertext2HandlesValidityProofContext,
range_proof_context: &BatchedRangeProofContext,
source_decrypt_handles: &SourceDecryptHandles,
fee_parameters: &TransferFee,
) -> Result<Self, ProgramError> {
let CiphertextCommitmentEqualityProofContext {
pubkey: source_pubkey,
ciphertext: new_source_ciphertext,
commitment: new_source_commitment,
} = equality_proof_context;
let BatchedGroupedCiphertext2HandlesValidityProofContext {
destination_pubkey,
auditor_pubkey,
grouped_ciphertext_lo: transfer_amount_ciphertext_lo,
grouped_ciphertext_hi: transfer_amount_ciphertext_hi,
} = transfer_amount_ciphertext_validity_proof_context;
let FeeSigmaProofContext {
fee_commitment,
delta_commitment,
claimed_commitment,
max_fee,
} = fee_sigma_proof_context;
let expected_maximum_fee: u64 = fee_parameters.maximum_fee.into();
let proof_maximum_fee: u64 = (*max_fee).into();
if expected_maximum_fee != proof_maximum_fee {
return Err(ProgramError::InvalidInstructionData);
}
let BatchedGroupedCiphertext2HandlesValidityProofContext {
destination_pubkey: destination_pubkey_from_transfer_fee_validity_proof,
auditor_pubkey: withdraw_withheld_authority_pubkey,
grouped_ciphertext_lo: fee_ciphertext_lo,
grouped_ciphertext_hi: fee_ciphertext_hi,
} = fee_ciphertext_validity_proof_context;
if destination_pubkey != destination_pubkey_from_transfer_fee_validity_proof {
return Err(ProgramError::InvalidInstructionData);
}
let BatchedRangeProofContext {
commitments: range_proof_commitments,
bit_lengths: range_proof_bit_lengths,
} = range_proof_context;
let transfer_amount_commitment_lo =
extract_commitment_from_grouped_ciphertext(transfer_amount_ciphertext_lo);
let transfer_amount_commitment_hi =
extract_commitment_from_grouped_ciphertext(transfer_amount_ciphertext_hi);
let fee_commitment_lo = extract_commitment_from_grouped_ciphertext(fee_ciphertext_lo);
let fee_commitment_hi = extract_commitment_from_grouped_ciphertext(fee_ciphertext_hi);
const MAX_FEE_BASIS_POINTS: u64 = 10_000;
let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS);
let max_fee_basis_points_commitment =
ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &G)
.ok_or(TokenError::CiphertextArithmeticFailed)?;
let claimed_complement_commitment = ristretto::subtract_ristretto(
&max_fee_basis_points_commitment,
&(*claimed_commitment).into(),
)
.ok_or(TokenError::CiphertextArithmeticFailed)?;
let expected_commitments = [
*new_source_commitment,
transfer_amount_commitment_lo,
transfer_amount_commitment_hi,
*claimed_commitment,
claimed_complement_commitment.into(),
fee_commitment_lo,
fee_commitment_hi,
];
if !range_proof_commitments
.iter()
.zip(expected_commitments.iter())
.all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment)
{
return Err(ProgramError::InvalidInstructionData);
}
const REMAINING_BALANCE_BIT_LENGTH: u8 = 64;
const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16;
const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32;
const DELTA_BIT_LENGTH: u8 = 48;
const FEE_AMOUNT_LO_BIT_LENGTH: u8 = 16;
const FEE_AMOUNT_HI_BIT_LENGTH: u8 = 32;
let expected_bit_lengths = [
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,
]
.iter();
if !range_proof_bit_lengths
.iter()
.zip(expected_bit_lengths)
.all(|(proof_len, expected_len)| proof_len == expected_len)
{
return Err(ProgramError::InvalidInstructionData);
}
let sigma_proof_fee_commitment_point: PodRistrettoPoint = (*fee_commitment).into();
let validity_proof_fee_point =
combine_lo_hi_pedersen_points(&fee_commitment_lo.into(), &fee_commitment_hi.into())
.ok_or(TokenError::CiphertextArithmeticFailed)?;
if validity_proof_fee_point != sigma_proof_fee_commitment_point {
return Err(ProgramError::InvalidInstructionData);
}
verify_delta_commitment(
&transfer_amount_commitment_lo,
&transfer_amount_commitment_hi,
fee_commitment,
delta_commitment,
fee_parameters.transfer_fee_basis_points.into(),
)?;
let transfer_with_fee_pubkeys = TransferWithFeePubkeysInfo {
source: *source_pubkey,
destination: *destination_pubkey,
auditor: *auditor_pubkey,
withdraw_withheld_authority: *withdraw_withheld_authority_pubkey,
};
let transfer_amount_ciphertext_lo = transfer_amount_encryption_from_decrypt_handle(
&source_decrypt_handles.lo,
transfer_amount_ciphertext_lo,
);
let transfer_amount_ciphertext_hi = transfer_amount_encryption_from_decrypt_handle(
&source_decrypt_handles.hi,
transfer_amount_ciphertext_hi,
);
Ok(Self {
ciphertext_lo: transfer_amount_ciphertext_lo,
ciphertext_hi: transfer_amount_ciphertext_hi,
transfer_with_fee_pubkeys,
new_source_ciphertext: *new_source_ciphertext,
fee_ciphertext_lo: FeeEncryption(*fee_ciphertext_lo),
fee_ciphertext_hi: FeeEncryption(*fee_ciphertext_hi),
})
}
}
#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
pub struct SourceDecryptHandles {
#[cfg_attr(feature = "serde-traits", serde(with = "decrypthandle_fromstr"))]
pub lo: DecryptHandle,
#[cfg_attr(feature = "serde-traits", serde(with = "decrypthandle_fromstr"))]
pub hi: DecryptHandle,
}
const G: PodRistrettoPoint = PodRistrettoPoint([
226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165,
130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118,
]);
fn u16_to_scalar(amount: u16) -> PodScalar {
let mut bytes = [0u8; 32];
bytes[..2].copy_from_slice(&amount.to_le_bytes());
PodScalar(bytes)
}
fn u64_to_scalar(amount: u64) -> PodScalar {
let mut bytes = [0u8; 32];
bytes[..8].copy_from_slice(&amount.to_le_bytes());
PodScalar(bytes)
}
fn combine_lo_hi_pedersen_points(
point_lo: &PodRistrettoPoint,
point_hi: &PodRistrettoPoint,
) -> Option<PodRistrettoPoint> {
const SCALING_CONSTANT: u64 = 65536;
let scaling_constant_scalar = u64_to_scalar(SCALING_CONSTANT);
let scaled_point_hi = ristretto::multiply_ristretto(&scaling_constant_scalar, point_hi)?;
ristretto::add_ristretto(point_lo, &scaled_point_hi)
}
fn verify_delta_commitment(
transfer_amount_commitment_lo: &PedersenCommitment,
transfer_amount_commitment_hi: &PedersenCommitment,
fee_commitment: &PedersenCommitment,
proof_delta_commitment: &PedersenCommitment,
transfer_fee_basis_points: u16,
) -> Result<(), ProgramError> {
let transfer_amount_point = combine_lo_hi_pedersen_points(
&(*transfer_amount_commitment_lo).into(),
&(*transfer_amount_commitment_hi).into(),
)
.ok_or(TokenError::CiphertextArithmeticFailed)?;
let transfer_fee_basis_points_scalar = u16_to_scalar(transfer_fee_basis_points);
let scaled_transfer_amount_point =
ristretto::multiply_ristretto(&transfer_fee_basis_points_scalar, &transfer_amount_point)
.ok_or(TokenError::CiphertextArithmeticFailed)?;
const MAX_FEE_BASIS_POINTS: u64 = 10_000;
let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS);
let fee_point: PodRistrettoPoint = (*fee_commitment).into();
let scaled_fee_point = ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &fee_point)
.ok_or(TokenError::CiphertextArithmeticFailed)?;
let expected_delta_commitment_point =
ristretto::subtract_ristretto(&scaled_fee_point, &scaled_transfer_amount_point)
.ok_or(TokenError::CiphertextArithmeticFailed)?;
let proof_delta_commitment_point = (*proof_delta_commitment).into();
if expected_delta_commitment_point != proof_delta_commitment_point {
return Err(ProgramError::InvalidInstructionData);
}
Ok(())
}