diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 51985806fca..628740b12ff 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.78.0" +channel = "1.79.0" diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index ddbfb242635..ce9da2f2afd 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -81,6 +81,8 @@ pub enum ConfidentialTransferInstruction { /// /// Accounts expected by this instruction: /// + /// TODO: Add an option to include an `ElGamalRegistry` address + /// /// * Single owner/delegate /// 0. `[writeable]` The SPL Token account. /// 1. `[]` The corresponding SPL Token mint. @@ -106,7 +108,8 @@ pub enum ConfidentialTransferInstruction { /// account. /// /// Data expected by this instruction: - /// `ConfigureAccountInstructionData` + /// None if an `ElGamalRegistry` address is provided in the list of accounts + /// `ConfigureAccountInstructionData` otherwise ConfigureAccount, /// Approves a token account for confidential transfers. diff --git a/token/program-2022/src/extension/confidential_transfer/mod.rs b/token/program-2022/src/extension/confidential_transfer/mod.rs index f779db36938..0558938af61 100644 --- a/token/program-2022/src/extension/confidential_transfer/mod.rs +++ b/token/program-2022/src/extension/confidential_transfer/mod.rs @@ -23,6 +23,9 @@ pub const MAXIMUM_DEPOSIT_TRANSFER_AMOUNT: u64 = (u16::MAX as u64) + (1 << 16) * /// Bit length of the low bits of pending balance plaintext pub const PENDING_BALANCE_LO_BIT_LENGTH: u32 = 16; +/// The default maximum pending balance credit counter. +pub const DEFAULT_MAXIMUM_PENDING_BALANCE_CREDIT_COUNTER: u64 = 65536; + /// Confidential Transfer Extension instructions pub mod instruction; diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 49f6ba2d8f7..9609a5b2606 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -22,7 +22,7 @@ use { instruction::{decode_instruction_data, decode_instruction_type}, pod::{PodAccount, PodMint}, processor::Processor, - proof::verify_and_extract_context, + proof::{verify_and_extract_context, ElGamalRegistry}, }, solana_program::{ account_info::{next_account_info, AccountInfo}, @@ -33,6 +33,7 @@ use { pubkey::Pubkey, sysvar::Sysvar, }, + spl_pod::bytemuck::pod_from_bytes, spl_token_confidential_transfer_proof_extraction::{ transfer::TransferProofContext, transfer_with_fee::TransferWithFeeProofContext, }, @@ -92,23 +93,62 @@ fn process_update_mint( Ok(()) } +/// Processes a [ConfigureAccount] instruction with the assumption that an ElGamal registry is +/// provided. +fn process_configure_account_from_registry( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let elgamal_registry_account = accounts.get(2).unwrap(); + + if elgamal_registry_account.owner != program_id { + return Err(TokenError::OwnerMismatch.into()); + } + + let elgamal_registry_account_data = &elgamal_registry_account.data.borrow(); + let elgamal_registry_account = + pod_from_bytes::(elgamal_registry_account_data)?; + + let decryptable_zero_balance = PodAeCiphertext::default(); + let maximum_pending_balance_credit_counter = + DEFAULT_MAXIMUM_PENDING_BALANCE_CREDIT_COUNTER.into(); + + process_configure_account( + program_id, + accounts, + &decryptable_zero_balance, + &maximum_pending_balance_credit_counter, + None, + Some(elgamal_registry_account), + ) +} + /// Processes a [ConfigureAccount] instruction. fn process_configure_account( program_id: &Pubkey, accounts: &[AccountInfo], decryptable_zero_balance: &DecryptableBalance, maximum_pending_balance_credit_counter: &PodU64, - proof_instruction_offset: i64, + proof_instruction_offset: Option, + elgamal_registry_account: Option<&ElGamalRegistry>, ) -> 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)?; - // zero-knowledge proof certifies that the supplied ElGamal public key is valid - let proof_context = verify_and_extract_context::< - PubkeyValidityProofData, - PubkeyValidityProofContext, - >(account_info_iter, proof_instruction_offset, None)?; + let elgamal_pubkey = if let Some(offset) = proof_instruction_offset { + // zero-knowledge proof certifies that the supplied ElGamal public key is valid + let proof_context = verify_and_extract_context::< + PubkeyValidityProofData, + PubkeyValidityProofContext, + >(account_info_iter, offset, None)?; + proof_context.pubkey + } else { + // if proof instruction offset is `None`, then assume that the proof + // was already verified in an ElGamal registry account + let _elgamal_registry_account = next_account_info(account_info_iter)?; + elgamal_registry_account.unwrap().elgamal_pubkey + }; let authority_info = next_account_info(account_info_iter)?; let authority_info_data_len = authority_info.data_len(); @@ -121,13 +161,21 @@ fn process_configure_account( return Err(TokenError::MintMismatch.into()); } - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; + if let Some(registry_account) = elgamal_registry_account { + // if ElGamal registry was provided, then just verify that the registry owner and the + // account match, then skip the signature verification check + if registry_account.owner != *authority_info.key { + return Err(TokenError::OwnerMismatch.into()); + } + } else { + Processor::validate_owner( + program_id, + &token_account.base.owner, + authority_info, + authority_info_data_len, + account_info_iter.as_slice(), + )?; + } check_program_account(mint_info.owner)?; let mint_data = &mut mint_info.data.borrow(); @@ -140,7 +188,7 @@ fn process_configure_account( 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 = proof_context.pubkey; + confidential_transfer_account.elgamal_pubkey = elgamal_pubkey; confidential_transfer_account.maximum_pending_balance_credit_counter = *maximum_pending_balance_credit_counter; @@ -1102,14 +1150,20 @@ pub(crate) fn process_instruction( } ConfidentialTransferInstruction::ConfigureAccount => { msg!("ConfidentialTransferInstruction::ConfigureAccount"); - let data = decode_instruction_data::(input)?; - process_configure_account( - program_id, - accounts, - &data.decryptable_zero_balance, - &data.maximum_pending_balance_credit_counter, - data.proof_instruction_offset as i64, - ) + if input.is_empty() { + // instruction data is empty, so assume an ElGamal registry is provided + process_configure_account_from_registry(program_id, accounts) + } else { + let data = decode_instruction_data::(input)?; + process_configure_account( + program_id, + accounts, + &data.decryptable_zero_balance, + &data.maximum_pending_balance_credit_counter, + Some(data.proof_instruction_offset as i64), + None, + ) + } } ConfidentialTransferInstruction::ApproveAccount => { msg!("ConfidentialTransferInstruction::ApproveAccount"); diff --git a/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs index 9b2cb55109b..0805c66958b 100644 --- a/token/program-2022/src/instruction.rs +++ b/token/program-2022/src/instruction.rs @@ -708,6 +708,42 @@ pub enum TokenInstruction<'a> { /// for further details about the extended instructions that share this /// instruction prefix GroupMemberPointerExtension, + /// Initialize an ElGamal public key registry for an account. + /// + /// 0. `[writable]` The account to initialize + /// 1. `[]` Instructions sysvar if `VerifyPubkeyValidity` is included in + /// the same transaction or context state account if + /// `VerifyPubkeyValidity` is pre-verified into a context state + /// account. + /// 2. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + CreateElGamalRegistry { + /// The owner of the ElGamal registry account + #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] + owner: Pubkey, + /// Relative location of the `ProofInstruction::PubkeyValidityProof` + /// instruction to the `CreateElGamalRegistry` instruction in the + /// transaction. If the offset is `0`, then use a context state account + /// for the proof. + proof_instruction_offset: i8, + }, + /// Update an ElGamal public key registry with a new ElGamal public key. + /// + /// 0. `[writable]` The account to initialize + /// 1. `[signer]` The owner of the ElGamal public key registry + /// 2. `[]` Instructions sysvar if `VerifyPubkeyValidity` is included in + /// the same transaction or context state account if + /// `VerifyPubkeyValidity` is pre-verified into a context state + /// account. + /// 3. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + UpdateElGamalRegistry { + /// Relative location of the `ProofInstruction::PubkeyValidityProof` + /// instruction to the `UpdateElGamalRegistry` instruction in the + /// transaction. If the offset is `0`, then use a context state account + /// for the proof. + proof_instruction_offset: i8, + }, } impl<'a> TokenInstruction<'a> { /// Unpacks a byte buffer into a @@ -1018,6 +1054,20 @@ impl<'a> TokenInstruction<'a> { &Self::GroupMemberPointerExtension => { buf.push(41); } + &Self::CreateElGamalRegistry { + owner, + proof_instruction_offset, + } => { + buf.push(42); + buf.extend_from_slice(owner.as_ref()); + buf.extend_from_slice(&proof_instruction_offset.to_le_bytes()); + } + &Self::UpdateElGamalRegistry { + proof_instruction_offset, + } => { + buf.push(43); + buf.extend_from_slice(&proof_instruction_offset.to_le_bytes()); + } }; buf } diff --git a/token/program-2022/src/pod_instruction.rs b/token/program-2022/src/pod_instruction.rs index dcf487c966d..694768525c6 100644 --- a/token/program-2022/src/pod_instruction.rs +++ b/token/program-2022/src/pod_instruction.rs @@ -53,6 +53,20 @@ pub(crate) struct SetAuthorityData { // The new authority option comes later, but cannot be included as // plain old data in this struct } +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +pub(crate) struct CreateElGamalRegistryData { + /// The owner of the ElGamal registry account + pub(crate) owner: Pubkey, + /// The proof instruction offset + pub(crate) proof_instruction_offset: i8, +} +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +pub(crate) struct UpdateElGamalRegistryData { + /// The proof instruction offset + pub(crate) proof_instruction_offset: i8, +} /// All of the base instructions in Token-2022, reduced down to their one-byte /// discriminant. @@ -114,6 +128,8 @@ pub(crate) enum PodTokenInstruction { // 40 GroupPointerExtension, GroupMemberPointerExtension, + CreateElGamalRegistry, + UpdateElGamalRegistry, } 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..cdbb3862c0c 100644 --- a/token/program-2022/src/processor.rs +++ b/token/program-2022/src/processor.rs @@ -34,8 +34,10 @@ use { pod::{PodAccount, PodCOption, PodMint, PodMultisig}, pod_instruction::{ decode_instruction_data_with_coption_pubkey, AmountCheckedData, AmountData, - InitializeMintData, InitializeMultisigData, PodTokenInstruction, SetAuthorityData, + CreateElGamalRegistryData, InitializeMintData, InitializeMultisigData, + PodTokenInstruction, SetAuthorityData, UpdateElGamalRegistryData, }, + proof::{verify_and_extract_context, ElGamalRegistry}, state::{Account, AccountState, Mint, PackedSizeOf}, }, solana_program::{ @@ -50,6 +52,9 @@ use { system_instruction, system_program, sysvar::{rent::Rent, Sysvar}, }, + solana_zk_sdk::zk_elgamal_proof_program::proof_data::pubkey_validity::{ + PubkeyValidityProofContext, PubkeyValidityProofData, + }, spl_pod::{ bytemuck::{pod_from_bytes, pod_from_bytes_mut}, primitives::{PodBool, PodU64}, @@ -1539,6 +1544,63 @@ impl Processor { Ok(()) } + /// Processes an [CreateElGamalRegistry](enum.TokenInstruction.html) instruction + pub fn process_create_elgamal_registry( + accounts: &[AccountInfo], + owner: &Pubkey, + proof_instruction_offset: i64, + ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let registry_account_info = next_account_info(account_info_iter)?; + + // zero-knowledge proof certifies that the supplied ElGamal public key is valid + let proof_context = verify_and_extract_context::< + PubkeyValidityProofData, + PubkeyValidityProofContext, + >(account_info_iter, proof_instruction_offset, None)?; + + let registry_account_data = &mut registry_account_info.data.borrow_mut(); + let registry_account = pod_from_bytes_mut::(registry_account_data)?; + + registry_account.owner = *owner; + registry_account.elgamal_pubkey = proof_context.pubkey; + + Ok(()) + } + + /// Processes an [UpdateElGamalRegistry](enum.TokenInstruction.html) instruction + pub fn process_update_elgamal_registry( + program_id: &Pubkey, + accounts: &[AccountInfo], + proof_instruction_offset: i64, + ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let registry_account_info = next_account_info(account_info_iter)?; + let owner_info = next_account_info(account_info_iter)?; + let owner_info_data_len = owner_info.data_len(); + + // zero-knowledge proof certifies that the supplied ElGamal public key is valid + let proof_context = verify_and_extract_context::< + PubkeyValidityProofData, + PubkeyValidityProofContext, + >(account_info_iter, proof_instruction_offset, None)?; + + let registry_account_data = &mut registry_account_info.data.borrow_mut(); + let registry_account = pod_from_bytes_mut::(registry_account_data)?; + + Processor::validate_owner( + program_id, + ®istry_account.owner, + owner_info, + owner_info_data_len, + account_info_iter.as_slice(), + )?; + + registry_account.elgamal_pubkey = proof_context.pubkey; + + Ok(()) + } + /// Processes an [Instruction](enum.Instruction.html). pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { if let Ok(instruction_type) = decode_instruction_type(input) { @@ -1793,6 +1855,24 @@ impl Processor { &input[1..], ) } + PodTokenInstruction::CreateElGamalRegistry => { + msg!("Instruction: InitializeElGamalRegistry"); + let data = decode_instruction_data::(input)?; + Self::process_create_elgamal_registry( + accounts, + &data.owner, + data.proof_instruction_offset.into(), + ) + } + PodTokenInstruction::UpdateElGamalRegistry => { + msg!("Instruction: UpdateElGamalRegistry"); + let data = decode_instruction_data::(input)?; + Self::process_update_elgamal_registry( + program_id, + accounts, + data.proof_instruction_offset.into(), + ) + } } } else if let Ok(instruction) = TokenMetadataInstruction::unpack(input) { token_metadata::processor::process_instruction(program_id, accounts, instruction) diff --git a/token/program-2022/src/proof.rs b/token/program-2022/src/proof.rs index 1bebfabe827..2d3c0282f16 100644 --- a/token/program-2022/src/proof.rs +++ b/token/program-2022/src/proof.rs @@ -2,7 +2,7 @@ use { crate::check_zk_elgamal_proof_program_account, - bytemuck::Pod, + bytemuck::{Pod, Zeroable}, solana_program::{ account_info::{next_account_info, AccountInfo}, instruction::Instruction, @@ -11,16 +11,30 @@ use { pubkey::Pubkey, sysvar::instructions::get_instruction_relative, }, - solana_zk_sdk::zk_elgamal_proof_program::{ - self, - instruction::ProofInstruction, - proof_data::{ProofType, ZkProofData}, - state::ProofContextState, + solana_zk_sdk::{ + encryption::pod::elgamal::PodElGamalPubkey, + zk_elgamal_proof_program::{ + self, + instruction::ProofInstruction, + proof_data::{ProofType, ZkProofData}, + state::ProofContextState, + }, }, spl_pod::bytemuck::pod_from_bytes, std::{num::NonZeroI8, slice::Iter}, }; +/// ElGamal public key registry. It contains an ElGamal public key that is +/// associated with a wallet account, but independent of any specific mint. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +pub struct ElGamalRegistry { + /// The owner of the registry + pub owner: Pubkey, + /// The ElGamal public key associated with an account + pub elgamal_pubkey: PodElGamalPubkey, +} + /// If a proof is to be read from a record account, the proof instruction data /// must be 5 bytes: 1 byte for the proof type and 4 bytes for the u32 offset const INSTRUCTION_DATA_LENGTH_WITH_RECORD_ACCOUNT: usize = 5;