Repository: solana-labs/solana-program-library
Pull request #7341 (2024-10-08 21:24:58)

[token-2022] Add elgamal registry account
#### Problem
Currently, it is not possible to use associated token accounts for confidential transfers because in order to initialize a confidential transfer account, the owner must initialize it with an ElGamal public key.

It would be nice to have a special program "registry" program where a user can store an ElGamal public key that it can use for any mint with confidential transfers enabled.

#### Summary of Changes
- I created a simple ElGamal registry program where a user can store its ElGamal public key in a PDA account that is derived from the user's wallet address - [ba60c3d](https://github.com/solana-labs/solana-program-library/pull/7341/commits/ba60c3d2dc3b3365fe3e9819dce67e75d4bb4676). The program has two instructions. The `CreateRegistry` instruction derives the PDA, invokes the system program to create the account, and then initializes the registry account. The `UpdateRegistry` instruction checks the owner signature and updates the old ElGamal key with a new key. I believe the `UpdateRegistry` instruction does not need to check the PDA address, but please correct me if I am overlooking something 🙏 .
  - In the process, I refactored the proof extraction logic that lived in `program-2022` to `confidential-transfer-proof-extraction` module since this will be shared by both program-2022 and ElGamal registry program - [eef0666](https://github.com/solana-labs/solana-program-library/pull/7341/commits/eef066656694bb866b161625281506f50d970e21)
- I added a new `ConfigureAccountWithRegistry` instruction in the confidential transfer extension which is identical to the `ConfigureAccount` instruction, but skips the signature and zk proof verification - [3104459](https://github.com/solana-labs/solana-program-library/pull/7341/commits/310445934de5dbad3f3cea262e16403b08c11201). I believe the token program itself just need to check the provided registry account is owned by the registry program and does not need to necessarily check the validity of the PDA address, but please correct me if I am overlooking something 🙏 . Another thing to note is the fact that on `ConfigureAccountWithRegistry` instruction, the program initializes the confidential transfer account with default maximum pending credits counter and also `AeCiphertext::default()` (all zero ciphertext). Unlike ElGamal ciphertexts, the all-zero symmetric ciphertext is not really valid, but I added this logic to simplify the program at the cost of increasing the complexity slightly on the client side (client can just interpret all zero ciphertext as encrypting 0). An alternative would be to add a valid zero-ciphertext field to the ElGamal registry account, but I thought this would make things a bit more complicated.
- Added support for `ConfigureAccountWithRegistry` instruction in the token-client - [1a8dc29](https://github.com/solana-labs/solana-program-library/pull/7341/commits/1a8dc290ebc172d94d8ce05f452216a16e3a1a01)
- Added tests to `program-2022-test` - https://github.com/solana-labs/solana-program-library/pull/7341/commits/48624649a2a574726ad46e977283ca963207db55



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 2acd3d431..6ac47cf5e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6989,6 +6989,17 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "spl-elgamal-registry"
+version = "0.1.0"
+dependencies = [
+ "bytemuck",
+ "solana-program",
+ "solana-zk-sdk",
+ "spl-pod 0.4.0",
+ "spl-token-confidential-transfer-proof-extraction",
+]
+
 [[package]]
 name = "spl-example-cross-program-invocation"
 version = "1.0.0"
@@ -7588,6 +7599,7 @@ dependencies = [
  "solana-sdk",
  "solana-security-txt",
  "solana-zk-sdk",
+ "spl-elgamal-registry",
  "spl-memo 5.0.0",
  "spl-pod 0.4.0",
  "spl-tlv-account-resolution 0.8.1",
@@ -7614,6 +7626,7 @@ dependencies = [
  "solana-program-test",
  "solana-sdk",
  "spl-associated-token-account 5.0.1",
+ "spl-elgamal-registry",
  "spl-instruction-padding",
  "spl-memo 5.0.0",
  "spl-pod 0.4.0",
@@ -7621,6 +7634,7 @@ dependencies = [
  "spl-tlv-account-resolution 0.8.1",
  "spl-token-2022 5.0.2",
  "spl-token-client",
+ "spl-token-confidential-transfer-proof-extraction",
  "spl-token-confidential-transfer-proof-generation",
  "spl-token-group-interface 0.4.2",
  "spl-token-metadata-interface 0.5.1",
@@ -7685,10 +7699,12 @@ dependencies = [
  "solana-rpc-client-api",
  "solana-sdk",
  "spl-associated-token-account-client",
+ "spl-elgamal-registry",
  "spl-memo 5.0.0",
  "spl-record",
  "spl-token 6.0.0",
  "spl-token-2022 5.0.2",
+ "spl-token-confidential-transfer-proof-extraction",
  "spl-token-confidential-transfer-proof-generation",
  "spl-token-group-interface 0.4.2",
  "spl-token-metadata-interface 0.5.1",
@@ -7732,7 +7748,9 @@ version = "0.1.0"
 dependencies = [
  "bytemuck",
  "solana-curve25519",
+ "solana-program",
  "solana-zk-sdk",
+ "spl-pod 0.4.0",
  "thiserror",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 76890c341..1e8fb54a8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -75,6 +75,7 @@ members = [
   "token/confidential-transfer/proof-extraction",
   "token/confidential-transfer/proof-generation",
   "token/confidential-transfer/proof-tests",
+  "token/confidential-transfer/elgamal-registry",
   "token/client",
   "utils/cgen",
   "utils/test-client",
diff --git a/token/client/Cargo.toml b/token/client/Cargo.toml
index 2ecf6710e..b5d526e99 100644
--- a/token/client/Cargo.toml
+++ b/token/client/Cargo.toml
@@ -20,6 +20,7 @@ solana-rpc-client = "2.0.3"
 solana-rpc-client-api = "2.0.3"
 solana-sdk = "2.0.3"
 spl-associated-token-account-client = { version = "1.0.0", path = "../../associated-token-account/client" }
+spl-elgamal-registry = { version = "0.1.0", path = "../confidential-transfer/elgamal-registry"}
 spl-memo = { version = "5.0", path = "../../memo/program", features = [
   "no-entrypoint",
 ] }
@@ -27,6 +28,7 @@ spl-record = { version = "0.2.1", path = "../../record/program", features = ["no
 spl-token = { version = "6.0", path = "../program", features = [
   "no-entrypoint",
 ] }
+spl-token-confidential-transfer-proof-extraction = { version = "0.1.0", path = "../confidential-transfer/proof-extraction" }
 spl-token-confidential-transfer-proof-generation = { version = "0.1.0", path = "../confidential-transfer/proof-generation" }
 spl-token-2022 = { version = "5.0.2", path = "../program-2022" }
 spl-token-group-interface = { version = "0.4.2", path = "../../token-group/interface" }
diff --git a/token/client/src/token.rs b/token/client/src/token.rs
index 165616a64..33bab1514 100644
--- a/token/client/src/token.rs
+++ b/token/client/src/token.rs
@@ -47,7 +47,6 @@ use {
             BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsOwned,
         },
         instruction, offchain,
-        proof::{zk_proof_type_to_instruction, ProofData, ProofLocation},
         solana_zk_sdk::{
             encryption::{
                 auth_encryption::AeKey,
@@ -63,6 +62,9 @@ use {
         },
         state::{Account, AccountState, Mint, Multisig},
     },
+    spl_token_confidential_transfer_proof_extraction::instruction::{
+        zk_proof_type_to_instruction, ProofData, ProofLocation,
+    },
     spl_token_confidential_transfer_proof_generation::{
         transfer::TransferProofData, transfer_with_fee::TransferWithFeeProofData,
         withdraw::WithdrawProofData,
@@ -1972,6 +1974,29 @@ where
         .await
     }
 
+    /// Configures confidential transfers for a token account using an ElGamal
+    /// registry account
+    pub async fn confidential_transfer_configure_token_account_with_registry(
+        &self,
+        account: &Pubkey,
+        elgamal_registry_account: &Pubkey,
+        payer: Option<&Pubkey>,
+    ) -> TokenResult<T::Output> {
+        self.process_ixs::<[&dyn Signer; 0]>(
+            &[
+                confidential_transfer::instruction::configure_account_with_registry(
+                    &self.program_id,
+                    account,
+                    &self.pubkey,
+                    elgamal_registry_account,
+                    payer,
+                )?,
+            ],
+            &[],
+        )
+        .await
+    }
+
     /// Approves a token account for confidential transfers
     pub async fn confidential_transfer_approve_account<S: Signers>(
         &self,
diff --git a/token/confidential-transfer/elgamal-registry/Cargo.toml b/token/confidential-transfer/elgamal-registry/Cargo.toml
new file mode 100644
index 000000000..5c1ac47e9
--- /dev/null
+++ b/token/confidential-transfer/elgamal-registry/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "spl-elgamal-registry"
+version = "0.1.0"
+description = "Solana ElGamal Registry Program"
+authors = ["Solana Labs Maintainers <maintainers@solanalabs.com>"]
+repository = "https://github.com/solana-labs/solana-program-library"
+license = "Apache-2.0"
+edition = "2021"
+
+[features]
+no-entrypoint = []
+test-sbf = []
+
+[dependencies]
+bytemuck = { version = "1.18.0", features = ["derive"] }
+solana-program = "2.0.3"
+solana-zk-sdk = "2.0.3"
+spl-pod = { version = "0.4.0", path = "../../../libraries/pod" }
+spl-token-confidential-transfer-proof-extraction = { version = "0.1.0", path = "../proof-extraction" }
+
+[lib]
+crate-type = ["cdylib", "lib"]
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
diff --git a/token/confidential-transfer/elgamal-registry/src/entrypoint.rs b/token/confidential-transfer/elgamal-registry/src/entrypoint.rs
new file mode 100644
index 000000000..62a02f2a4
--- /dev/null
+++ b/token/confidential-transfer/elgamal-registry/src/entrypoint.rs
@@ -0,0 +1,14 @@
+//! Program entrypoint
+
+#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))]
+
+use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
+
+solana_program::entrypoint!(process_instruction);
+fn process_instruction(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    instruction_data: &[u8],
+) -> ProgramResult {
+    crate::processor::process_instruction(program_id, accounts, instruction_data)
+}
diff --git a/token/confidential-transfer/elgamal-registry/src/instruction.rs b/token/confidential-transfer/elgamal-registry/src/instruction.rs
new file mode 100644
index 000000000..0fa33c9ca
--- /dev/null
+++ b/token/confidential-transfer/elgamal-registry/src/instruction.rs
@@ -0,0 +1,194 @@
+use {
+    crate::{get_elgamal_registry_address, id},
+    solana_program::{
+        instruction::{AccountMeta, Instruction},
+        program_error::ProgramError,
+        pubkey::Pubkey,
+        system_program, sysvar,
+    },
+    solana_zk_sdk::zk_elgamal_proof_program::{
+        instruction::ProofInstruction, proof_data::PubkeyValidityProofData,
+    },
+    spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation},
+};
+
+#[derive(Clone, Debug, PartialEq)]
+#[repr(u8)]
+pub enum RegistryInstruction {
+    /// Initialize an ElGamal public key registry.
+    ///
+    /// 0. `[writable]` The account to be created
+    /// 1. `[signer]` The wallet address (will also be the owner address for the
+    ///    registry account)
+    /// 2. `[]` System program
+    /// 3. `[]` Instructions sysvar if `VerifyPubkeyValidity` is included in the
+    ///    same transaction or context state account if `VerifyPubkeyValidity`
+    ///    is pre-verified into a context state account.
+    /// 4. `[]` (Optional) Record account if the accompanying proof is to be
+    ///    read from a record account.
+    CreateRegistry {
+        /// 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 ElGamal registry account
+    /// 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.
+    /// 3. `[signer]` The owner of the ElGamal public key registry
+    UpdateRegistry {
+        /// 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 RegistryInstruction {
+    /// Unpacks a byte buffer into a `RegistryInstruction`
+    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
+        let (&tag, rest) = input
+            .split_first()
+            .ok_or(ProgramError::InvalidInstructionData)?;
+
+        Ok(match tag {
+            0 => {
+                let proof_instruction_offset =
+                    *rest.first().ok_or(ProgramError::InvalidInstructionData)?;
+                Self::CreateRegistry {
+                    proof_instruction_offset: proof_instruction_offset as i8,
+                }
+            }
+            1 => {
+                let proof_instruction_offset =
+                    *rest.first().ok_or(ProgramError::InvalidInstructionData)?;
+                Self::UpdateRegistry {
+                    proof_instruction_offset: proof_instruction_offset as i8,
+                }
+            }
+            _ => return Err(ProgramError::InvalidInstructionData),
+        })
+    }
+
+    /// Packs a `RegistryInstruction` into a byte buffer.
+    pub fn pack(&self) -> Vec<u8> {
+        let mut buf = vec![];
+        match self {
+            Self::CreateRegistry {
+                proof_instruction_offset,
+            } => {
+                buf.push(0);
+                buf.extend_from_slice(&proof_instruction_offset.to_le_bytes());
+            }
+            Self::UpdateRegistry {
+                proof_instruction_offset,
+            } => {
+                buf.push(1);
+                buf.extend_from_slice(&proof_instruction_offset.to_le_bytes());
+            }
+        };
+        buf
+    }
+}
+
+/// Create a `RegistryInstruction::CreateRegistry` instruction
+pub fn create_registry(
+    owner_address: &Pubkey,
+    proof_location: ProofLocation<PubkeyValidityProofData>,
+) -> Result<Vec<Instruction>, ProgramError> {
+    let elgamal_registry_address = get_elgamal_registry_address(owner_address, &id());
+
+    let mut accounts = vec![
+        AccountMeta::new(elgamal_registry_address, false),
+        AccountMeta::new_readonly(*owner_address, true),
+        AccountMeta::new_readonly(system_program::id(), false),
+    ];
+    let proof_instruction_offset = proof_instruction_offset(&mut accounts, proof_location);
+
+    let mut instructions = vec![Instruction {
+        program_id: id(),
+        accounts,
+        data: RegistryInstruction::CreateRegistry {
+            proof_instruction_offset,
+        }
+        .pack(),
+    }];
+    append_zk_elgamal_proof(&mut instructions, proof_location)?;
+    Ok(instructions)
+}
+
+/// Create a `RegistryInstruction::UpdateRegistry` instruction
+pub fn update_registry(
+    owner_address: &Pubkey,
+    proof_location: ProofLocation<PubkeyValidityProofData>,
+) -> Result<Vec<Instruction>, ProgramError> {
+    let elgamal_registry_address = get_elgamal_registry_address(owner_address, &id());
+
+    let mut accounts = vec![AccountMeta::new(elgamal_registry_address, false)];
+    let proof_instruction_offset = proof_instruction_offset(&mut accounts, proof_location);
+    accounts.push(AccountMeta::new_readonly(*owner_address, true));
+
+    let mut instructions = vec![Instruction {
+        program_id: id(),
+        accounts,
+        data: RegistryInstruction::UpdateRegistry {
+            proof_instruction_offset,
+        }
+        .pack(),
+    }];
+    append_zk_elgamal_proof(&mut instructions, proof_location)?;
+    Ok(instructions)
+}
+
+/// Takes a `ProofLocation`, updates the list of accounts, and returns a
+/// suitable proof location
+fn proof_instruction_offset(
+    accounts: &mut Vec<AccountMeta>,
+    proof_location: ProofLocation<PubkeyValidityProofData>,
+) -> i8 {
+    match proof_location {
+        ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => {
+            accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false));
+            if let ProofData::RecordAccount(record_address, _) = proof_data {
+                accounts.push(AccountMeta::new_readonly(*record_address, false));
+            }
+            proof_instruction_offset.into()
+        }
+        ProofLocation::ContextStateAccount(context_state_account) => {
+            accounts.push(AccountMeta::new_readonly(*context_state_account, false));
+            0
+        }
+    }
+}
+
+/// Takes a `RegistryInstruction` and appends the pubkey validity proof
+/// instruction
+fn append_zk_elgamal_proof(
+    instructions: &mut Vec<Instruction>,
+    proof_data_location: ProofLocation<PubkeyValidityProofData>,
+) -> Result<(), ProgramError> {
+    if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) =
+        proof_data_location
+    {
+        let proof_instruction_offset: i8 = proof_instruction_offset.into();
+        if proof_instruction_offset != 1 {
+            return Err(ProgramError::InvalidArgument);
+        }
+        match proof_data {
+            ProofData::InstructionData(data) => instructions
+                .push(ProofInstruction::VerifyPubkeyValidity.encode_verify_proof(None, data)),
+            ProofData::RecordAccount(address, offset) => instructions.push(
+                ProofInstruction::VerifyPubkeyValidity
+                    .encode_verify_proof_from_account(None, address, offset),
+            ),
+        }
+    }
+    Ok(())
+}
diff --git a/token/confidential-transfer/elgamal-registry/src/lib.rs b/token/confidential-transfer/elgamal-registry/src/lib.rs
new file mode 100644
index 000000000..895f42daa
--- /dev/null
+++ b/token/confidential-transfer/elgamal-registry/src/lib.rs
@@ -0,0 +1,28 @@
+mod entrypoint;
+pub mod instruction;
+pub mod processor;
+pub mod state;
+
+use solana_program::pubkey::Pubkey;
+
+/// Seed for the ElGamal registry program-derived address
+pub const REGISTRY_ADDRESS_SEED: &[u8] = b"elgamal-registry";
+
+/// Derives the ElGamal registry account address and seed for the given wallet
+/// address
+pub fn get_elgamal_registry_address_and_bump_seed(
+    wallet_address: &Pubkey,
+    program_id: &Pubkey,
+) -> (Pubkey, u8) {
+    Pubkey::find_program_address(
+        &[REGISTRY_ADDRESS_SEED, wallet_address.as_ref()],
+        program_id,
+    )
+}
+
+/// Derives the ElGamal registry account address for the given wallet address
+pub fn get_elgamal_registry_address(wallet_address: &Pubkey, program_id: &Pubkey) -> Pubkey {
+    get_elgamal_registry_address_and_bump_seed(wallet_address, program_id).0
+}
+
+solana_program::declare_id!("regVYJW7tcT8zipN5YiBvHsvR5jXW1uLFxaHSbugABg");
diff --git a/token/confidential-transfer/elgamal-registry/src/processor.rs b/token/confidential-transfer/elgamal-registry/src/processor.rs
new file mode 100644
index 000000000..53be973c6
--- /dev/null
+++ b/token/confidential-transfer/elgamal-registry/src/processor.rs
@@ -0,0 +1,166 @@
+use {
+    crate::{
+        get_elgamal_registry_address_and_bump_seed,
+        instruction::RegistryInstruction,
+        state::{ElGamalRegistry, ELGAMAL_REGISTRY_ACCOUNT_LEN},
+        REGISTRY_ADDRESS_SEED,
+    },
+    solana_program::{
+        account_info::{next_account_info, AccountInfo},
+        entrypoint::ProgramResult,
+        msg,
+        program::invoke_signed,
+        program_error::ProgramError,
+        pubkey::Pubkey,
+        rent::Rent,
+        system_instruction,
+        sysvar::Sysvar,
+    },
+    solana_zk_sdk::zk_elgamal_proof_program::proof_data::pubkey_validity::{
+        PubkeyValidityProofContext, PubkeyValidityProofData,
+    },
+    spl_pod::bytemuck::pod_from_bytes_mut,
+    spl_token_confidential_transfer_proof_extraction::instruction::verify_and_extract_context,
+};
+
+/// Processes `CreateRegistry` instruction
+pub fn process_create_registry_account(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    proof_instruction_offset: i64,
+) -> ProgramResult {
+    let account_info_iter = &mut accounts.iter();
+    let elgamal_registry_account_info = next_account_info(account_info_iter)?;
+    let wallet_account_info = next_account_info(account_info_iter)?;
+    let system_program_info = next_account_info(account_info_iter)?;
+
+    if !wallet_account_info.is_signer {
+        return Err(ProgramError::MissingRequiredSignature);
+    }
+
+    // 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_registry_account_address, bump_seed) =
+        get_elgamal_registry_address_and_bump_seed(wallet_account_info.key, program_id);
+    if elgamal_registry_account_address != *elgamal_registry_account_info.key {
+        msg!("Error: ElGamal registry account address does not match seed derivation");
+        return Err(ProgramError::InvalidSeeds);
+    }
+
+    let elgamal_registry_account_seeds: &[&[_]] = &[
+        REGISTRY_ADDRESS_SEED,
+        wallet_account_info.key.as_ref(),
+        &[bump_seed],
+    ];
+    let rent = Rent::get()?;
+
+    create_pda_account(
+        &rent,
+        ELGAMAL_REGISTRY_ACCOUNT_LEN,
+        program_id,
+        system_program_info,
+        elgamal_registry_account_info,
+        elgamal_registry_account_seeds,
+    )?;
+
+    let elgamal_registry_account_data = &mut elgamal_registry_account_info.data.borrow_mut();
+    let elgamal_registry_account =
+        pod_from_bytes_mut::<ElGamalRegistry>(elgamal_registry_account_data)?;
+    elgamal_registry_account.owner = *wallet_account_info.key;
+    elgamal_registry_account.elgamal_pubkey = proof_context.pubkey;
+
+    Ok(())
+}
+
+/// Processes `UpdateRegistry` instruction
+pub fn process_update_registry_account(
+    _program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    proof_instruction_offset: i64,
+) -> ProgramResult {
+    let account_info_iter = &mut accounts.iter();
+    let elgamal_registry_account_info = next_account_info(account_info_iter)?;
+    let elgamal_registry_account_data = &mut elgamal_registry_account_info.data.borrow_mut();
+    let elgamal_registry_account =
+        pod_from_bytes_mut::<ElGamalRegistry>(elgamal_registry_account_data)?;
+
+    // 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 owner_info = next_account_info(account_info_iter)?;
+    validate_owner(owner_info, &elgamal_registry_account.owner)?;
+
+    elgamal_registry_account.elgamal_pubkey = proof_context.pubkey;
+    Ok(())
+}
+
+/// Instruction processor
+pub fn process_instruction(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    input: &[u8],
+) -> ProgramResult {
+    let instruction = RegistryInstruction::unpack(input)?;
+    match instruction {
+        RegistryInstruction::CreateRegistry {
+            proof_instruction_offset,
+        } => {
+            msg!("ElGamalRegistryInstruction::CreateRegistry");
+            process_create_registry_account(program_id, accounts, proof_instruction_offset as i64)
+        }
+        RegistryInstruction::UpdateRegistry {
+            proof_instruction_offset,
+        } => {
+            msg!("ElGamalRegistryInstruction::UpdateRegistry");
+            process_update_registry_account(program_id, accounts, proof_instruction_offset as i64)
+        }
+    }
+}
+
+fn validate_owner(owner_info: &AccountInfo, expected_owner: &Pubkey) -> ProgramResult {
+    if expected_owner != owner_info.key {
+        return Err(ProgramError::InvalidAccountOwner);
+    }
+    if !owner_info.is_signer {
+        return Err(ProgramError::MissingRequiredSignature);
+    }
+    Ok(())
+}
+
+/// Allocate ElGamal registry account using Program Derived Address for the
+/// given seeds
+pub fn create_pda_account<'a>(
+    rent: &Rent,
+    space: usize,
+    owner: &Pubkey,
+    system_program: &AccountInfo<'a>,
+    new_pda_account: &AccountInfo<'a>,
+    new_pda_signer_seeds: &[&[u8]],
+) -> ProgramResult {
+    let required_lamports = rent
+        .minimum_balance(space)
+        .saturating_sub(new_pda_account.lamports());
+
+    if required_lamports > 0 {
+        return Err(ProgramError::AccountNotRentExempt);
+    }
+
+    invoke_signed(
+        &system_instruction::allocate(new_pda_account.key, space as u64),
+        &[new_pda_account.clone(), system_program.clone()],
+        &[new_pda_signer_seeds],
+    )?;
+
+    invoke_signed(
+        &system_instruction::assign(new_pda_account.key, owner),
+        &[new_pda_account.clone(), system_program.clone()],
+        &[new_pda_signer_seeds],
+    )
+}
diff --git a/token/confidential-transfer/elgamal-registry/src/state.rs b/token/confidential-transfer/elgamal-registry/src/state.rs
new file mode 100644
index 000000000..55e77689d
--- /dev/null
+++ b/token/confidential-transfer/elgamal-registry/src/state.rs
@@ -0,0 +1,18 @@
+use {
+    bytemuck::{Pod, Zeroable},
+    solana_program::pubkey::Pubkey,
+    solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey,
+};
+
+pub const ELGAMAL_REGISTRY_ACCOUNT_LEN: usize = 64;
+
+/// 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,
+}
diff --git a/token/confidential-transfer/proof-extraction/Cargo.toml b/token/confidential-transfer/proof-extraction/Cargo.toml
index bbe079778..c9065f2a7 100644
--- a/token/confidential-transfer/proof-extraction/Cargo.toml
+++ b/token/confidential-transfer/proof-extraction/Cargo.toml
@@ -10,7 +10,9 @@ edition = "2021"
 [dependencies]
 bytemuck = "1.19.0"
 solana-curve25519 = "2.0.3"
+solana-program = "2.0.3"
 solana-zk-sdk = "2.0.3"
+spl-pod = { version = "0.4.0", path = "../../../libraries/pod" }
 thiserror = "1.0.65"
 
 [lib]
diff --git a/token/program-2022/src/proof.rs b/token/confidential-transfer/proof-extraction/src/instruction.rs
similarity index 91%
rename from token/program-2022/src/proof.rs
rename to token/confidential-transfer/proof-extraction/src/instruction.rs
index 1bebfabe8..761a2b3fc 100644
--- a/token/program-2022/src/proof.rs
+++ b/token/confidential-transfer/proof-extraction/src/instruction.rs
@@ -1,10 +1,11 @@
-//! Helper for processing instruction data from ZK ElGamal proof program
+//! Utility functions to simplify the handling of ZK ElGamal proof program
+//! instruction data in SPL crates
 
 use {
-    crate::check_zk_elgamal_proof_program_account,
     bytemuck::Pod,
     solana_program::{
         account_info::{next_account_info, AccountInfo},
+        entrypoint::ProgramResult,
         instruction::Instruction,
         msg,
         program_error::ProgramError,
@@ -21,6 +22,17 @@ use {
     std::{num::NonZeroI8, slice::Iter},
 };
 
+/// Checks that the supplied program ID is correct for the ZK ElGamal proof
+/// program
+pub fn check_zk_elgamal_proof_program_account(
+    zk_elgamal_proof_program_id: &Pubkey,
+) -> ProgramResult {
+    if zk_elgamal_proof_program_id != &solana_zk_sdk::zk_elgamal_proof_program::id() {
+        return Err(ProgramError::IncorrectProgramId);
+    }
+    Ok(())
+}
+
 /// 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;
@@ -167,15 +179,3 @@ pub fn zk_proof_type_to_instruction(
         ProofType::Uninitialized => Err(ProgramError::InvalidInstructionData),
     }
 }
-
-/// Instruction options for when using split context state accounts
-#[derive(Clone, Copy)]
-pub struct SplitContextStateAccountsConfig {
-    /// If true, execute no op when an associated split proof context state
-    /// account is not initialized. Otherwise, fail on an uninitialized
-    /// context state account.
-    pub no_op_on_uninitialized_split_context_state: bool,
-    /// Close associated context states after a complete execution of the
-    /// transfer instruction.
-    pub close_split_context_state_on_execution: bool,
-}
diff --git a/token/confidential-transfer/proof-extraction/src/lib.rs b/token/confidential-transfer/proof-extraction/src/lib.rs
index f3f99bc10..82dcff0d3 100644
--- a/token/confidential-transfer/proof-extraction/src/lib.rs
+++ b/token/confidential-transfer/proof-extraction/src/lib.rs
@@ -1,6 +1,7 @@
 pub mod burn;
 pub mod encryption;
 pub mod errors;
+pub mod instruction;
 pub mod mint;
 pub mod transfer;
 pub mod transfer_with_fee;
diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml
index a62489d40..145a59f7c 100644
--- a/token/program-2022/Cargo.toml
+++ b/token/program-2022/Cargo.toml
@@ -25,6 +25,7 @@ num_enum = "0.7.3"
 solana-program = "2.0.3"
 solana-security-txt = "1.1.1"
 solana-zk-sdk = "2.0.3"
+spl-elgamal-registry = { version = "0.1.0", path = "../confidential-transfer/elgamal-registry", features = ["no-entrypoint"] }
 spl-memo = { version = "5.0", path = "../../memo/program", features = [ "no-entrypoint" ] }
 spl-token = { version = "6.0",  path = "../program", features = ["no-entrypoint"] }
 spl-token-confidential-transfer-ciphertext-arithmetic = { version = "0.1.0", path = "../confidential-transfer/ciphertext-arithmetic" }
diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs
index ddbfb2426..e82f4f521 100644
--- a/token/program-2022/src/extension/confidential_transfer/instruction.rs
+++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs
@@ -11,7 +11,6 @@ use {
         check_program_account,
         extension::confidential_transfer::*,
         instruction::{encode_instruction, TokenInstruction},
-        proof::{ProofData, ProofLocation},
     },
     bytemuck::Zeroable,
     num_enum::{IntoPrimitive, TryFromPrimitive},
@@ -19,8 +18,9 @@ use {
         instruction::{AccountMeta, Instruction},
         program_error::ProgramError,
         pubkey::Pubkey,
-        sysvar,
+        system_program, sysvar,
     },
+    spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation},
 };
 
 /// Confidential Transfer extension instructions
@@ -472,6 +472,37 @@ pub enum ConfidentialTransferInstruction {
     /// Data expected by this instruction:
     ///   `TransferWithFeeInstructionData`
     TransferWithFee,
+
+    /// Configures confidential transfers for a token account.
+    ///
+    /// This instruction is identical to the `ConfigureAccount` account except
+    /// that a valid `ElGamalRegistry` account is expected in place of the
+    /// `VerifyPubkeyValidity` proof.
+    ///
+    /// An `ElGamalRegistry` account is valid if it shares the same owner with
+    /// the token account. If a valid `ElGamalRegistry` account is provided,
+    /// then the program skips the verification of the ElGamal pubkey
+    /// validity proof as well as the token owner signature.
+    ///
+    /// If the token account is not large enough to include the new
+    /// cconfidential transfer extension, then optionally reallocate the
+    /// account to increase the data size. To reallocate, a payer account to
+    /// fund the reallocation and the system account should be included in the
+    /// instruction.
+    ///
+    /// Accounts expected by this instruction:
+    ///
+    ///   * Single owner/delegate
+    ///   0. `[writable]` The SPL Token account.
+    ///   1. `[]` The corresponding SPL Token mint.
+    ///   2. `[]` The ElGamal registry account.
+    ///   3. `[signer, writable]` (Optional) The payer account to fund
+    ///      reallocation
+    ///   4. `[]` (Optional) System program for reallocation funding
+    ///
+    /// Data expected by this instruction:
+    ///   None
+    ConfigureAccountWithRegistry,
 }
 
 /// Data expected by `ConfidentialTransferInstruction::InitializeMint`
@@ -1701,3 +1732,31 @@ pub fn transfer_with_fee(
 
     Ok(instructions)
 }
+
+/// Create a `ConfigureAccountWithRegistry` instruction
+pub fn configure_account_with_registry(
+    token_program_id: &Pubkey,
+    token_account: &Pubkey,
+    mint: &Pubkey,
+    elgamal_registry_account: &Pubkey,
+    payer: Option<&Pubkey>,
+) -> Result<Instruction, ProgramError> {
+    check_program_account(token_program_id)?;
+    let mut accounts = vec![
+        AccountMeta::new(*token_account, false),
+        AccountMeta::new_readonly(*mint, false),
+        AccountMeta::new_readonly(*elgamal_registry_account, false),
+    ];
+    if let Some(payer) = payer {
+        accounts.push(AccountMeta::new(*payer, true));
+        accounts.push(AccountMeta::new_readonly(system_program::id(), false));
+    }
+
+    Ok(encode_instruction(
+        token_program_id,
+        accounts,
+        TokenInstruction::ConfidentialTransferExtension,
+        ConfidentialTransferInstruction::ConfigureAccountWithRegistry,
+        &(),
+    ))
+}
diff --git a/token/program-2022/src/extension/confidential_transfer/mod.rs b/token/program-2022/src/extension/confidential_transfer/mod.rs
index f779db369..0558938af 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 49f6ba2d8..a93b5e852 100644
--- a/token/program-2022/src/extension/confidential_transfer/processor.rs
+++ b/token/program-2022/src/extension/confidential_transfer/processor.rs
@@ -6,7 +6,7 @@ use {
 };
 use {
     crate::{
-        check_program_account,
+        check_elgamal_registry_program_account, check_program_account,
         error::TokenError,
         extension::{
             confidential_transfer::{instruction::*, verify_proof::*, *},
@@ -15,6 +15,7 @@ use {
                 EncryptedWithheldAmount,
             },
             memo_transfer::{check_previous_sibling_instruction_is_memo, memo_required},
+            set_account_type,
             transfer_fee::TransferFeeConfig,
             transfer_hook, BaseStateWithExtensions, BaseStateWithExtensionsMut,
             PodStateWithExtensions, PodStateWithExtensionsMut,
@@ -22,19 +23,24 @@ use {
         instruction::{decode_instruction_data, decode_instruction_type},
         pod::{PodAccount, PodMint},
         processor::Processor,
-        proof::verify_and_extract_context,
+        state::Account,
     },
     solana_program::{
         account_info::{next_account_info, AccountInfo},
         clock::Clock,
         entrypoint::ProgramResult,
         msg,
+        program::invoke,
         program_error::ProgramError,
         pubkey::Pubkey,
-        sysvar::Sysvar,
+        system_instruction,
+        sysvar::{rent::Rent, Sysvar},
     },
+    spl_elgamal_registry::state::ElGamalRegistry,
+    spl_pod::bytemuck::pod_from_bytes,
     spl_token_confidential_transfer_proof_extraction::{
-        transfer::TransferProofContext, transfer_with_fee::TransferWithFeeProofContext,
+        instruction::verify_and_extract_context, transfer::TransferProofContext,
+        transfer_with_fee::TransferWithFeeProofContext,
     },
 };
 
@@ -92,26 +98,128 @@ fn process_update_mint(
     Ok(())
 }
 
+enum ElGamalPubkeySource<'a> {
+    ProofInstructionOffset(i64),
+    ElGamalRegistry(&'a ElGamalRegistry),
+}
+
+/// Processes a [ConfigureAccountWithRegistry] instruction.
+fn process_configure_account_with_registry(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+) -> 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)?;
+    let elgamal_registry_account = next_account_info(account_info_iter)?;
+
+    check_elgamal_registry_program_account(elgamal_registry_account.owner)?;
+
+    // if a payer account for reallcation is provided, then reallocate
+    if let Ok(payer_info) = next_account_info(account_info_iter) {
+        let system_program_info = next_account_info(account_info_iter)?;
+        reallocate_for_configure_account_with_registry(
+            token_account_info,
+            payer_info,
+            system_program_info,
+        )?;
+    }
+
+    let elgamal_registry_account_data = &elgamal_registry_account.data.borrow();
+    let elgamal_registry_account =
+        pod_from_bytes::<ElGamalRegistry>(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,
+        ElGamalPubkeySource::ElGamalRegistry(elgamal_registry_account),
+    )
+}
+
+fn reallocate_for_configure_account_with_registry<'a>(
+    token_account_info: &AccountInfo<'a>,
+    payer_info: &AccountInfo<'a>,
+    system_program_info: &AccountInfo<'a>,
+) -> ProgramResult {
+    let mut current_extension_types = {
+        let token_account = token_account_info.data.borrow();
+        let account = PodStateWithExtensions::<PodAccount>::unpack(&token_account)?;
+        account.get_extension_types()?
+    };
+    // `try_calculate_account_len` dedupes extension types, so always push
+    // the `ConfidentialTransferAccount` type
+    current_extension_types.push(ExtensionType::ConfidentialTransferAccount);
+    let needed_account_len =
+        ExtensionType::try_calculate_account_len::<Account>(&current_extension_types)?;
+
+    // if account is already large enough, return early
+    if token_account_info.data_len() >= needed_account_len {
+        return Ok(());
+    }
+
+    // reallocate
+    msg!(
+        "account needs realloc, +{:?} bytes",
+        needed_account_len - token_account_info.data_len()
+    );
+    token_account_info.realloc(needed_account_len, false)?;
+
+    // if additional lamports needed to remain rent-exempt, transfer them
+    let rent = Rent::get()?;
+    let new_rent_exempt_reserve = rent.minimum_balance(needed_account_len);
+
+    let current_lamport_reserve = token_account_info.lamports();
+    let lamports_diff = new_rent_exempt_reserve.saturating_sub(current_lamport_reserve);
+    if lamports_diff > 0 {
+        invoke(
+            &system_instruction::transfer(payer_info.key, token_account_info.key, lamports_diff),
+            &[
+                payer_info.clone(),
+                token_account_info.clone(),
+                system_program_info.clone(),
+            ],
+        )?;
+    }
+
+    // set account_type, if needed
+    let mut token_account_data = token_account_info.data.borrow_mut();
+    set_account_type::<Account>(&mut token_account_data)?;
+
+    Ok(())
+}
+
 /// 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,
+    elgamal_pubkey_source: ElGamalPubkeySource,
 ) -> 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 authority_info = next_account_info(account_info_iter)?;
-    let authority_info_data_len = authority_info.data_len();
+    let elgamal_pubkey = match elgamal_pubkey_source {
+        ElGamalPubkeySource::ProofInstructionOffset(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
+        }
+        ElGamalPubkeySource::ElGamalRegistry(elgamal_registry_account) => {
+            let _elgamal_registry_account = next_account_info(account_info_iter)?;
+            elgamal_registry_account.elgamal_pubkey
+        }
+    };
 
     check_program_account(token_account_info.owner)?;
     let token_account_data = &mut token_account_info.data.borrow_mut();
@@ -121,13 +229,28 @@ 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(),
-    )?;
+    match elgamal_pubkey_source {
+        ElGamalPubkeySource::ProofInstructionOffset(_) => {
+            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(),
+            )?;
+        }
+        ElGamalPubkeySource::ElGamalRegistry(elgamal_registry_account) => {
+            // if ElGamal registry was provided, then just verify that the owners of the
+            // registry and token accounts match, then skip the signature
+            // verification check
+            if elgamal_registry_account.owner != token_account.base.owner {
+                return Err(TokenError::OwnerMismatch.into());
+            }
+        }
+    };
 
     check_program_account(mint_info.owner)?;
     let mint_data = &mut mint_info.data.borrow();
@@ -140,7 +263,7 @@ fn process_configure_account(
     let confidential_transfer_account =
         token_account.init_extension::<ConfidentialTransferAccount>(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;
 
@@ -1108,7 +1231,7 @@ pub(crate) fn process_instruction(
                 accounts,
                 &data.decryptable_zero_balance,
                 &data.maximum_pending_balance_credit_counter,
-                data.proof_instruction_offset as i64,
+                ElGamalPubkeySource::ProofInstructionOffset(data.proof_instruction_offset as i64),
             )
         }
         ConfidentialTransferInstruction::ApproveAccount => {
@@ -1217,5 +1340,9 @@ pub(crate) fn process_instruction(
             #[cfg(not(feature = "zk-ops"))]
             Err(ProgramError::InvalidInstructionData)
         }
+        ConfidentialTransferInstruction::ConfigureAccountWithRegistry => {
+            msg!("ConfidentialTransferInstruction::ConfigureAccountWithRegistry");
+            process_configure_account_with_registry(program_id, accounts)
+        }
     }
 }
diff --git a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs
index 912d3c66d..88df40744 100644
--- a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs
+++ b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs
@@ -2,15 +2,14 @@ use {
     crate::{
         error::TokenError,
         extension::{confidential_transfer::instruction::*, transfer_fee::TransferFee},
-        proof::verify_and_extract_context,
     },
     solana_program::{
         account_info::{next_account_info, AccountInfo},
         program_error::ProgramError,
     },
     spl_token_confidential_transfer_proof_extraction::{
-        transfer::TransferProofContext, transfer_with_fee::TransferWithFeeProofContext,
-        withdraw::WithdrawProofContext,
+        instruction::verify_and_extract_context, transfer::TransferProofContext,
+        transfer_with_fee::TransferWithFeeProofContext, withdraw::WithdrawProofContext,
     },
     std::slice::Iter,
 };
diff --git a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs
index aeb243303..6c625df3c 100644
--- a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs
+++ b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs
@@ -11,7 +11,6 @@ use {
             instruction::CiphertextCiphertextEqualityProofData, DecryptableBalance,
         },
         instruction::{encode_instruction, TokenInstruction},
-        proof::{ProofData, ProofLocation},
         solana_zk_sdk::{
             encryption::pod::elgamal::PodElGamalPubkey,
             zk_elgamal_proof_program::instruction::ProofInstruction,
@@ -26,6 +25,7 @@ use {
         sysvar,
     },
     spl_pod::optional_keys::OptionalNonZeroPubkey,
+    spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation},
     std::convert::TryFrom,
 };
 
diff --git a/token/program-2022/src/extension/confidential_transfer_fee/processor.rs b/token/program-2022/src/extension/confidential_transfer_fee/processor.rs
index 82549130f..f71880cbb 100644
--- a/token/program-2022/src/extension/confidential_transfer_fee/processor.rs
+++ b/token/program-2022/src/extension/confidential_transfer_fee/processor.rs
@@ -27,7 +27,6 @@ use {
         instruction::{decode_instruction_data, decode_instruction_type},
         pod::{PodAccount, PodMint},
         processor::Processor,
-        proof::verify_and_extract_context,
         solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey,
     },
     bytemuck::Zeroable,
@@ -39,6 +38,7 @@ use {
         pubkey::Pubkey,
     },
     spl_pod::optional_keys::OptionalNonZeroPubkey,
+    spl_token_confidential_transfer_proof_extraction::instruction::verify_and_extract_context,
 };
 
 /// Processes an [InitializeConfidentialTransferFeeConfig] instruction.
diff --git a/token/program-2022/src/lib.rs b/token/program-2022/src/lib.rs
index 573fd0f18..d413d5903 100644
--- a/token/program-2022/src/lib.rs
+++ b/token/program-2022/src/lib.rs
@@ -14,7 +14,6 @@ pub mod onchain;
 pub mod pod;
 pub mod pod_instruction;
 pub mod processor;
-pub mod proof;
 #[cfg(feature = "serde-traits")]
 pub mod serialization;
 pub mod state;
@@ -130,3 +129,13 @@ pub fn check_system_program_account(system_program_id: &Pubkey) -> ProgramResult
     }
     Ok(())
 }
+
+/// Checks if the supplied program ID is that of the ElGamal registry program
+pub(crate) fn check_elgamal_registry_program_account(
+    elgamal_registry_account_program_id: &Pubkey,
+) -> ProgramResult {
+    if elgamal_registry_account_program_id != &spl_elgamal_registry::id() {
+        return Err(ProgramError::IncorrectProgramId);
+    }
+    Ok(())
+}


Test patch:
diff --git a/token/program-2022-test/Cargo.toml b/token/program-2022-test/Cargo.toml
index 79f9c7c11..7f6fd1bee 100644
--- a/token/program-2022-test/Cargo.toml
+++ b/token/program-2022-test/Cargo.toml
@@ -24,6 +24,7 @@ solana-program = "2.0.3"
 solana-program-test = "2.0.3"
 solana-sdk = "2.0.3"
 spl-associated-token-account = { version = "5.0.1", path = "../../associated-token-account/program" }
+spl-elgamal-registry = { version = "0.1.0", path = "../confidential-transfer/elgamal-registry" }
 spl-memo = { version = "5.0.0", path = "../../memo/program", features = [
   "no-entrypoint",
 ] }
@@ -34,6 +35,7 @@ spl-record = { version = "0.2.1", path = "../../record/program", features = [
 spl-token-2022 = { version = "5.0.2", path = "../program-2022", features = [
   "no-entrypoint",
 ] }
+spl-token-confidential-transfer-proof-extraction = { version = "0.1.0", path = "../confidential-transfer/proof-extraction" }
 spl-token-confidential-transfer-proof-generation = { version = "0.1.0", path = "../confidential-transfer/proof-generation" }
 spl-instruction-padding = { version = "0.2.0", path = "../../instruction-padding/program", features = [
   "no-entrypoint",
diff --git a/token/program-2022-test/tests/confidential_transfer.rs b/token/program-2022-test/tests/confidential_transfer.rs
index 64ae39c72..3e756082c 100644
--- a/token/program-2022-test/tests/confidential_transfer.rs
+++ b/token/program-2022-test/tests/confidential_transfer.rs
@@ -13,14 +13,17 @@ use {
         pubkey::Pubkey,
         signature::Signer,
         signer::{keypair::Keypair, signers::Signers},
-        transaction::TransactionError,
+        system_instruction,
+        transaction::{Transaction, TransactionError},
         transport::TransportError,
     },
+    spl_elgamal_registry::state::ELGAMAL_REGISTRY_ACCOUNT_LEN,
     spl_record::state::RecordData,
     spl_token_2022::{
         error::TokenError,
         extension::{
             confidential_transfer::{
+                self,
                 account_info::{EmptyAccountAccountInfo, TransferAccountInfo, WithdrawAccountInfo},
                 ConfidentialTransferAccount, MAXIMUM_DEPOSIT_TRANSFER_AMOUNT,
             },
@@ -38,6 +41,7 @@ use {
             TokenResult,
         },
     },
+    spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation},
     spl_token_confidential_transfer_proof_generation::{
         transfer::TransferProofData, transfer_with_fee::TransferWithFeeProofData,
         withdraw::WithdrawProofData,
@@ -2810,3 +2814,126 @@ async fn confidential_transfer_transfer_with_fee_and_memo_option(
         )
         .await;
 }
+
+#[tokio::test]
+async fn confidential_transfer_configure_token_account_with_registry() {
+    let authority = Keypair::new();
+    let auto_approve_new_accounts = false;
+    let auditor_elgamal_keypair = ElGamalKeypair::new_rand();
+    let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into();
+
+    let mut context = TestContext::new().await;
+    context
+        .init_token_with_mint(vec![
+            ExtensionInitializationParams::ConfidentialTransferMint {
+                authority: Some(authority.pubkey()),
+                auto_approve_new_accounts,
+                auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey),
+            },
+        ])
+        .await
+        .unwrap();
+
+    let TokenContext { token, alice, .. } = context.token_context.unwrap();
+    let alice_account_keypair = Keypair::new();
+    token
+        .create_auxiliary_token_account_with_extension_space(
+            &alice_account_keypair,
+            &alice.pubkey(),
+            vec![ExtensionType::ConfidentialTransferAccount],
+        )
+        .await
+        .unwrap();
+    let elgamal_keypair = ElGamalKeypair::new_rand();
+
+    // create ElGamal registry
+    let mut ctx = context.context.lock().await;
+    let proof_data =
+        confidential_transfer::instruction::PubkeyValidityProofData::new(&elgamal_keypair).unwrap();
+    let proof_location = ProofLocation::InstructionOffset(
+        1.try_into().unwrap(),
+        ProofData::InstructionData(&proof_data),
+    );
+
+    let elgamal_registry_address = spl_elgamal_registry::get_elgamal_registry_address(
+        &alice.pubkey(),
+        &spl_elgamal_registry::id(),
+    );
+
+    let rent = ctx.banks_client.get_rent().await.unwrap();
+    let space = ELGAMAL_REGISTRY_ACCOUNT_LEN;
+    let system_instruction = system_instruction::transfer(
+        &ctx.payer.pubkey(),
+        &elgamal_registry_address,
+        rent.minimum_balance(space),
+    );
+    let create_registry_instructions =
+        spl_elgamal_registry::instruction::create_registry(&alice.pubkey(), proof_location)
+            .unwrap();
+
+    let instructions = [&[system_instruction], &create_registry_instructions[..]].concat();
+    let tx = Transaction::new_signed_with_payer(
+        &instructions,
+        Some(&ctx.payer.pubkey()),
+        &[&ctx.payer, &alice],
+        ctx.last_blockhash,
+    );
+    ctx.banks_client.process_transaction(tx).await.unwrap();
+
+    // update ElGamal registry
+    let new_elgamal_keypair =
+        ElGamalKeypair::new_from_signer(&alice, &alice_account_keypair.pubkey().to_bytes())
+            .unwrap();
+    let proof_data =
+        confidential_transfer::instruction::PubkeyValidityProofData::new(&new_elgamal_keypair)
+            .unwrap();
+    let proof_location = ProofLocation::InstructionOffset(
+        1.try_into().unwrap(),
+        ProofData::InstructionData(&proof_data),
+    );
+
+    let payer_pubkey = ctx.payer.pubkey();
+    let instructions =
+        spl_elgamal_registry::instruction::update_registry(&alice.pubkey(), proof_location)
+            .unwrap();
+    let tx = Transaction::new_signed_with_payer(
+        &instructions,
+        Some(&ctx.payer.pubkey()),
+        &[&ctx.payer, &alice],
+        ctx.last_blockhash,
+    );
+    ctx.banks_client.process_transaction(tx).await.unwrap();
+    drop(ctx);
+
+    // configure account using ElGamal registry
+    let alice_account_keypair = Keypair::new();
+    let alice_token_account = alice_account_keypair.pubkey();
+    token
+        .create_auxiliary_token_account_with_extension_space(
+            &alice_account_keypair,
+            &alice.pubkey(),
+            vec![], // do not allocate space for confidential transfers
+        )
+        .await
+        .unwrap();
+
+    token
+        .confidential_transfer_configure_token_account_with_registry(
+            &alice_account_keypair.pubkey(),
+            &elgamal_registry_address,
+            Some(&payer_pubkey), // test account allocation
+        )
+        .await
+        .unwrap();
+
+    let state = token.get_account_info(&alice_token_account).await.unwrap();
+    let extension = state
+        .get_extension::<ConfidentialTransferAccount>()
+        .unwrap();
+    assert!(!bool::from(&extension.approved));
+    assert!(bool::from(&extension.allow_confidential_credits));
+    assert_eq!(
+        extension.elgamal_pubkey,
+        (*new_elgamal_keypair.pubkey()).into()
+    );
+}
diff --git a/token/program-2022-test/tests/program_test.rs b/token/program-2022-test/tests/program_test.rs
index 84846c5ab..1fbd749da 100644
--- a/token/program-2022-test/tests/program_test.rs
+++ b/token/program-2022-test/tests/program_test.rs
@@ -50,6 +50,11 @@ impl TestContext {
             spl_record::id(),
             processor!(spl_record::processor::process_instruction),
         );
+        program_test.add_program(
+            "spl_elgamal_registry",
+            spl_elgamal_registry::id(),
+            processor!(spl_elgamal_registry::processor::process_instruction),
+        );
         let context = program_test.start_with_context().await;
         let context = Arc::new(Mutex::new(context));