From a028b1ae0deb552ede1fd43187344b64beae393b Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Sun, 8 Sep 2024 12:38:18 +0900 Subject: [PATCH 1/6] add proof generation logic for confidential mint and burn --- .../proof-generation/src/burn.rs | 146 ++++++++++++++++++ .../proof-generation/src/encryption.rs | 52 +++++++ .../proof-generation/src/lib.rs | 2 + .../proof-generation/src/mint.rs | 88 +++++++++++ 4 files changed, 288 insertions(+) create mode 100644 token/confidential-transfer/proof-generation/src/burn.rs create mode 100644 token/confidential-transfer/proof-generation/src/mint.rs diff --git a/token/confidential-transfer/proof-generation/src/burn.rs b/token/confidential-transfer/proof-generation/src/burn.rs new file mode 100644 index 00000000000..105127030c3 --- /dev/null +++ b/token/confidential-transfer/proof-generation/src/burn.rs @@ -0,0 +1,146 @@ +use { + crate::{ + encryption::BurnAmountCiphertext, errors::TokenProofGenerationError, + try_combine_lo_hi_ciphertexts, try_split_u64, + }, + solana_zk_sdk::{ + encryption::{ + auth_encryption::{AeCiphertext, AeKey}, + elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, + pedersen::Pedersen, + }, + zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCommitmentEqualityProofData, + }, + }, +}; + +const REMAINING_BALANCE_BIT_LENGTH: usize = 64; +const BURN_AMOUNT_LO_BIT_LENGTH: usize = 16; +const BURN_AMOUNT_HI_BIT_LENGTH: usize = 32; +/// The padding bit length in range proofs to make the bit-length power-of-2 +const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16; + +/// The proof data required for a confidential burn instruction +pub struct BurnProofData { + pub equality_proof_data: CiphertextCommitmentEqualityProofData, + pub ciphertext_validity_proof_data: BatchedGroupedCiphertext3HandlesValidityProofData, + pub range_proof_data: BatchedRangeProofU128Data, +} + +pub fn burn_split_proof_data( + current_available_balance_ciphertext: &ElGamalCiphertext, + current_decryptable_available_balance: &AeCiphertext, + burn_amount: u64, + source_elgamal_keypair: &ElGamalKeypair, + aes_key: &AeKey, + auditor_elgamal_pubkey: &ElGamalPubkey, + supply_elgamal_pubkey: &ElGamalPubkey, +) -> Result { + // split the burn amount into low and high bits + let (burn_amount_lo, burn_amount_hi) = try_split_u64(burn_amount, BURN_AMOUNT_LO_BIT_LENGTH) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // encrypt the burn amount under the source and auditor's ElGamal public key + let (burn_amount_ciphertext_lo, burn_amount_opening_lo) = BurnAmountCiphertext::new( + burn_amount_lo, + source_elgamal_keypair.pubkey(), + auditor_elgamal_pubkey, + supply_elgamal_pubkey, + ); + + let (burn_amount_ciphertext_hi, burn_amount_opening_hi) = BurnAmountCiphertext::new( + burn_amount_hi, + source_elgamal_keypair.pubkey(), + auditor_elgamal_pubkey, + supply_elgamal_pubkey, + ); + + // decrypt the current available balance at the source + let current_decrypted_available_balance = current_decryptable_available_balance + .decrypt(aes_key) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // copmute the remaining balance ciphertext + let burn_amount_ciphertext_source_lo = burn_amount_ciphertext_lo + .0 + .to_elgamal_ciphertext(0) + .unwrap(); + let burn_amount_ciphertext_source_hi = burn_amount_ciphertext_hi + .0 + .to_elgamal_ciphertext(0) + .unwrap(); + + #[allow(clippy::arithmetic_side_effects)] + let new_available_balance_ciphertext = current_available_balance_ciphertext + - try_combine_lo_hi_ciphertexts( + &burn_amount_ciphertext_source_lo, + &burn_amount_ciphertext_source_hi, + BURN_AMOUNT_LO_BIT_LENGTH, + ) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // compute the remaining balance at the source + let remaining_balance = current_decrypted_available_balance + .checked_sub(burn_amount) + .ok_or(TokenProofGenerationError::NotEnoughFunds)?; + + let (new_available_balance_commitment, new_available_balance_opening) = + Pedersen::new(remaining_balance); + + // generate equality proof data + let equality_proof_data = CiphertextCommitmentEqualityProofData::new( + source_elgamal_keypair, + &new_available_balance_ciphertext, + &new_available_balance_commitment, + &new_available_balance_opening, + remaining_balance, + ) + .map_err(TokenProofGenerationError::from)?; + + // generate ciphertext validity data + let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new( + source_elgamal_keypair.pubkey(), + auditor_elgamal_pubkey, + supply_elgamal_pubkey, + &burn_amount_ciphertext_lo.0, + &burn_amount_ciphertext_hi.0, + burn_amount_lo, + burn_amount_hi, + &burn_amount_opening_lo, + &burn_amount_opening_hi, + ) + .map_err(TokenProofGenerationError::from)?; + + // generate range proof data + let (padding_commitment, padding_opening) = Pedersen::new(0_u64); + let range_proof_data = BatchedRangeProofU128Data::new( + vec![ + &new_available_balance_commitment, + burn_amount_ciphertext_lo.get_commitment(), + burn_amount_ciphertext_hi.get_commitment(), + &padding_commitment, + ], + vec![remaining_balance, burn_amount_lo, burn_amount_hi, 0], + vec![ + REMAINING_BALANCE_BIT_LENGTH, + BURN_AMOUNT_LO_BIT_LENGTH, + BURN_AMOUNT_HI_BIT_LENGTH, + RANGE_PROOF_PADDING_BIT_LENGTH, + ], + vec![ + &new_available_balance_opening, + &burn_amount_opening_lo, + &burn_amount_opening_hi, + &padding_opening, + ], + ) + .map_err(TokenProofGenerationError::from)?; + + Ok(BurnProofData { + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + }) +} diff --git a/token/confidential-transfer/proof-generation/src/encryption.rs b/token/confidential-transfer/proof-generation/src/encryption.rs index 8583cc7120c..3f3f875aa44 100644 --- a/token/confidential-transfer/proof-generation/src/encryption.rs +++ b/token/confidential-transfer/proof-generation/src/encryption.rs @@ -86,3 +86,55 @@ impl FeeCiphertext { self.0.handles.get(1).unwrap() } } + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct BurnAmountCiphertext(pub(crate) GroupedElGamalCiphertext<3>); + +impl BurnAmountCiphertext { + pub fn new( + amount: u64, + source_pubkey: &ElGamalPubkey, + auditor_pubkey: &ElGamalPubkey, + supply_pubkey: &ElGamalPubkey, + ) -> (Self, PedersenOpening) { + let opening = PedersenOpening::new_rand(); + let grouped_ciphertext = GroupedElGamal::<3>::encrypt_with( + [source_pubkey, auditor_pubkey, supply_pubkey], + amount, + &opening, + ); + + (Self(grouped_ciphertext), opening) + } + + pub fn get_commitment(&self) -> &PedersenCommitment { + &self.0.commitment + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct MintAmountCiphertext(pub(crate) GroupedElGamalCiphertext<3>); + +impl MintAmountCiphertext { + pub fn new( + amount: u64, + source_pubkey: &ElGamalPubkey, + auditor_pubkey: &ElGamalPubkey, + supply_pubkey: &ElGamalPubkey, + ) -> (Self, PedersenOpening) { + let opening = PedersenOpening::new_rand(); + let grouped_ciphertext = GroupedElGamal::<3>::encrypt_with( + [source_pubkey, auditor_pubkey, supply_pubkey], + amount, + &opening, + ); + + (Self(grouped_ciphertext), opening) + } + + pub fn get_commitment(&self) -> &PedersenCommitment { + &self.0.commitment + } +} diff --git a/token/confidential-transfer/proof-generation/src/lib.rs b/token/confidential-transfer/proof-generation/src/lib.rs index ba769a06fb4..f8883f31954 100644 --- a/token/confidential-transfer/proof-generation/src/lib.rs +++ b/token/confidential-transfer/proof-generation/src/lib.rs @@ -6,8 +6,10 @@ use { }, }; +pub mod burn; pub mod encryption; pub mod errors; +pub mod mint; pub mod transfer; pub mod transfer_with_fee; pub mod withdraw; diff --git a/token/confidential-transfer/proof-generation/src/mint.rs b/token/confidential-transfer/proof-generation/src/mint.rs new file mode 100644 index 00000000000..7fab270415e --- /dev/null +++ b/token/confidential-transfer/proof-generation/src/mint.rs @@ -0,0 +1,88 @@ +use { + crate::{encryption::MintAmountCiphertext, errors::TokenProofGenerationError, try_split_u64}, + solana_zk_sdk::{ + encryption::{elgamal::ElGamalPubkey, pedersen::Pedersen}, + zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU64Data, + }, + }, +}; + +const MINT_AMOUNT_LO_BIT_LENGTH: usize = 16; +const MINT_AMOUNT_HI_BIT_LENGTH: usize = 32; +/// The padding bit length in range proofs to make the bit-length power-of-2 +const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16; + +/// The proof data required for a confidential mint instruction +pub struct MintProofData { + pub ciphertext_validity_proof_data: BatchedGroupedCiphertext3HandlesValidityProofData, + pub range_proof_data: BatchedRangeProofU64Data, +} + +pub fn mint_split_proof_data( + mint_amount: u64, + destination_elgamal_pubkey: &ElGamalPubkey, + auditor_elgamal_pubkey: &ElGamalPubkey, + supply_elgamal_pubkey: &ElGamalPubkey, +) -> Result { + // split the mint amount into low and high bits + let (mint_amount_lo, mint_amount_hi) = try_split_u64(mint_amount, MINT_AMOUNT_LO_BIT_LENGTH) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // encrypt the mint amount under the destination and auditor's ElGamal public + // keys + let (mint_amount_grouped_ciphertext_lo, mint_amount_opening_lo) = MintAmountCiphertext::new( + mint_amount_lo, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + supply_elgamal_pubkey, + ); + + let (mint_amount_grouped_ciphertext_hi, mint_amount_opening_hi) = MintAmountCiphertext::new( + mint_amount_hi, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + supply_elgamal_pubkey, + ); + + // generate ciphertext validity proof data + let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new( + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + supply_elgamal_pubkey, + &mint_amount_grouped_ciphertext_lo.0, + &mint_amount_grouped_ciphertext_hi.0, + mint_amount_lo, + mint_amount_hi, + &mint_amount_opening_lo, + &mint_amount_opening_hi, + ) + .map_err(TokenProofGenerationError::from)?; + + // generate range proof data + let (padding_commitment, padding_opening) = Pedersen::new(0_u64); + let range_proof_data = BatchedRangeProofU64Data::new( + vec![ + mint_amount_grouped_ciphertext_lo.get_commitment(), + mint_amount_grouped_ciphertext_hi.get_commitment(), + &padding_commitment, + ], + vec![mint_amount_lo, mint_amount_hi, 0], + vec![ + MINT_AMOUNT_LO_BIT_LENGTH, + MINT_AMOUNT_HI_BIT_LENGTH, + RANGE_PROOF_PADDING_BIT_LENGTH, + ], + vec![ + &mint_amount_opening_lo, + &mint_amount_opening_hi, + &padding_opening, + ], + ) + .map_err(TokenProofGenerationError::from)?; + + Ok(MintProofData { + ciphertext_validity_proof_data, + range_proof_data, + }) +} From 2a39c69e94c8a155766dde8130bf6ee3c9cdf607 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Sun, 8 Sep 2024 12:38:38 +0900 Subject: [PATCH 2/6] add proof extraction logic for confidential mint and burn --- .../proof-extraction/src/burn.rs | 130 ++++++++++++++++++ .../proof-extraction/src/encryption.rs | 8 ++ .../proof-extraction/src/lib.rs | 2 + .../proof-extraction/src/mint.rs | 100 ++++++++++++++ 4 files changed, 240 insertions(+) create mode 100644 token/confidential-transfer/proof-extraction/src/burn.rs create mode 100644 token/confidential-transfer/proof-extraction/src/mint.rs diff --git a/token/confidential-transfer/proof-extraction/src/burn.rs b/token/confidential-transfer/proof-extraction/src/burn.rs new file mode 100644 index 00000000000..1bfc706e369 --- /dev/null +++ b/token/confidential-transfer/proof-extraction/src/burn.rs @@ -0,0 +1,130 @@ +use { + crate::{encryption::PodBurnAmountCiphertext, errors::TokenProofExtractionError}, + solana_zk_sdk::{ + encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext, + CiphertextCommitmentEqualityProofContext, + }, + }, +}; + +/// The public keys associated with a confidential burn +pub struct BurnPubkeys { + pub source: PodElGamalPubkey, + pub auditor: PodElGamalPubkey, + pub supply: PodElGamalPubkey, +} + +/// The proof context information needed to process a confidential burn +/// instruction +pub struct BurnProofContext { + pub burn_amount_ciphertext_lo: PodBurnAmountCiphertext, + pub burn_amount_ciphertext_hi: PodBurnAmountCiphertext, + pub burn_pubkeys: BurnPubkeys, + pub remaining_balance_ciphertext: PodElGamalCiphertext, +} + +impl BurnProofContext { + pub fn verify_and_extract( + equality_proof_context: &CiphertextCommitmentEqualityProofContext, + ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, + range_proof_context: &BatchedRangeProofContext, + ) -> Result { + // The equality proof context consists of the source ElGamal public key, the new + // source available balance ciphertext, and the new source avaialble + // balance commitment. The public key should be checked with ciphertext + // validity proof context for consistency and the commitment should be + // checked with range proof for consistency. The public key and + // the cihpertext should be returned as part of `BurnProofContext`. + let CiphertextCommitmentEqualityProofContext { + pubkey: source_elgamal_pubkey_from_equality_proof, + ciphertext: remaining_balance_ciphertext, + commitment: remaining_balance_commitment, + } = equality_proof_context; + + // The ciphertext validity proof context consists of the source ElGamal public + // key, the auditor ElGamal public key, and the grouped ElGamal + // ciphertexts for the low and high bits of the burn amount. The source + // ElGamal public key should be checked with equality + // proof for consistency and the rest of the data should be returned as part of + // `BurnProofContext`. + let BatchedGroupedCiphertext3HandlesValidityProofContext { + first_pubkey: source_elgamal_pubkey_from_validity_proof, + second_pubkey: auditor_elgamal_pubkey, + third_pubkey: supply_elgamal_pubkey, + grouped_ciphertext_lo: burn_amount_ciphertext_lo, + grouped_ciphertext_hi: burn_amount_ciphertext_hi, + } = ciphertext_validity_proof_context; + + // The range proof context consists of the Pedersen commitments and bit-lengths + // for which the range proof is proved. The commitments must consist of + // three commitments pertaining to the new source available balance, the + // low bits of the burn amount, and high bits of the burn + // amount. These commitments must be checked for bit lengths `64`, `16`, + // and `32`. + let BatchedRangeProofContext { + commitments: range_proof_commitments, + bit_lengths: range_proof_bit_lengths, + } = range_proof_context; + + // check that the source pubkey is consistent between equality and ciphertext + // validity proofs + if source_elgamal_pubkey_from_equality_proof != source_elgamal_pubkey_from_validity_proof { + return Err(TokenProofExtractionError::ElGamalPubkeyMismatch); + } + + // check that the range proof was created for the correct set of Pedersen + // commitments + let burn_amount_commitment_lo = burn_amount_ciphertext_lo.extract_commitment(); + let burn_amount_commitment_hi = burn_amount_ciphertext_hi.extract_commitment(); + + let expected_commitments = [ + *remaining_balance_commitment, + burn_amount_commitment_lo, + burn_amount_commitment_hi, + ]; + + if !range_proof_commitments + .iter() + .zip(expected_commitments.iter()) + .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) + { + return Err(TokenProofExtractionError::PedersenCommitmentMismatch); + } + + // check that the range proof was created for the correct number of bits + const REMAINING_BALANCE_BIT_LENGTH: u8 = 64; + const BURN_AMOUNT_LO_BIT_LENGTH: u8 = 16; + const BURN_AMOUNT_HI_BIT_LENGTH: u8 = 32; + const PADDING_BIT_LENGTH: u8 = 16; + let expected_bit_lengths = [ + REMAINING_BALANCE_BIT_LENGTH, + BURN_AMOUNT_LO_BIT_LENGTH, + BURN_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(TokenProofExtractionError::RangeProofLengthMismatch); + } + + let burn_pubkeys = BurnPubkeys { + source: *source_elgamal_pubkey_from_equality_proof, + auditor: *auditor_elgamal_pubkey, + supply: *supply_elgamal_pubkey, + }; + + Ok(BurnProofContext { + burn_amount_ciphertext_lo: PodBurnAmountCiphertext(*burn_amount_ciphertext_lo), + burn_amount_ciphertext_hi: PodBurnAmountCiphertext(*burn_amount_ciphertext_hi), + burn_pubkeys, + remaining_balance_ciphertext: *remaining_balance_ciphertext, + }) + } +} diff --git a/token/confidential-transfer/proof-extraction/src/encryption.rs b/token/confidential-transfer/proof-extraction/src/encryption.rs index 34ca2fff156..b0a991235f3 100644 --- a/token/confidential-transfer/proof-extraction/src/encryption.rs +++ b/token/confidential-transfer/proof-extraction/src/encryption.rs @@ -46,3 +46,11 @@ impl PodFeeCiphertext { .map_err(|_| TokenProofExtractionError::CiphertextExtraction) } } + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct PodBurnAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct PodMintAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles); diff --git a/token/confidential-transfer/proof-extraction/src/lib.rs b/token/confidential-transfer/proof-extraction/src/lib.rs index 15cee9bdc5d..f3f99bc1072 100644 --- a/token/confidential-transfer/proof-extraction/src/lib.rs +++ b/token/confidential-transfer/proof-extraction/src/lib.rs @@ -1,5 +1,7 @@ +pub mod burn; pub mod encryption; pub mod errors; +pub mod mint; pub mod transfer; pub mod transfer_with_fee; pub mod withdraw; diff --git a/token/confidential-transfer/proof-extraction/src/mint.rs b/token/confidential-transfer/proof-extraction/src/mint.rs new file mode 100644 index 00000000000..c98bdf5b175 --- /dev/null +++ b/token/confidential-transfer/proof-extraction/src/mint.rs @@ -0,0 +1,100 @@ +use { + crate::{encryption::PodMintAmountCiphertext, errors::TokenProofExtractionError}, + solana_zk_sdk::{ + encryption::pod::elgamal::PodElGamalPubkey, + zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext, + }, + }, +}; + +/// The public keys associated with a confidential mint +pub struct MintPubkeys { + pub destination: PodElGamalPubkey, + pub auditor: PodElGamalPubkey, + pub supply: PodElGamalPubkey, +} + +/// The proof context information needed to process a confidential mint +/// instruction +pub struct MintProofContext { + pub mint_amount_ciphertext_lo: PodMintAmountCiphertext, + pub mint_amount_ciphertext_hi: PodMintAmountCiphertext, + pub mint_pubkeys: MintPubkeys, +} + +impl MintProofContext { + pub fn verify_and_extract( + ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, + range_proof_context: &BatchedRangeProofContext, + ) -> Result { + // The ciphertext validity proof context consists of the destination ElGamal + // public key, the auditor ElGamal public key, and the grouped ElGamal + // ciphertexts for the low and high bits of the burn amount. These + // fields should be returned as part of `MintProofContext`. + let BatchedGroupedCiphertext3HandlesValidityProofContext { + first_pubkey: destination_elgamal_pubkey, + second_pubkey: auditor_elgamal_pubkey, + third_pubkey: supply_elgamal_pubkey, + grouped_ciphertext_lo: mint_amount_ciphertext_lo, + grouped_ciphertext_hi: mint_amount_ciphertext_hi, + } = ciphertext_validity_proof_context; + + // The range proof context consists of the Pedersen commitments and bit-lengths + // for which the range proof is proved. The commitments must consist of + // two commitments pertaining to the the + // low bits of the mint amount, and high bits of the mint + // amount. These commitments must be checked for bit lengths `16` and + // and `32`. + let BatchedRangeProofContext { + commitments: range_proof_commitments, + bit_lengths: range_proof_bit_lengths, + } = range_proof_context; + + // check that the range proof was created for the correct set of Pedersen + // commitments + let mint_amount_commitment_lo = mint_amount_ciphertext_lo.extract_commitment(); + let mint_amount_commitment_hi = mint_amount_ciphertext_hi.extract_commitment(); + + let expected_commitments = [mint_amount_commitment_lo, mint_amount_commitment_hi]; + + if !range_proof_commitments + .iter() + .zip(expected_commitments.iter()) + .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) + { + return Err(TokenProofExtractionError::PedersenCommitmentMismatch); + } + + // check that the range proof was created for the correct number of bits + const MINT_AMOUNT_LO_BIT_LENGTH: u8 = 16; + const MINT_AMOUNT_HI_BIT_LENGTH: u8 = 32; + const PADDING_BIT_LENGTH: u8 = 16; + let expected_bit_lengths = [ + MINT_AMOUNT_LO_BIT_LENGTH, + MINT_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(TokenProofExtractionError::RangeProofLengthMismatch); + } + + let mint_pubkeys = MintPubkeys { + destination: *destination_elgamal_pubkey, + auditor: *auditor_elgamal_pubkey, + supply: *supply_elgamal_pubkey, + }; + + Ok(MintProofContext { + mint_amount_ciphertext_lo: PodMintAmountCiphertext(*mint_amount_ciphertext_lo), + mint_amount_ciphertext_hi: PodMintAmountCiphertext(*mint_amount_ciphertext_hi), + mint_pubkeys, + }) + } +} From b92324d0902895f93508922939224583d701cd10 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Sun, 8 Sep 2024 12:38:45 +0900 Subject: [PATCH 3/6] add tests for confidential mint and burn --- .../proof-tests/tests/proof_test.rs | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/token/confidential-transfer/proof-tests/tests/proof_test.rs b/token/confidential-transfer/proof-tests/tests/proof_test.rs index 0796cbb9a6a..4c09d29e90e 100644 --- a/token/confidential-transfer/proof-tests/tests/proof_test.rs +++ b/token/confidential-transfer/proof-tests/tests/proof_test.rs @@ -4,10 +4,12 @@ use { zk_elgamal_proof_program::proof_data::ZkProofData, }, spl_token_confidential_transfer_proof_extraction::{ - transfer::TransferProofContext, transfer_with_fee::TransferWithFeeProofContext, - withdraw::WithdrawProofContext, + burn::BurnProofContext, mint::MintProofContext, transfer::TransferProofContext, + transfer_with_fee::TransferWithFeeProofContext, withdraw::WithdrawProofContext, }, spl_token_confidential_transfer_proof_generation::{ + burn::{burn_split_proof_data, BurnProofData}, + mint::{mint_split_proof_data, MintProofData}, transfer::{transfer_split_proof_data, TransferProofData}, transfer_with_fee::{transfer_with_fee_split_proof_data, TransferWithFeeProofData}, withdraw::{withdraw_proof_data, WithdrawProofData}, @@ -182,3 +184,92 @@ fn test_withdraw_validity(spendable_balance: u64, withdraw_amount: u64) { ) .unwrap(); } + +#[test] +fn test_mint_proof_correctness() { + test_mint_validity(0); + test_mint_validity(1); + test_mint_validity(65535); + test_mint_validity(65536); + test_mint_validity(281474976710655); +} + +fn test_mint_validity(mint_amount: u64) { + 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 supply_keypair = ElGamalKeypair::new_rand(); + let supply_pubkey = supply_keypair.pubkey(); + + let MintProofData { + ciphertext_validity_proof_data, + range_proof_data, + } = mint_split_proof_data( + mint_amount, + destination_pubkey, + auditor_pubkey, + supply_pubkey, + ) + .unwrap(); + + ciphertext_validity_proof_data.verify_proof().unwrap(); + range_proof_data.verify_proof().unwrap(); + + MintProofContext::verify_and_extract( + ciphertext_validity_proof_data.context_data(), + range_proof_data.context_data(), + ) + .unwrap(); +} + +#[test] +fn test_burn_proof_correctness() { + test_burn_validity(0, 0); + test_burn_validity(77, 55); + test_burn_validity(65535, 65535); + test_burn_validity(65536, 65536); + test_burn_validity(281474976710655, 281474976710655); +} + +fn test_burn_validity(spendable_balance: u64, burn_amount: u64) { + let source_keypair = ElGamalKeypair::new_rand(); + let aes_key = AeKey::new_rand(); + + let auditor_keypair = ElGamalKeypair::new_rand(); + let auditor_pubkey = auditor_keypair.pubkey(); + + let supply_keypair = ElGamalKeypair::new_rand(); + let supply_pubkey = supply_keypair.pubkey(); + + let spendable_balance_ciphertext = source_keypair.pubkey().encrypt(spendable_balance); + let decryptable_balance = aes_key.encrypt(spendable_balance); + + let BurnProofData { + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + } = burn_split_proof_data( + &spendable_balance_ciphertext, + &decryptable_balance, + burn_amount, + &source_keypair, + &aes_key, + auditor_pubkey, + supply_pubkey, + ) + .unwrap(); + + equality_proof_data.verify_proof().unwrap(); + ciphertext_validity_proof_data.verify_proof().unwrap(); + range_proof_data.verify_proof().unwrap(); + + BurnProofContext::verify_and_extract( + equality_proof_data.context_data(), + ciphertext_validity_proof_data.context_data(), + range_proof_data.context_data(), + ) + .unwrap(); +} From ec462441c923de93882b039f1403b478f76c12da Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 10 Sep 2024 12:22:49 +0900 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Jon C --- token/confidential-transfer/proof-extraction/src/mint.rs | 2 +- token/confidential-transfer/proof-generation/src/burn.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/token/confidential-transfer/proof-extraction/src/mint.rs b/token/confidential-transfer/proof-extraction/src/mint.rs index c98bdf5b175..887789d07a6 100644 --- a/token/confidential-transfer/proof-extraction/src/mint.rs +++ b/token/confidential-transfer/proof-extraction/src/mint.rs @@ -30,7 +30,7 @@ impl MintProofContext { ) -> Result { // The ciphertext validity proof context consists of the destination ElGamal // public key, the auditor ElGamal public key, and the grouped ElGamal - // ciphertexts for the low and high bits of the burn amount. These + // ciphertexts for the low and high bits of the mint amount. These // fields should be returned as part of `MintProofContext`. let BatchedGroupedCiphertext3HandlesValidityProofContext { first_pubkey: destination_elgamal_pubkey, diff --git a/token/confidential-transfer/proof-generation/src/burn.rs b/token/confidential-transfer/proof-generation/src/burn.rs index 105127030c3..872d7c4cd41 100644 --- a/token/confidential-transfer/proof-generation/src/burn.rs +++ b/token/confidential-transfer/proof-generation/src/burn.rs @@ -62,7 +62,7 @@ pub fn burn_split_proof_data( .decrypt(aes_key) .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - // copmute the remaining balance ciphertext + // compute the remaining balance ciphertext let burn_amount_ciphertext_source_lo = burn_amount_ciphertext_lo .0 .to_elgamal_ciphertext(0) From c041e4cf6c67f1989c22b6e6e0c723cbad70c2ed Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Wed, 11 Sep 2024 12:38:12 +0900 Subject: [PATCH 5/6] add equality and range proof to confidential mint proof --- .../proof-extraction/src/mint.rs | 38 ++++++++- .../proof-generation/src/mint.rs | 78 ++++++++++++++++--- .../proof-tests/tests/proof_test.rs | 37 +++++++-- 3 files changed, 131 insertions(+), 22 deletions(-) diff --git a/token/confidential-transfer/proof-extraction/src/mint.rs b/token/confidential-transfer/proof-extraction/src/mint.rs index 887789d07a6..cffe8616dc6 100644 --- a/token/confidential-transfer/proof-extraction/src/mint.rs +++ b/token/confidential-transfer/proof-extraction/src/mint.rs @@ -1,9 +1,10 @@ use { crate::{encryption::PodMintAmountCiphertext, errors::TokenProofExtractionError}, solana_zk_sdk::{ - encryption::pod::elgamal::PodElGamalPubkey, + encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, zk_elgamal_proof_program::proof_data::{ BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext, + CiphertextCommitmentEqualityProofContext, }, }, }; @@ -21,13 +22,27 @@ pub struct MintProofContext { pub mint_amount_ciphertext_lo: PodMintAmountCiphertext, pub mint_amount_ciphertext_hi: PodMintAmountCiphertext, pub mint_pubkeys: MintPubkeys, + pub new_supply_ciphertext: PodElGamalCiphertext, } impl MintProofContext { pub fn verify_and_extract( + equality_proof_context: &CiphertextCommitmentEqualityProofContext, ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, range_proof_context: &BatchedRangeProofContext, ) -> Result { + // The equality proof context consists of the supply ElGamal public key, the new + // supply ciphertext, and the new supply commitment. The supply ElGamal + // public key should be checked with ciphertext validity proof for + // consistency and the new supply commitment should be checked with + // range proof for consistency. The new supply ciphertext should be + // returned as part of `MintProofContext`. + let CiphertextCommitmentEqualityProofContext { + pubkey: supply_elgamal_pubkey_from_equality_proof, + ciphertext: new_supply_ciphertext, + commitment: new_supply_commitment, + } = equality_proof_context; + // The ciphertext validity proof context consists of the destination ElGamal // public key, the auditor ElGamal public key, and the grouped ElGamal // ciphertexts for the low and high bits of the mint amount. These @@ -35,7 +50,7 @@ impl MintProofContext { let BatchedGroupedCiphertext3HandlesValidityProofContext { first_pubkey: destination_elgamal_pubkey, second_pubkey: auditor_elgamal_pubkey, - third_pubkey: supply_elgamal_pubkey, + third_pubkey: supply_elgamal_pubkey_from_ciphertext_validity_proof, grouped_ciphertext_lo: mint_amount_ciphertext_lo, grouped_ciphertext_hi: mint_amount_ciphertext_hi, } = ciphertext_validity_proof_context; @@ -51,12 +66,24 @@ impl MintProofContext { bit_lengths: range_proof_bit_lengths, } = range_proof_context; + // check that the supply pubkey is consistent between equality and ciphertext + // validity proofs + if supply_elgamal_pubkey_from_equality_proof + != supply_elgamal_pubkey_from_ciphertext_validity_proof + { + return Err(TokenProofExtractionError::ElGamalPubkeyMismatch); + } + // check that the range proof was created for the correct set of Pedersen // commitments let mint_amount_commitment_lo = mint_amount_ciphertext_lo.extract_commitment(); let mint_amount_commitment_hi = mint_amount_ciphertext_hi.extract_commitment(); - let expected_commitments = [mint_amount_commitment_lo, mint_amount_commitment_hi]; + let expected_commitments = [ + *new_supply_commitment, + mint_amount_commitment_lo, + mint_amount_commitment_hi, + ]; if !range_proof_commitments .iter() @@ -67,10 +94,12 @@ impl MintProofContext { } // check that the range proof was created for the correct number of bits + const NEW_SUPPLY_BIT_LENGTH: u8 = 64; const MINT_AMOUNT_LO_BIT_LENGTH: u8 = 16; const MINT_AMOUNT_HI_BIT_LENGTH: u8 = 32; const PADDING_BIT_LENGTH: u8 = 16; let expected_bit_lengths = [ + NEW_SUPPLY_BIT_LENGTH, MINT_AMOUNT_LO_BIT_LENGTH, MINT_AMOUNT_HI_BIT_LENGTH, PADDING_BIT_LENGTH, @@ -88,13 +117,14 @@ impl MintProofContext { let mint_pubkeys = MintPubkeys { destination: *destination_elgamal_pubkey, auditor: *auditor_elgamal_pubkey, - supply: *supply_elgamal_pubkey, + supply: *supply_elgamal_pubkey_from_equality_proof, }; Ok(MintProofContext { mint_amount_ciphertext_lo: PodMintAmountCiphertext(*mint_amount_ciphertext_lo), mint_amount_ciphertext_hi: PodMintAmountCiphertext(*mint_amount_ciphertext_hi), mint_pubkeys, + new_supply_ciphertext: *new_supply_ciphertext, }) } } diff --git a/token/confidential-transfer/proof-generation/src/mint.rs b/token/confidential-transfer/proof-generation/src/mint.rs index 7fab270415e..e25dc70a210 100644 --- a/token/confidential-transfer/proof-generation/src/mint.rs +++ b/token/confidential-transfer/proof-generation/src/mint.rs @@ -1,13 +1,22 @@ use { - crate::{encryption::MintAmountCiphertext, errors::TokenProofGenerationError, try_split_u64}, + crate::{ + encryption::MintAmountCiphertext, errors::TokenProofGenerationError, + try_combine_lo_hi_ciphertexts, try_split_u64, + }, solana_zk_sdk::{ - encryption::{elgamal::ElGamalPubkey, pedersen::Pedersen}, + encryption::{ + auth_encryption::{AeCiphertext, AeKey}, + elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, + pedersen::Pedersen, + }, zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU64Data, + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCommitmentEqualityProofData, }, }, }; +const NEW_SUPPLY_BIT_LENGTH: usize = 64; const MINT_AMOUNT_LO_BIT_LENGTH: usize = 16; const MINT_AMOUNT_HI_BIT_LENGTH: usize = 32; /// The padding bit length in range proofs to make the bit-length power-of-2 @@ -15,15 +24,19 @@ const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16; /// The proof data required for a confidential mint instruction pub struct MintProofData { + pub equality_proof_data: CiphertextCommitmentEqualityProofData, pub ciphertext_validity_proof_data: BatchedGroupedCiphertext3HandlesValidityProofData, - pub range_proof_data: BatchedRangeProofU64Data, + pub range_proof_data: BatchedRangeProofU128Data, } pub fn mint_split_proof_data( + current_supply_ciphertext: &ElGamalCiphertext, + current_decryptable_supply: &AeCiphertext, mint_amount: u64, + supply_elgamal_keypair: &ElGamalKeypair, + supply_aes_key: &AeKey, destination_elgamal_pubkey: &ElGamalPubkey, auditor_elgamal_pubkey: &ElGamalPubkey, - supply_elgamal_pubkey: &ElGamalPubkey, ) -> Result { // split the mint amount into low and high bits let (mint_amount_lo, mint_amount_hi) = try_split_u64(mint_amount, MINT_AMOUNT_LO_BIT_LENGTH) @@ -35,21 +48,62 @@ pub fn mint_split_proof_data( mint_amount_lo, destination_elgamal_pubkey, auditor_elgamal_pubkey, - supply_elgamal_pubkey, + supply_elgamal_keypair.pubkey(), ); let (mint_amount_grouped_ciphertext_hi, mint_amount_opening_hi) = MintAmountCiphertext::new( mint_amount_hi, destination_elgamal_pubkey, auditor_elgamal_pubkey, - supply_elgamal_pubkey, + supply_elgamal_keypair.pubkey(), ); + // compute the new supply ciphertext + let mint_amount_ciphertext_supply_lo = mint_amount_grouped_ciphertext_lo + .0 + .to_elgamal_ciphertext(2) + .unwrap(); + let mint_amount_ciphertext_supply_hi = mint_amount_grouped_ciphertext_hi + .0 + .to_elgamal_ciphertext(2) + .unwrap(); + + #[allow(clippy::arithmetic_side_effects)] + let new_supply_ciphertext = current_supply_ciphertext + + try_combine_lo_hi_ciphertexts( + &mint_amount_ciphertext_supply_lo, + &mint_amount_ciphertext_supply_hi, + MINT_AMOUNT_LO_BIT_LENGTH, + ) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // decrypt the current supply + let current_supply = current_decryptable_supply + .decrypt(supply_aes_key) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // compute the new supply + let new_supply = current_supply + .checked_add(mint_amount) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + let (new_supply_commitment, new_supply_opening) = Pedersen::new(new_supply); + + // generate equality proof data + let equality_proof_data = CiphertextCommitmentEqualityProofData::new( + supply_elgamal_keypair, + &new_supply_ciphertext, + &new_supply_commitment, + &new_supply_opening, + new_supply, + ) + .map_err(TokenProofGenerationError::from)?; + // generate ciphertext validity proof data let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new( destination_elgamal_pubkey, auditor_elgamal_pubkey, - supply_elgamal_pubkey, + supply_elgamal_keypair.pubkey(), &mint_amount_grouped_ciphertext_lo.0, &mint_amount_grouped_ciphertext_hi.0, mint_amount_lo, @@ -61,19 +115,22 @@ pub fn mint_split_proof_data( // generate range proof data let (padding_commitment, padding_opening) = Pedersen::new(0_u64); - let range_proof_data = BatchedRangeProofU64Data::new( + let range_proof_data = BatchedRangeProofU128Data::new( vec![ + &new_supply_commitment, mint_amount_grouped_ciphertext_lo.get_commitment(), mint_amount_grouped_ciphertext_hi.get_commitment(), &padding_commitment, ], - vec![mint_amount_lo, mint_amount_hi, 0], + vec![new_supply, mint_amount_lo, mint_amount_hi, 0], vec![ + NEW_SUPPLY_BIT_LENGTH, MINT_AMOUNT_LO_BIT_LENGTH, MINT_AMOUNT_HI_BIT_LENGTH, RANGE_PROOF_PADDING_BIT_LENGTH, ], vec![ + &new_supply_opening, &mint_amount_opening_lo, &mint_amount_opening_hi, &padding_opening, @@ -82,6 +139,7 @@ pub fn mint_split_proof_data( .map_err(TokenProofGenerationError::from)?; Ok(MintProofData { + equality_proof_data, ciphertext_validity_proof_data, range_proof_data, }) diff --git a/token/confidential-transfer/proof-tests/tests/proof_test.rs b/token/confidential-transfer/proof-tests/tests/proof_test.rs index 4c09d29e90e..f4c3a7f3a9e 100644 --- a/token/confidential-transfer/proof-tests/tests/proof_test.rs +++ b/token/confidential-transfer/proof-tests/tests/proof_test.rs @@ -187,14 +187,26 @@ fn test_withdraw_validity(spendable_balance: u64, withdraw_amount: u64) { #[test] fn test_mint_proof_correctness() { - test_mint_validity(0); - test_mint_validity(1); - test_mint_validity(65535); - test_mint_validity(65536); - test_mint_validity(281474976710655); + test_mint_validity(0, 0); + test_mint_validity(1, 0); + test_mint_validity(65535, 0); + test_mint_validity(65536, 0); + test_mint_validity(281474976710655, 0); + + test_mint_validity(0, 65535); + test_mint_validity(1, 65535); + test_mint_validity(65535, 65535); + test_mint_validity(65536, 65535); + test_mint_validity(281474976710655, 65535); + + test_mint_validity(0, 281474976710655); + test_mint_validity(1, 281474976710655); + test_mint_validity(65535, 281474976710655); + test_mint_validity(65536, 281474976710655); + test_mint_validity(281474976710655, 281474976710655); } -fn test_mint_validity(mint_amount: u64) { +fn test_mint_validity(mint_amount: u64, supply: u64) { let destination_keypair = ElGamalKeypair::new_rand(); let destination_pubkey = destination_keypair.pubkey(); @@ -202,23 +214,32 @@ fn test_mint_validity(mint_amount: u64) { let auditor_pubkey = auditor_keypair.pubkey(); let supply_keypair = ElGamalKeypair::new_rand(); - let supply_pubkey = supply_keypair.pubkey(); + let supply_aes_key = AeKey::new_rand(); + + let supply_ciphertext = supply_keypair.pubkey().encrypt(supply); + let decryptable_supply = supply_aes_key.encrypt(supply); let MintProofData { + equality_proof_data, ciphertext_validity_proof_data, range_proof_data, } = mint_split_proof_data( + &supply_ciphertext, + &decryptable_supply, mint_amount, + &supply_keypair, + &supply_aes_key, destination_pubkey, auditor_pubkey, - supply_pubkey, ) .unwrap(); + equality_proof_data.verify_proof().unwrap(); ciphertext_validity_proof_data.verify_proof().unwrap(); range_proof_data.verify_proof().unwrap(); MintProofContext::verify_and_extract( + equality_proof_data.context_data(), ciphertext_validity_proof_data.context_data(), range_proof_data.context_data(), ) From 8bb645a393969ffb6fd7cb1349acb8358d11d81a Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Wed, 11 Sep 2024 12:39:03 +0900 Subject: [PATCH 6/6] rename `aes_key` to `source_aes_key` in confidential burn --- token/confidential-transfer/proof-generation/src/burn.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token/confidential-transfer/proof-generation/src/burn.rs b/token/confidential-transfer/proof-generation/src/burn.rs index 872d7c4cd41..9b927384ac8 100644 --- a/token/confidential-transfer/proof-generation/src/burn.rs +++ b/token/confidential-transfer/proof-generation/src/burn.rs @@ -34,7 +34,7 @@ pub fn burn_split_proof_data( current_decryptable_available_balance: &AeCiphertext, burn_amount: u64, source_elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, + source_aes_key: &AeKey, auditor_elgamal_pubkey: &ElGamalPubkey, supply_elgamal_pubkey: &ElGamalPubkey, ) -> Result { @@ -59,7 +59,7 @@ pub fn burn_split_proof_data( // decrypt the current available balance at the source let current_decrypted_available_balance = current_decryptable_available_balance - .decrypt(aes_key) + .decrypt(source_aes_key) .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; // compute the remaining balance ciphertext