diff --git a/token/confidential-transfer/proof-extraction/src/encryption.rs b/token/confidential-transfer/proof-extraction/src/encryption.rs index b0a991235f3..383108d41b1 100644 --- a/token/confidential-transfer/proof-extraction/src/encryption.rs +++ b/token/confidential-transfer/proof-extraction/src/encryption.rs @@ -5,7 +5,6 @@ use { grouped_elgamal::{ PodGroupedElGamalCiphertext2Handles, PodGroupedElGamalCiphertext3Handles, }, - pedersen::PodPedersenCommitment, }, }; @@ -14,10 +13,6 @@ use { pub struct PodTransferAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles); impl PodTransferAmountCiphertext { - pub fn extract_commitment(&self) -> PodPedersenCommitment { - self.0.extract_commitment() - } - pub fn try_extract_ciphertext( &self, index: usize, @@ -33,10 +28,6 @@ impl PodTransferAmountCiphertext { pub struct PodFeeCiphertext(pub(crate) PodGroupedElGamalCiphertext2Handles); impl PodFeeCiphertext { - pub fn extract_commitment(&self) -> PodPedersenCommitment { - self.0.extract_commitment() - } - pub fn try_extract_ciphertext( &self, index: usize, @@ -51,6 +42,28 @@ impl PodFeeCiphertext { #[repr(C)] pub struct PodBurnAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles); +impl PodBurnAmountCiphertext { + pub fn try_extract_ciphertext( + &self, + index: usize, + ) -> Result { + self.0 + .try_extract_ciphertext(index) + .map_err(|_| TokenProofExtractionError::CiphertextExtraction) + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(C)] pub struct PodMintAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles); + +impl PodMintAmountCiphertext { + pub fn try_extract_ciphertext( + &self, + index: usize, + ) -> Result { + self.0 + .try_extract_ciphertext(index) + .map_err(|_| TokenProofExtractionError::CiphertextExtraction) + } +} diff --git a/token/confidential-transfer/proof-extraction/src/instruction.rs b/token/confidential-transfer/proof-extraction/src/instruction.rs index 761a2b3fcfd..be41a4bb6a9 100644 --- a/token/confidential-transfer/proof-extraction/src/instruction.rs +++ b/token/confidential-transfer/proof-extraction/src/instruction.rs @@ -6,11 +6,11 @@ use { solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, - instruction::Instruction, + instruction::{AccountMeta, Instruction}, msg, program_error::ProgramError, pubkey::Pubkey, - sysvar::instructions::get_instruction_relative, + sysvar::{self, instructions::get_instruction_relative}, }, solana_zk_sdk::zk_elgamal_proof_program::{ self, @@ -146,6 +146,56 @@ pub fn verify_and_extract_context<'a, T: Pod + ZkProofData, U: Pod>( } } +/// Processes a proof location for instruction creation. Adds relevant accounts +/// to supplied account vector +/// +/// If the proof location is an instruction offset the corresponding proof +/// instruction is created and added to the `proof_instructions` vector. +pub fn process_proof_location( + accounts: &mut Vec, + expected_instruction_offset: &mut i8, + proof_instructions: &mut Vec, + proof_location: ProofLocation, + push_sysvar_to_accounts: bool, + proof_instruction_type: ProofInstruction, +) -> Result +where + T: Pod + ZkProofData, + U: Pod, +{ + match proof_location { + ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { + let proof_instruction_offset: i8 = proof_instruction_offset.into(); + if &proof_instruction_offset != expected_instruction_offset { + return Err(ProgramError::InvalidInstructionData); + } + + if push_sysvar_to_accounts { + accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); + } + match proof_data { + ProofData::InstructionData(data) => proof_instructions + .push(proof_instruction_type.encode_verify_proof::(None, data)), + ProofData::RecordAccount(address, offset) => { + accounts.push(AccountMeta::new_readonly(*address, false)); + proof_instructions.push( + proof_instruction_type + .encode_verify_proof_from_account(None, address, offset), + ) + } + }; + *expected_instruction_offset = expected_instruction_offset + .checked_add(1) + .ok_or(ProgramError::InvalidInstructionData)?; + Ok(proof_instruction_offset) + } + ProofLocation::ContextStateAccount(context_state_account) => { + accounts.push(AccountMeta::new_readonly(*context_state_account, false)); + Ok(0) + } + } +} + /// Converts a zk proof type to a corresponding ZK ElGamal proof program /// instruction that verifies the proof. pub fn zk_proof_type_to_instruction( diff --git a/token/confidential-transfer/proof-generation/src/mint.rs b/token/confidential-transfer/proof-generation/src/mint.rs index e25dc70a210..1ada01840ab 100644 --- a/token/confidential-transfer/proof-generation/src/mint.rs +++ b/token/confidential-transfer/proof-generation/src/mint.rs @@ -27,12 +27,13 @@ pub struct MintProofData { pub equality_proof_data: CiphertextCommitmentEqualityProofData, pub ciphertext_validity_proof_data: BatchedGroupedCiphertext3HandlesValidityProofData, pub range_proof_data: BatchedRangeProofU128Data, + pub new_decryptable_supply: AeCiphertext, } pub fn mint_split_proof_data( current_supply_ciphertext: &ElGamalCiphertext, - current_decryptable_supply: &AeCiphertext, mint_amount: u64, + current_supply: u64, supply_elgamal_keypair: &ElGamalKeypair, supply_aes_key: &AeKey, destination_elgamal_pubkey: &ElGamalPubkey, @@ -77,11 +78,6 @@ pub fn mint_split_proof_data( ) .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) @@ -142,5 +138,6 @@ pub fn mint_split_proof_data( equality_proof_data, ciphertext_validity_proof_data, range_proof_data, + new_decryptable_supply: supply_aes_key.encrypt(new_supply), }) } diff --git a/token/confidential-transfer/proof-tests/tests/proof_test.rs b/token/confidential-transfer/proof-tests/tests/proof_test.rs index f4c3a7f3a9e..6e73e303ed3 100644 --- a/token/confidential-transfer/proof-tests/tests/proof_test.rs +++ b/token/confidential-transfer/proof-tests/tests/proof_test.rs @@ -217,16 +217,16 @@ fn test_mint_validity(mint_amount: u64, supply: u64) { 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, + new_decryptable_supply: _, } = mint_split_proof_data( &supply_ciphertext, - &decryptable_supply, mint_amount, + supply, &supply_keypair, &supply_aes_key, destination_pubkey, diff --git a/token/program-2022/src/error.rs b/token/program-2022/src/error.rs index a886eb29e4e..54e4e250190 100644 --- a/token/program-2022/src/error.rs +++ b/token/program-2022/src/error.rs @@ -258,6 +258,11 @@ pub enum TokenError { /// Fee calculation failed #[error("Fee calculation failed")] FeeCalculation, + + //65 + /// Withdraw / Deposit not allowed for confidential-mint-burn + #[error("Withdraw / Deposit not allowed for confidential-mint-burn")] + IllegalMintBurnConversion, } impl From for ProgramError { fn from(e: TokenError) -> Self { @@ -445,6 +450,9 @@ impl PrintProgramError for TokenError { TokenError::FeeCalculation => { msg!("Transfer fee calculation failed") } + TokenError::IllegalMintBurnConversion => { + msg!("Conversions from normal to confidential token balance and vice versa are illegal if the confidential-mint-burn extension is enabled") + } } } } diff --git a/token/program-2022/src/extension/confidential_mint_burn/account_info.rs b/token/program-2022/src/extension/confidential_mint_burn/account_info.rs new file mode 100644 index 00000000000..01cae83acc7 --- /dev/null +++ b/token/program-2022/src/extension/confidential_mint_burn/account_info.rs @@ -0,0 +1,105 @@ +use { + super::ConfidentialMintBurn, + crate::error::TokenError, + bytemuck::{Pod, Zeroable}, + solana_zk_sdk::{ + encryption::{ + auth_encryption::{AeCiphertext, AeKey}, + elgamal::{ElGamalCiphertext, ElGamalKeypair}, + pedersen::PedersenOpening, + pod::{ + auth_encryption::PodAeCiphertext, + elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + }, + }, + zk_elgamal_proof_program::proof_data::CiphertextCiphertextEqualityProofData, + }, +}; + +/// Confidential Mint Burn extension information needed to construct a +/// `RotateSupplyElgamalPubkey` instruction. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +pub struct SupplyAccountInfo { + /// The available balance (encrypted by `supply_elgamal_pubkey`) + pub current_supply: PodElGamalCiphertext, + /// The decryptable supply + pub decryptable_supply: PodAeCiphertext, + /// The supply's elgamal pubkey + pub supply_elgamal_pubkey: PodElGamalPubkey, +} + +impl SupplyAccountInfo { + /// Creates a SupplyAccountInfo from ConfidentialMintBurn extension account + /// data + pub fn new(extension: &ConfidentialMintBurn) -> Self { + Self { + current_supply: extension.confidential_supply, + decryptable_supply: extension.decryptable_supply, + supply_elgamal_pubkey: extension.supply_elgamal_pubkey, + } + } + + /// Computes the current supply from the decryptable supply and the + /// difference between the decryptable supply and the elgamal encrypted + /// supply ciphertext + pub fn decrypt_current_supply( + &self, + aes_key: &AeKey, + elgamal_keypair: &ElGamalKeypair, + ) -> Result { + // decrypt the decryptable supply + let current_decyptable_supply = AeCiphertext::try_from(self.decryptable_supply) + .map_err(|_| TokenError::MalformedCiphertext)? + .decrypt(aes_key) + .ok_or(TokenError::MalformedCiphertext)?; + + // get the difference between the supply ciphertext and the decryptable supply + // explanation see https://github.com/solana-labs/solana-program-library/pull/6881#issuecomment-2385579058 + let decryptable_supply_ciphertext = + elgamal_keypair.pubkey().encrypt(current_decyptable_supply); + #[allow(clippy::arithmetic_side_effects)] + let supply_delta_ciphertext = decryptable_supply_ciphertext + - ElGamalCiphertext::try_from(self.current_supply) + .map_err(|_| TokenError::MalformedCiphertext)?; + let decryptable_to_current_diff = elgamal_keypair + .secret() + .decrypt_u32(&supply_delta_ciphertext) + .ok_or(TokenError::MalformedCiphertext)?; + + // compute the current supply + current_decyptable_supply + .checked_sub(decryptable_to_current_diff) + .ok_or(TokenError::Overflow) + } + + /// Generates the `CiphertextCiphertextEqualityProofData` needed for a + /// `RotateSupplyElgamalPubkey` instruction + pub fn generate_rotate_supply_elgamal_pubkey_proof( + &self, + aes_key: &AeKey, + current_supply_elgamal_keypair: &ElGamalKeypair, + new_supply_elgamal_keypair: &ElGamalKeypair, + ) -> Result { + let current_supply = + self.decrypt_current_supply(aes_key, current_supply_elgamal_keypair)?; + + let new_supply_opening = PedersenOpening::new_rand(); + let new_supply_ciphertext = new_supply_elgamal_keypair + .pubkey() + .encrypt_with(current_supply, &new_supply_opening); + + CiphertextCiphertextEqualityProofData::new( + current_supply_elgamal_keypair, + new_supply_elgamal_keypair.pubkey(), + &self + .current_supply + .try_into() + .map_err(|_| TokenError::MalformedCiphertext)?, + &new_supply_ciphertext, + &new_supply_opening, + current_supply, + ) + .map_err(|_| TokenError::ProofGeneration) + } +} diff --git a/token/program-2022/src/extension/confidential_mint_burn/instruction.rs b/token/program-2022/src/extension/confidential_mint_burn/instruction.rs new file mode 100644 index 00000000000..6e808d3653e --- /dev/null +++ b/token/program-2022/src/extension/confidential_mint_burn/instruction.rs @@ -0,0 +1,549 @@ +#[cfg(feature = "serde-traits")] +use { + crate::serialization::{aeciphertext_fromstr, elgamalpubkey_fromstr}, + serde::{Deserialize, Serialize}, +}; +use { + crate::{ + check_program_account, + extension::confidential_transfer::DecryptableBalance, + instruction::{encode_instruction, TokenInstruction}, + }, + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_program::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + }, + solana_zk_sdk::encryption::pod::{auth_encryption::PodAeCiphertext, elgamal::PodElGamalPubkey}, +}; +#[cfg(not(target_os = "solana"))] +use { + solana_zk_sdk::{ + encryption::{auth_encryption::AeCiphertext, elgamal::ElGamalPubkey}, + zk_elgamal_proof_program::{ + instruction::ProofInstruction, + proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCiphertextEqualityProofData, CiphertextCommitmentEqualityProofData, + }, + }, + }, + spl_token_confidential_transfer_proof_extraction::instruction::{ + process_proof_location, ProofLocation, + }, +}; + +/// Confidential Transfer extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] +#[repr(u8)] +pub enum ConfidentialMintBurnInstruction { + /// Initializes confidential mints and burns for a mint. + /// + /// The `ConfidentialMintBurnInstruction::InitializeMint` instruction + /// requires no signers and MUST be included within the same Transaction + /// as `TokenInstruction::InitializeMint`. Otherwise another party can + /// initialize the configuration. + /// + /// The instruction fails if the `TokenInstruction::InitializeMint` + /// instruction has already executed for the mint. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The SPL Token mint. + /// + /// Data expected by this instruction: + /// `InitializeMintData` + InitializeMint, + /// Rotates the ElGamal pubkey used to encrypt confidential supply + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The SPL Token mint. + /// 1. `[]` Instructions sysvar if `CiphertextCiphertextEquality` is + /// included in the same transaction or context state account if + /// `CiphertextCiphertextEquality` is pre-verified into a context state + /// account. + /// 2. `[signer]` Confidential mint authority. + /// + /// * Multisignature authority + /// 0. `[writable]` The SPL Token mint. + /// 1. `[]` Instructions sysvar if `CiphertextCiphertextEquality` is + /// included in the same transaction or context state account if + /// `CiphertextCiphertextEquality` is pre-verified into a context state + /// account. + /// 2. `[]` The multisig authority account owner. + /// 3.. `[signer]` Required M signer accounts for the SPL Token Multisig + /// + /// Data expected by this instruction: + /// `RotateSupplyElGamalPubkeyData` + RotateSupplyElGamalPubkey, + /// Updates the decryptable supply of the mint + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The SPL Token mint. + /// 1. `[signer]` Confidential mint authority. + /// + /// * Multisignature authority + /// 0. `[writable]` The SPL Token mint. + /// 1. `[]` The multisig authority account owner. + /// 2.. `[signer]` Required M signer accounts for the SPL Token Multisig + /// + /// Data expected by this instruction: + /// `UpdateDecryptableSupplyData` + UpdateDecryptableSupply, + /// Mints tokens to confidential balance + /// + /// Fails if the destination account is frozen. + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The SPL Token account. + /// 1. `[]` The SPL Token mint. `[writable]` if the mint has a non-zero + /// supply elgamal-pubkey + /// 2. `[]` (Optional) Instructions sysvar if at least one of the + /// `zk_elgamal_proof` instructions are included in the same + /// transaction. + /// 3. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyCiphertextCommitmentEquality` proof + /// 4. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyBatchedGroupedCiphertext3HandlesValidity` proof + /// 5. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyBatchedRangeProofU128` + /// 6. `[signer]` The single account owner. + /// + /// * Multisignature authority + /// 0. `[writable]` The SPL Token mint. + /// 1. `[]` The SPL Token mint. `[writable]` if the mint has a non-zero + /// supply elgamal-pubkey + /// 2. `[]` (Optional) Instructions sysvar if at least one of the + /// `zk_elgamal_proof` instructions are included in the same + /// transaction. + /// 3. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyCiphertextCommitmentEquality` proof + /// 4. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyBatchedGroupedCiphertext3HandlesValidity` proof + /// 5. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyBatchedRangeProofU128` + /// 6. `[]` The multisig account owner. + /// 7.. `[signer]` Required M signer accounts for the SPL Token Multisig + /// + /// Data expected by this instruction: + /// `MintInstructionData` + Mint, + /// Burn tokens from confidential balance + /// + /// Fails if the destination account is frozen. + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The SPL Token account. + /// 1. `[]` The SPL Token mint. `[writable]` if the mint has a non-zero + /// supply elgamal-pubkey + /// 2. `[]` (Optional) Instructions sysvar if at least one of the + /// `zk_elgamal_proof` instructions are included in the same + /// transaction. + /// 3. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyCiphertextCommitmentEquality` proof + /// 4. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyBatchedGroupedCiphertext3HandlesValidity` proof + /// 5. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyBatchedRangeProofU128` + /// 6. `[signer]` The single account owner. + /// + /// * Multisignature authority + /// 0. `[writable]` The SPL Token mint. + /// 1. `[]` The SPL Token mint. `[writable]` if the mint has a non-zero + /// supply elgamal-pubkey + /// 2. `[]` (Optional) Instructions sysvar if at least one of the + /// `zk_elgamal_proof` instructions are included in the same + /// transaction. + /// 3. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyCiphertextCommitmentEquality` proof + /// 4. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyBatchedGroupedCiphertext3HandlesValidity` proof + /// 5. `[]` (Optional) The context state account containing the + /// pre-verified `VerifyBatchedRangeProofU128` + /// 6. `[]` The multisig account owner. + /// 7.. `[signer]` Required M signer accounts for the SPL Token Multisig + /// + /// Data expected by this instruction: + /// `BurnInstructionData` + Burn, +} + +/// Data expected by `ConfidentialMintBurnInstruction::InitializeMint` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct InitializeMintData { + /// The ElGamal pubkey used to encrypt the confidential supply + #[cfg_attr(feature = "serde-traits", serde(with = "elgamalpubkey_fromstr"))] + pub supply_elgamal_pubkey: PodElGamalPubkey, + /// The initial 0 supply ecrypted with the supply aes key + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] + pub decryptable_supply: PodAeCiphertext, +} + +/// Data expected by `ConfidentialMintBurnInstruction::RotateSupplyElGamal` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct RotateSupplyElGamalPubkeyData { + /// The new ElGamal pubkey for supply encryption + #[cfg_attr(feature = "serde-traits", serde(with = "elgamalpubkey_fromstr"))] + pub new_supply_elgamal_pubkey: PodElGamalPubkey, + /// The location of the + /// `ProofInstruction::VerifyCiphertextCiphertextEquality` instruction + /// relative to the `RotateSupplyElGamal` instruction in the transaction + pub proof_instruction_offset: i8, +} + +/// Data expected by `ConfidentialMintBurnInstruction::UpdateDecryptableSupply` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct UpdateDecryptableSupplyData { + /// The new decryptable supply + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] + pub new_decryptable_supply: PodAeCiphertext, +} + +/// Data expected by `ConfidentialMintBurnInstruction::ConfidentialMint` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct MintInstructionData { + /// The new decryptable supply if the mint succeeds + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] + pub new_decryptable_supply: PodAeCiphertext, + /// Relative location of the + /// `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction + /// to the `ConfidentialMint` instruction in the transaction. 0 if the + /// proof is in a pre-verified context account + pub equality_proof_instruction_offset: i8, + /// Relative location of the + /// `ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity` + /// instruction to the `ConfidentialMint` instruction in the + /// transaction. 0 if the proof is in a pre-verified context account + pub ciphertext_validity_proof_instruction_offset: i8, + /// Relative location of the `ProofInstruction::VerifyBatchedRangeProofU128` + /// instruction to the `ConfidentialMint` instruction in the + /// transaction. 0 if the proof is in a pre-verified context account + pub range_proof_instruction_offset: i8, +} + +/// Data expected by `ConfidentialMintBurnInstruction::ConfidentialBurn` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct BurnInstructionData { + /// The new decryptable balance of the burner if the burn succeeds + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] + pub new_decryptable_available_balance: DecryptableBalance, + /// Relative location of the + /// `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction + /// to the `ConfidentialMint` instruction in the transaction. 0 if the + /// proof is in a pre-verified context account + pub equality_proof_instruction_offset: i8, + /// Relative location of the + /// `ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity` + /// instruction to the `ConfidentialMint` instruction in the + /// transaction. 0 if the proof is in a pre-verified context account + pub ciphertext_validity_proof_instruction_offset: i8, + /// Relative location of the `ProofInstruction::VerifyBatchedRangeProofU128` + /// instruction to the `ConfidentialMint` instruction in the + /// transaction. 0 if the proof is in a pre-verified context account + pub range_proof_instruction_offset: i8, +} + +/// Create a `InitializeMint` instruction +pub fn initialize_mint( + token_program_id: &Pubkey, + mint: &Pubkey, + supply_elgamal_pubkey: PodElGamalPubkey, + decryptable_supply: PodAeCiphertext, +) -> Result { + check_program_account(token_program_id)?; + let accounts = vec![AccountMeta::new(*mint, false)]; + + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialMintBurnExtension, + ConfidentialMintBurnInstruction::InitializeMint, + &InitializeMintData { + supply_elgamal_pubkey, + decryptable_supply, + }, + )) +} + +/// Create a `RotateSupplyElGamal` instruction +#[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] +pub fn rotate_supply_elgamal_pubkey( + token_program_id: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, + multisig_signers: &[&Pubkey], + new_supply_elgamal_pubkey: ElGamalPubkey, + ciphertext_equality_proof: ProofLocation, +) -> Result, ProgramError> { + check_program_account(token_program_id)?; + let mut accounts = vec![AccountMeta::new(*mint, false)]; + + let mut expected_instruction_offset = 1; + let mut proof_instructions = vec![]; + + let proof_instruction_offset = process_proof_location( + &mut accounts, + &mut expected_instruction_offset, + &mut proof_instructions, + ciphertext_equality_proof, + true, + ProofInstruction::VerifyCiphertextCiphertextEquality, + )?; + + accounts.push(AccountMeta::new_readonly( + *authority, + multisig_signers.is_empty(), + )); + for multisig_signer in multisig_signers.iter() { + accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); + } + + let mut instructions = vec![encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialMintBurnExtension, + ConfidentialMintBurnInstruction::RotateSupplyElGamalPubkey, + &RotateSupplyElGamalPubkeyData { + new_supply_elgamal_pubkey: PodElGamalPubkey::from(new_supply_elgamal_pubkey), + proof_instruction_offset, + }, + )]; + + instructions.extend_from_slice(&proof_instructions); + + Ok(instructions) +} + +/// Create a `UpdateMint` instruction +#[cfg(not(target_os = "solana"))] +pub fn update_decryptable_supply( + token_program_id: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, + multisig_signers: &[&Pubkey], + new_decryptable_supply: AeCiphertext, +) -> Result { + check_program_account(token_program_id)?; + let mut accounts = vec![ + AccountMeta::new(*mint, false), + AccountMeta::new_readonly(*authority, multisig_signers.is_empty()), + ]; + for multisig_signer in multisig_signers.iter() { + accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); + } + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialMintBurnExtension, + ConfidentialMintBurnInstruction::UpdateDecryptableSupply, + &UpdateDecryptableSupplyData { + new_decryptable_supply: new_decryptable_supply.into(), + }, + )) +} + +/// Context state accounts used in confidential mint +#[derive(Clone, Copy)] +pub struct MintSplitContextStateAccounts<'a> { + /// Location of equality proof + pub equality_proof: &'a Pubkey, + /// Location of ciphertext validity proof + pub ciphertext_validity_proof: &'a Pubkey, + /// Location of range proof + pub range_proof: &'a Pubkey, + /// Authority able to close proof accounts + pub authority: &'a Pubkey, +} + +/// Create a `ConfidentialMint` instruction +#[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] +pub fn confidential_mint_with_split_proofs( + token_program_id: &Pubkey, + token_account: &Pubkey, + mint: &Pubkey, + supply_elgamal_pubkey: Option, + authority: &Pubkey, + multisig_signers: &[&Pubkey], + equality_proof_location: ProofLocation, + ciphertext_validity_proof_location: ProofLocation< + BatchedGroupedCiphertext3HandlesValidityProofData, + >, + range_proof_location: ProofLocation, + new_decryptable_supply: AeCiphertext, +) -> Result, ProgramError> { + check_program_account(token_program_id)?; + let mut accounts = vec![AccountMeta::new(*token_account, false)]; + // we only need write lock to adjust confidential suppy on + // mint if a value for supply_elgamal_pubkey has been set + if supply_elgamal_pubkey.is_some() { + accounts.push(AccountMeta::new(*mint, false)); + } else { + accounts.push(AccountMeta::new_readonly(*mint, false)); + } + + let mut expected_instruction_offset = 1; + let mut proof_instructions = vec![]; + + let equality_proof_instruction_offset = process_proof_location( + &mut accounts, + &mut expected_instruction_offset, + &mut proof_instructions, + equality_proof_location, + true, + ProofInstruction::VerifyCiphertextCommitmentEquality, + )?; + + let ciphertext_validity_proof_instruction_offset = process_proof_location( + &mut accounts, + &mut expected_instruction_offset, + &mut proof_instructions, + ciphertext_validity_proof_location, + false, + ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity, + )?; + + let range_proof_instruction_offset = process_proof_location( + &mut accounts, + &mut expected_instruction_offset, + &mut proof_instructions, + range_proof_location, + false, + ProofInstruction::VerifyBatchedRangeProofU128, + )?; + + accounts.push(AccountMeta::new_readonly( + *authority, + multisig_signers.is_empty(), + )); + for multisig_signer in multisig_signers.iter() { + accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); + } + + let mut instructions = vec![encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialMintBurnExtension, + ConfidentialMintBurnInstruction::Mint, + &MintInstructionData { + new_decryptable_supply: new_decryptable_supply.into(), + equality_proof_instruction_offset, + ciphertext_validity_proof_instruction_offset, + range_proof_instruction_offset, + }, + )]; + + instructions.extend_from_slice(&proof_instructions); + + Ok(instructions) +} + +/// Create a inner `ConfidentialBurn` instruction +#[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] +pub fn confidential_burn_with_split_proofs( + token_program_id: &Pubkey, + token_account: &Pubkey, + mint: &Pubkey, + supply_elgamal_pubkey: Option, + new_decryptable_available_balance: DecryptableBalance, + authority: &Pubkey, + multisig_signers: &[&Pubkey], + equality_proof_location: ProofLocation, + ciphertext_validity_proof_location: ProofLocation< + BatchedGroupedCiphertext3HandlesValidityProofData, + >, + range_proof_location: ProofLocation, +) -> Result, ProgramError> { + check_program_account(token_program_id)?; + let mut accounts = vec![AccountMeta::new(*token_account, false)]; + if supply_elgamal_pubkey.is_some() { + accounts.push(AccountMeta::new(*mint, false)); + } else { + accounts.push(AccountMeta::new_readonly(*mint, false)); + } + + let mut expected_instruction_offset = 1; + let mut proof_instructions = vec![]; + + let equality_proof_instruction_offset = process_proof_location( + &mut accounts, + &mut expected_instruction_offset, + &mut proof_instructions, + equality_proof_location, + true, + ProofInstruction::VerifyCiphertextCommitmentEquality, + )?; + + let ciphertext_validity_proof_instruction_offset = process_proof_location( + &mut accounts, + &mut expected_instruction_offset, + &mut proof_instructions, + ciphertext_validity_proof_location, + false, + ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity, + )?; + + let range_proof_instruction_offset = process_proof_location( + &mut accounts, + &mut expected_instruction_offset, + &mut proof_instructions, + range_proof_location, + false, + ProofInstruction::VerifyBatchedRangeProofU128, + )?; + + accounts.push(AccountMeta::new_readonly( + *authority, + multisig_signers.is_empty(), + )); + + for multisig_signer in multisig_signers.iter() { + accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); + } + + let mut instructions = vec![encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialMintBurnExtension, + ConfidentialMintBurnInstruction::Burn, + &BurnInstructionData { + new_decryptable_available_balance, + equality_proof_instruction_offset, + ciphertext_validity_proof_instruction_offset, + range_proof_instruction_offset, + }, + )]; + + instructions.extend_from_slice(&proof_instructions); + + Ok(instructions) +} diff --git a/token/program-2022/src/extension/confidential_mint_burn/mod.rs b/token/program-2022/src/extension/confidential_mint_burn/mod.rs new file mode 100644 index 00000000000..049ced2684c --- /dev/null +++ b/token/program-2022/src/extension/confidential_mint_burn/mod.rs @@ -0,0 +1,45 @@ +use { + crate::extension::{Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, + solana_zk_sdk::encryption::pod::{ + auth_encryption::PodAeCiphertext, + elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + }, +}; + +/// Maximum bit length of any mint or burn amount +/// +/// Any mint or burn amount must be less than 2^48 +pub const MAXIMUM_DEPOSIT_TRANSFER_AMOUNT: u64 = (u16::MAX as u64) + (1 << 16) * (u32::MAX as u64); + +/// Bit length of the low bits of pending balance plaintext +pub const PENDING_BALANCE_LO_BIT_LENGTH: u32 = 16; + +/// Confidential Mint-Burn Extension instructions +pub mod instruction; + +/// Confidential Mint-Burn Extension processor +pub mod processor; + +/// Confidential Mint-Burn proof verification +pub mod verify_proof; + +/// Confidential Mint Burn Extension supply information needed for instructions +#[cfg(not(target_os = "solana"))] +pub mod account_info; + +/// Confidential mint-burn mint configuration +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct ConfidentialMintBurn { + /// The confidential supply of the mint (encrypted by `encryption_pubkey`) + pub confidential_supply: PodElGamalCiphertext, + /// The decryptable confidential supply of the mint + pub decryptable_supply: PodAeCiphertext, + /// The ElGamal pubkey used to encrypt the confidential supply + pub supply_elgamal_pubkey: PodElGamalPubkey, +} + +impl Extension for ConfidentialMintBurn { + const TYPE: ExtensionType = ExtensionType::ConfidentialMintBurn; +} diff --git a/token/program-2022/src/extension/confidential_mint_burn/processor.rs b/token/program-2022/src/extension/confidential_mint_burn/processor.rs new file mode 100644 index 00000000000..f0697337783 --- /dev/null +++ b/token/program-2022/src/extension/confidential_mint_burn/processor.rs @@ -0,0 +1,402 @@ +#[cfg(feature = "zk-ops")] +use spl_token_confidential_transfer_ciphertext_arithmetic as ciphertext_arithmetic; +use { + crate::{ + check_program_account, + error::TokenError, + extension::{ + confidential_mint_burn::{ + instruction::{ + BurnInstructionData, ConfidentialMintBurnInstruction, InitializeMintData, + MintInstructionData, RotateSupplyElGamalPubkeyData, + UpdateDecryptableSupplyData, + }, + verify_proof::{verify_burn_proof, verify_mint_proof}, + ConfidentialMintBurn, + }, + confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, + BaseStateWithExtensions, BaseStateWithExtensionsMut, PodStateWithExtensionsMut, + }, + instruction::{decode_instruction_data, decode_instruction_type}, + pod::{PodAccount, PodMint}, + processor::Processor, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + pubkey::Pubkey, + }, + solana_zk_sdk::{ + encryption::pod::{auth_encryption::PodAeCiphertext, elgamal::PodElGamalPubkey}, + zk_elgamal_proof_program::proof_data::{ + CiphertextCiphertextEqualityProofContext, CiphertextCiphertextEqualityProofData, + }, + }, + spl_token_confidential_transfer_proof_extraction::instruction::verify_and_extract_context, +}; + +/// Processes an [InitializeMint] instruction. +fn process_initialize_mint(accounts: &[AccountInfo], data: &InitializeMintData) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + + check_program_account(mint_info.owner)?; + + let mint_data = &mut mint_info.data.borrow_mut(); + let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(mint_data)?; + let mint_burn_extension = mint.init_extension::(true)?; + + mint_burn_extension.supply_elgamal_pubkey = data.supply_elgamal_pubkey; + mint_burn_extension.decryptable_supply = data.decryptable_supply; + + Ok(()) +} + +/// Processes an [RotateSupplyElGamal] instruction. +#[cfg(feature = "zk-ops")] +fn process_rotate_supply_elgamal_pubkey( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &RotateSupplyElGamalPubkeyData, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + + check_program_account(mint_info.owner)?; + let mint_data = &mut mint_info.data.borrow_mut(); + let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; + let mint_authority = mint.base.mint_authority; + let mint_burn_extension = mint.get_extension_mut::()?; + + let proof_context = verify_and_extract_context::< + CiphertextCiphertextEqualityProofData, + CiphertextCiphertextEqualityProofContext, + >( + account_info_iter, + data.proof_instruction_offset as i64, + None, + )?; + + let supply_elgamal_pubkey: Option = + mint_burn_extension.supply_elgamal_pubkey.into(); + let Some(supply_elgamal_pubkey) = supply_elgamal_pubkey else { + return Err(TokenError::InvalidState.into()); + }; + + if !supply_elgamal_pubkey.eq(&proof_context.first_pubkey) { + return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); + } + if mint_burn_extension.confidential_supply != proof_context.first_ciphertext { + return Err(ProgramError::InvalidInstructionData); + } + + let authority_info = next_account_info(account_info_iter)?; + let authority_info_data_len = authority_info.data_len(); + let authority = mint_authority.ok_or(TokenError::NoAuthorityExists)?; + + Processor::validate_owner( + program_id, + &authority, + authority_info, + authority_info_data_len, + account_info_iter.as_slice(), + )?; + + mint_burn_extension.supply_elgamal_pubkey = proof_context.second_pubkey; + mint_burn_extension.confidential_supply = proof_context.second_ciphertext; + + Ok(()) +} + +/// Processes an [UpdateAuthority] instruction. +fn process_update_decryptable_supply( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_decryptable_supply: PodAeCiphertext, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + + check_program_account(mint_info.owner)?; + let mint_data = &mut mint_info.data.borrow_mut(); + let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; + let mint_authority = mint.base.mint_authority; + let mint_burn_extension = mint.get_extension_mut::()?; + + let authority_info = next_account_info(account_info_iter)?; + let authority_info_data_len = authority_info.data_len(); + let authority = mint_authority.ok_or(TokenError::NoAuthorityExists)?; + + Processor::validate_owner( + program_id, + &authority, + authority_info, + authority_info_data_len, + account_info_iter.as_slice(), + )?; + + mint_burn_extension.decryptable_supply = new_decryptable_supply; + + Ok(()) +} + +/// Processes a [ConfidentialMint] instruction. +#[cfg(feature = "zk-ops")] +fn process_confidential_mint( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &MintInstructionData, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let token_account_info = next_account_info(account_info_iter)?; + let mint_info = next_account_info(account_info_iter)?; + + check_program_account(mint_info.owner)?; + let mint_data = &mut mint_info.data.borrow_mut(); + let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; + let mint_authority = mint.base.mint_authority; + + let auditor_elgamal_pubkey = mint + .get_extension::()? + .auditor_elgamal_pubkey; + let mint_burn_extension = mint.get_extension_mut::()?; + + let proof_context = verify_mint_proof( + account_info_iter, + data.equality_proof_instruction_offset, + data.ciphertext_validity_proof_instruction_offset, + data.range_proof_instruction_offset, + )?; + + check_program_account(token_account_info.owner)?; + let token_account_data = &mut token_account_info.data.borrow_mut(); + let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; + + let authority_info = next_account_info(account_info_iter)?; + let authority_info_data_len = authority_info.data_len(); + let authority = mint_authority.ok_or(TokenError::NoAuthorityExists)?; + + Processor::validate_owner( + program_id, + &authority, + authority_info, + authority_info_data_len, + account_info_iter.as_slice(), + )?; + + if token_account.base.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + + if token_account.base.mint != *mint_info.key { + return Err(TokenError::MintMismatch.into()); + } + + assert!(!token_account.base.is_native()); + + let confidential_transfer_account = + token_account.get_extension_mut::()?; + confidential_transfer_account.valid_as_destination()?; + + if proof_context.mint_pubkeys.destination != confidential_transfer_account.elgamal_pubkey { + return Err(ProgramError::InvalidInstructionData); + } + + if let Some(auditor_pubkey) = Option::::from(auditor_elgamal_pubkey) { + if auditor_pubkey != proof_context.mint_pubkeys.auditor { + return Err(ProgramError::InvalidInstructionData); + } + } + + confidential_transfer_account.pending_balance_lo = ciphertext_arithmetic::add( + &confidential_transfer_account.pending_balance_lo, + &proof_context + .mint_amount_ciphertext_lo + .try_extract_ciphertext(0) + .map_err(|_| ProgramError::InvalidAccountData)?, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; + confidential_transfer_account.pending_balance_hi = ciphertext_arithmetic::add( + &confidential_transfer_account.pending_balance_hi, + &proof_context + .mint_amount_ciphertext_hi + .try_extract_ciphertext(0) + .map_err(|_| ProgramError::InvalidAccountData)?, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; + + confidential_transfer_account.increment_pending_balance_credit_counter()?; + + // update supply + if mint_burn_extension.supply_elgamal_pubkey != proof_context.mint_pubkeys.supply { + return Err(ProgramError::InvalidInstructionData); + } + let current_supply = mint_burn_extension.confidential_supply; + mint_burn_extension.confidential_supply = ciphertext_arithmetic::add_with_lo_hi( + ¤t_supply, + &proof_context + .mint_amount_ciphertext_lo + .try_extract_ciphertext(2) + .map_err(|_| ProgramError::InvalidAccountData)?, + &proof_context + .mint_amount_ciphertext_hi + .try_extract_ciphertext(2) + .map_err(|_| ProgramError::InvalidAccountData)?, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; + mint_burn_extension.decryptable_supply = data.new_decryptable_supply; + + Ok(()) +} + +/// Processes a [ConfidentialBurn] instruction. +#[cfg(feature = "zk-ops")] +fn process_confidential_burn( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &BurnInstructionData, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let token_account_info = next_account_info(account_info_iter)?; + let mint_info = next_account_info(account_info_iter)?; + + check_program_account(mint_info.owner)?; + let mint_data = &mut mint_info.data.borrow_mut(); + let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; + + let auditor_elgamal_pubkey = mint + .get_extension::()? + .auditor_elgamal_pubkey; + let mint_burn_extension = mint.get_extension_mut::()?; + + let proof_context = verify_burn_proof( + account_info_iter, + data.equality_proof_instruction_offset, + data.ciphertext_validity_proof_instruction_offset, + data.range_proof_instruction_offset, + )?; + + check_program_account(token_account_info.owner)?; + let token_account_data = &mut token_account_info.data.borrow_mut(); + let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; + + let authority_info = next_account_info(account_info_iter)?; + let authority_info_data_len = authority_info.data_len(); + + Processor::validate_owner( + program_id, + &token_account.base.owner, + authority_info, + authority_info_data_len, + account_info_iter.as_slice(), + )?; + + if token_account.base.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + + if token_account.base.mint != *mint_info.key { + return Err(TokenError::MintMismatch.into()); + } + + let confidential_transfer_account = + token_account.get_extension_mut::()?; + confidential_transfer_account.valid_as_source()?; + + // Check that the source encryption public key is consistent with what was + // actually used to generate the zkp. + if proof_context.burn_pubkeys.source != confidential_transfer_account.elgamal_pubkey { + return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); + } + + let burn_amount_lo = &proof_context + .burn_amount_ciphertext_lo + .try_extract_ciphertext(0) + .map_err(|_| ProgramError::InvalidAccountData)?; + let burn_amount_hi = &proof_context + .burn_amount_ciphertext_hi + .try_extract_ciphertext(0) + .map_err(|_| ProgramError::InvalidAccountData)?; + + let new_source_available_balance = ciphertext_arithmetic::subtract_with_lo_hi( + &confidential_transfer_account.available_balance, + burn_amount_lo, + burn_amount_hi, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; + + // Check that the computed available balance is consistent with what was + // actually used to generate the zkp on the client side. + if new_source_available_balance != proof_context.remaining_balance_ciphertext { + return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); + } + + confidential_transfer_account.available_balance = new_source_available_balance; + confidential_transfer_account.decryptable_available_balance = + data.new_decryptable_available_balance; + + if let Some(auditor_pubkey) = Option::::from(auditor_elgamal_pubkey) { + if auditor_pubkey != proof_context.burn_pubkeys.auditor { + return Err(ProgramError::InvalidInstructionData); + } + } + + // update supply + if mint_burn_extension.supply_elgamal_pubkey != proof_context.burn_pubkeys.supply { + return Err(ProgramError::InvalidInstructionData); + } + let current_supply = mint_burn_extension.confidential_supply; + mint_burn_extension.confidential_supply = ciphertext_arithmetic::subtract_with_lo_hi( + ¤t_supply, + &proof_context + .burn_amount_ciphertext_lo + .try_extract_ciphertext(2) + .map_err(|_| ProgramError::InvalidAccountData)?, + &proof_context + .burn_amount_ciphertext_hi + .try_extract_ciphertext(2) + .map_err(|_| ProgramError::InvalidAccountData)?, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; + + Ok(()) +} + +#[allow(dead_code)] +pub(crate) fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + check_program_account(program_id)?; + + match decode_instruction_type(input)? { + ConfidentialMintBurnInstruction::InitializeMint => { + msg!("ConfidentialMintBurnInstruction::InitializeMint"); + let data = decode_instruction_data::(input)?; + process_initialize_mint(accounts, data) + } + ConfidentialMintBurnInstruction::RotateSupplyElGamalPubkey => { + msg!("ConfidentialMintBurnInstruction::RotateSupplyElGamal"); + let data = decode_instruction_data::(input)?; + process_rotate_supply_elgamal_pubkey(program_id, accounts, data) + } + ConfidentialMintBurnInstruction::UpdateDecryptableSupply => { + msg!("ConfidentialMintBurnInstruction::UpdateDecryptableSupply"); + let data = decode_instruction_data::(input)?; + process_update_decryptable_supply(program_id, accounts, data.new_decryptable_supply) + } + ConfidentialMintBurnInstruction::Mint => { + msg!("ConfidentialMintBurnInstruction::ConfidentialMint"); + let data = decode_instruction_data::(input)?; + process_confidential_mint(program_id, accounts, data) + } + ConfidentialMintBurnInstruction::Burn => { + msg!("ConfidentialMintBurnInstruction::ConfidentialBurn"); + let data = decode_instruction_data::(input)?; + process_confidential_burn(program_id, accounts, data) + } + } +} diff --git a/token/program-2022/src/extension/confidential_mint_burn/verify_proof.rs b/token/program-2022/src/extension/confidential_mint_burn/verify_proof.rs new file mode 100644 index 00000000000..cb1e3392df3 --- /dev/null +++ b/token/program-2022/src/extension/confidential_mint_burn/verify_proof.rs @@ -0,0 +1,114 @@ +use crate::error::TokenError; +#[cfg(feature = "zk-ops")] +use { + solana_program::{ + account_info::{next_account_info, AccountInfo}, + program_error::ProgramError, + }, + solana_zk_sdk::zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofContext, + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofContext, + BatchedRangeProofU128Data, CiphertextCommitmentEqualityProofContext, + CiphertextCommitmentEqualityProofData, + }, + spl_token_confidential_transfer_proof_extraction::{ + burn::BurnProofContext, instruction::verify_and_extract_context, mint::MintProofContext, + }, + std::slice::Iter, +}; + +/// Verify zero-knowledge proofs needed for a [ConfidentialMint] instruction and +/// return the corresponding proof context information. +#[cfg(feature = "zk-ops")] +pub fn verify_mint_proof( + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, + equality_proof_instruction_offset: i8, + ciphertext_validity_proof_instruction_offset: i8, + range_proof_instruction_offset: i8, +) -> Result { + let sysvar_account_info = if equality_proof_instruction_offset != 0 { + Some(next_account_info(account_info_iter)?) + } else { + None + }; + + let equality_proof_context = verify_and_extract_context::< + CiphertextCommitmentEqualityProofData, + CiphertextCommitmentEqualityProofContext, + >( + account_info_iter, + equality_proof_instruction_offset as i64, + sysvar_account_info, + )?; + + let ciphertext_validity_proof_context = verify_and_extract_context::< + BatchedGroupedCiphertext3HandlesValidityProofData, + BatchedGroupedCiphertext3HandlesValidityProofContext, + >( + account_info_iter, + ciphertext_validity_proof_instruction_offset as i64, + sysvar_account_info, + )?; + + let range_proof_context = + verify_and_extract_context::( + account_info_iter, + range_proof_instruction_offset as i64, + sysvar_account_info, + )?; + + Ok(MintProofContext::verify_and_extract( + &equality_proof_context, + &ciphertext_validity_proof_context, + &range_proof_context, + ) + .map_err(|e| -> TokenError { e.into() })?) +} + +/// Verify zero-knowledge proofs needed for a [ConfidentialBurn] instruction and +/// return the corresponding proof context information. +#[cfg(feature = "zk-ops")] +pub fn verify_burn_proof( + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, + equality_proof_instruction_offset: i8, + ciphertext_validity_proof_instruction_offset: i8, + range_proof_instruction_offset: i8, +) -> Result { + let sysvar_account_info = if equality_proof_instruction_offset != 0 { + Some(next_account_info(account_info_iter)?) + } else { + None + }; + + let equality_proof_context = verify_and_extract_context::< + CiphertextCommitmentEqualityProofData, + CiphertextCommitmentEqualityProofContext, + >( + account_info_iter, + equality_proof_instruction_offset as i64, + sysvar_account_info, + )?; + + let ciphertext_validity_proof_context = verify_and_extract_context::< + BatchedGroupedCiphertext3HandlesValidityProofData, + BatchedGroupedCiphertext3HandlesValidityProofContext, + >( + account_info_iter, + ciphertext_validity_proof_instruction_offset as i64, + sysvar_account_info, + )?; + + let range_proof_context = + verify_and_extract_context::( + account_info_iter, + range_proof_instruction_offset as i64, + sysvar_account_info, + )?; + + Ok(BurnProofContext::verify_and_extract( + &equality_proof_context, + &ciphertext_validity_proof_context, + &range_proof_context, + ) + .map_err(|e| -> TokenError { e.into() })?) +} diff --git a/token/program-2022/src/extension/confidential_transfer/account_info.rs b/token/program-2022/src/extension/confidential_transfer/account_info.rs index ed9a94e68dd..39b819b9a70 100644 --- a/token/program-2022/src/extension/confidential_transfer/account_info.rs +++ b/token/program-2022/src/extension/confidential_transfer/account_info.rs @@ -324,7 +324,8 @@ impl TransferAccountInfo { } } -fn combine_balances(balance_lo: u64, balance_hi: u64) -> Option { +/// Combines pending balances low and high bits into singular pending balance +pub fn combine_balances(balance_lo: u64, balance_hi: u64) -> Option { balance_hi .checked_shl(PENDING_BALANCE_LO_BIT_LENGTH)? .checked_add(balance_lo) diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index e82f4f521af..335fdf4a296 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -182,6 +182,7 @@ pub enum ConfidentialTransferInstruction { /// /// Fails if the source or destination accounts are frozen. /// Fails if the associated mint is extended as `NonTransferable`. + /// Fails if the associated mint is extended as `ConfidentialMintBurn`. /// /// Accounts expected by this instruction: /// @@ -215,6 +216,7 @@ pub enum ConfidentialTransferInstruction { /// /// Fails if the source or destination accounts are frozen. /// Fails if the associated mint is extended as `NonTransferable`. + /// Fails if the associated mint is extended as `ConfidentialMintBurn`. /// /// Accounts expected by this instruction: /// diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index a93b5e85268..c387dedff1f 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -1,6 +1,7 @@ // Remove feature once zk ops syscalls are enabled on all networks #[cfg(feature = "zk-ops")] use { + crate::extension::confidential_mint_burn::ConfidentialMintBurn, crate::extension::non_transferable::NonTransferableAccount, spl_token_confidential_transfer_ciphertext_arithmetic as ciphertext_arithmetic, }; @@ -262,6 +263,7 @@ fn process_configure_account( // `ConfidentialTransferAccount` extension let confidential_transfer_account = token_account.init_extension::(false)?; + confidential_transfer_account.approved = confidential_transfer_mint.auto_approve_new_accounts; confidential_transfer_account.elgamal_pubkey = elgamal_pubkey; confidential_transfer_account.maximum_pending_balance_credit_counter = @@ -399,6 +401,10 @@ fn process_deposit( return Err(TokenError::MintDecimalsMismatch.into()); } + if mint.get_extension::().is_ok() { + return Err(TokenError::IllegalMintBurnConversion.into()); + } + check_program_account(token_account_info.owner)?; let token_account_data = &mut token_account_info.data.borrow_mut(); let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; @@ -508,6 +514,10 @@ fn process_withdraw( return Err(TokenError::MintDecimalsMismatch.into()); } + if mint.get_extension::().is_ok() { + return Err(TokenError::IllegalMintBurnConversion.into()); + } + check_program_account(token_account_info.owner)?; let token_account_data = &mut token_account_info.data.borrow_mut(); let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; @@ -757,6 +767,7 @@ fn process_transfer( Ok(()) } +/// Processes the changes for the sending party of a confidential transfer #[cfg(feature = "zk-ops")] fn process_source_for_transfer( program_id: &Pubkey, diff --git a/token/program-2022/src/extension/mod.rs b/token/program-2022/src/extension/mod.rs index 0cfc0cbdc8f..f2e09ab2a07 100644 --- a/token/program-2022/src/extension/mod.rs +++ b/token/program-2022/src/extension/mod.rs @@ -6,6 +6,7 @@ use { crate::{ error::TokenError, extension::{ + confidential_mint_burn::ConfidentialMintBurn, confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, confidential_transfer_fee::{ ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, @@ -84,6 +85,9 @@ pub mod transfer_fee; /// Transfer Hook extension pub mod transfer_hook; +/// Confidential mint-burn extension +pub mod confidential_mint_burn; + /// Length in TLV structure #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] #[repr(transparent)] @@ -1101,6 +1105,9 @@ pub enum ExtensionType { GroupMemberPointer, /// Mint contains token group member configurations TokenGroupMember, + /// Mint allowing the minting and burning of confidential tokens + ConfidentialMintBurn, + /// Test variable-length mint extension #[cfg(test)] VariableLenMintTest = u16::MAX - 2, @@ -1181,6 +1188,7 @@ impl ExtensionType { ExtensionType::TokenGroup => pod_get_packed_len::(), ExtensionType::GroupMemberPointer => pod_get_packed_len::(), ExtensionType::TokenGroupMember => pod_get_packed_len::(), + ExtensionType::ConfidentialMintBurn => pod_get_packed_len::(), #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -1244,6 +1252,7 @@ impl ExtensionType { | ExtensionType::GroupPointer | ExtensionType::TokenGroup | ExtensionType::GroupMemberPointer + | ExtensionType::ConfidentialMintBurn | ExtensionType::TokenGroupMember => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount @@ -1295,6 +1304,7 @@ impl ExtensionType { let mut transfer_fee_config = false; let mut confidential_transfer_mint = false; let mut confidential_transfer_fee_config = false; + let mut confidential_mint_burn = false; for extension_type in mint_extension_types { match extension_type { @@ -1303,6 +1313,7 @@ impl ExtensionType { ExtensionType::ConfidentialTransferFeeConfig => { confidential_transfer_fee_config = true } + ExtensionType::ConfidentialMintBurn => confidential_mint_burn = true, _ => (), } } @@ -1316,6 +1327,10 @@ impl ExtensionType { return Err(TokenError::InvalidExtensionCombination); } + if confidential_mint_burn && !confidential_transfer_mint { + return Err(TokenError::InvalidExtensionCombination); + } + Ok(()) } } diff --git a/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs index 9b2cb55109b..3d766aa0230 100644 --- a/token/program-2022/src/instruction.rs +++ b/token/program-2022/src/instruction.rs @@ -708,6 +708,9 @@ pub enum TokenInstruction<'a> { /// for further details about the extended instructions that share this /// instruction prefix GroupMemberPointerExtension, + /// Instruction prefix for instructions to the confidential-mint-burn + /// extension + ConfidentialMintBurnExtension, } impl<'a> TokenInstruction<'a> { /// Unpacks a byte buffer into a @@ -847,6 +850,7 @@ impl<'a> TokenInstruction<'a> { 39 => Self::MetadataPointerExtension, 40 => Self::GroupPointerExtension, 41 => Self::GroupMemberPointerExtension, + 42 => Self::ConfidentialMintBurnExtension, _ => return Err(TokenError::InvalidInstruction.into()), }) } @@ -1018,6 +1022,9 @@ impl<'a> TokenInstruction<'a> { &Self::GroupMemberPointerExtension => { buf.push(41); } + &Self::ConfidentialMintBurnExtension => { + buf.push(42); + } }; buf } diff --git a/token/program-2022/src/pod_instruction.rs b/token/program-2022/src/pod_instruction.rs index dcf487c966d..a08f8b68a7a 100644 --- a/token/program-2022/src/pod_instruction.rs +++ b/token/program-2022/src/pod_instruction.rs @@ -114,6 +114,7 @@ pub(crate) enum PodTokenInstruction { // 40 GroupPointerExtension, GroupMemberPointerExtension, + ConfidentialMintBurnExtension, } fn unpack_pubkey_option(input: &[u8]) -> Result, ProgramError> { diff --git a/token/program-2022/src/processor.rs b/token/program-2022/src/processor.rs index 93e00c157de..697cf042f8a 100644 --- a/token/program-2022/src/processor.rs +++ b/token/program-2022/src/processor.rs @@ -5,6 +5,7 @@ use { check_program_account, error::TokenError, extension::{ + confidential_mint_burn::{self, ConfidentialMintBurn}, confidential_transfer::{self, ConfidentialTransferAccount, ConfidentialTransferMint}, confidential_transfer_fee::{ self, ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, @@ -322,6 +323,7 @@ impl Processor { { return Err(TokenError::NonTransferable.into()); } + let (fee, maybe_permanent_delegate, maybe_transfer_hook_program_id) = if let Some((mint_info, expected_decimals)) = expected_mint_info { if &source_account.base.mint != mint_info.key { @@ -369,9 +371,9 @@ impl Processor { .is_ok() { return Err(TokenError::MintRequiredForTransfer.into()); - } else { - (0, None, None) } + + (0, None, None) }; if let Some(expected_fee) = expected_fee { if expected_fee != fee { @@ -954,6 +956,10 @@ impl Processor { return Err(TokenError::NonTransferableNeedsImmutableOwnership.into()); } + if mint.get_extension::().is_ok() { + return Err(TokenError::IllegalMintBurnConversion.into()); + } + if let Some(expected_decimals) = expected_decimals { if expected_decimals != mint.base.decimals { return Err(TokenError::MintDecimalsMismatch.into()); @@ -1793,6 +1799,14 @@ impl Processor { &input[1..], ) } + PodTokenInstruction::ConfidentialMintBurnExtension => { + msg!("Instruction: ConfidentialMintBurnExtension"); + confidential_mint_burn::processor::process_instruction( + program_id, + accounts, + &input[1..], + ) + } } } else if let Ok(instruction) = TokenMetadataInstruction::unpack(input) { token_metadata::processor::process_instruction(program_id, accounts, instruction)