From 9ef1a747a88a888f2b82621b24343c2467b8a602 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 15 Oct 2024 18:33:28 +0900 Subject: [PATCH] create ElGamal registry program --- Cargo.toml | 1 + .../elgamal-registry/Cargo.toml | 25 +++ .../elgamal-registry/src/entrypoint.rs | 14 ++ .../elgamal-registry/src/instruction.rs | 197 ++++++++++++++++++ .../elgamal-registry/src/lib.rs | 7 + .../elgamal-registry/src/processor.rs | 64 ++++++ .../elgamal-registry/src/state.rs | 16 ++ 7 files changed, 324 insertions(+) create mode 100644 token/confidential-transfer/elgamal-registry/Cargo.toml create mode 100644 token/confidential-transfer/elgamal-registry/src/entrypoint.rs create mode 100644 token/confidential-transfer/elgamal-registry/src/instruction.rs create mode 100644 token/confidential-transfer/elgamal-registry/src/lib.rs create mode 100644 token/confidential-transfer/elgamal-registry/src/processor.rs create mode 100644 token/confidential-transfer/elgamal-registry/src/state.rs diff --git a/Cargo.toml b/Cargo.toml index 76890c34176..1e8fb54a826 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/confidential-transfer/elgamal-registry/Cargo.toml b/token/confidential-transfer/elgamal-registry/Cargo.toml new file mode 100644 index 00000000000..9501994e2de --- /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" +authors = ["Solana Labs Maintainers "] +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 00000000000..62a02f2a465 --- /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 00000000000..fd4b5a00cff --- /dev/null +++ b/token/confidential-transfer/elgamal-registry/src/instruction.rs @@ -0,0 +1,197 @@ +use { + crate::id, + solana_program::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::{Pubkey, PUBKEY_BYTES}, + sysvar, + }, + solana_zk_sdk::zk_elgamal_proof_program::{ + instruction::ProofInstruction, proof_data::PubkeyValidityProofData, + }, + spl_token_confidential_transfer_proof_extraction::{ProofData, ProofLocation}, +}; + +#[derive(Clone, Debug, PartialEq)] +#[repr(u8)] +pub enum RegistryInstruction { + /// Initialize an ElGamal public key registry. + /// + /// 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. + CreateRegistry { + /// The owner of the ElGamal registry account + 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. + 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 { + let (&tag, rest) = input + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + Ok(match tag { + 0 => { + let owner = rest + .get(..PUBKEY_BYTES) + .and_then(|x| Pubkey::try_from(x).ok()) + .ok_or(ProgramError::InvalidInstructionData)?; + let proof_instruction_offset = + *rest.first().ok_or(ProgramError::InvalidInstructionData)?; + Self::CreateRegistry { + owner, + proof_instruction_offset: proof_instruction_offset as i8, + } + } + 2 => { + 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 { + let mut buf = vec![]; + match self { + Self::CreateRegistry { + owner, + proof_instruction_offset, + } => { + buf.push(0); + buf.extend_from_slice(owner.as_ref()); + 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( + registry_account: &Pubkey, + owner: &Pubkey, + proof_location: ProofLocation, +) -> Result, ProgramError> { + let mut accounts = vec![AccountMeta::new(*registry_account, false)]; + let proof_instruction_offset = proof_instruction_offset(&mut accounts, proof_location); + + let registry_instruction = Instruction { + program_id: id(), + accounts, + data: RegistryInstruction::CreateRegistry { + owner: *owner, + proof_instruction_offset, + } + .pack(), + }; + append_zk_elgamal_proof(registry_instruction, proof_location) +} + +/// Create a `RegistryInstruction::UpdateRegistry` instruction +pub fn update_registry( + registry_account: &Pubkey, + owner: &Pubkey, + proof_location: ProofLocation, +) -> Result, ProgramError> { + let mut accounts = vec![ + AccountMeta::new(*registry_account, false), + AccountMeta::new_readonly(*owner, true), + ]; + let proof_instruction_offset = proof_instruction_offset(&mut accounts, proof_location); + + let registry_instruction = Instruction { + program_id: id(), + accounts, + data: RegistryInstruction::UpdateRegistry { + proof_instruction_offset, + } + .pack(), + }; + append_zk_elgamal_proof(registry_instruction, proof_location) +} + +/// Takes a `ProofLocation`, updates the list of accounts, and returns a +/// suitable proof location +fn proof_instruction_offset( + accounts: &mut Vec, + proof_location: ProofLocation, +) -> 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( + registry_instruction: Instruction, + proof_data_location: ProofLocation, +) -> Result, ProgramError> { + let mut instructions = vec![registry_instruction]; + + 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(instructions) +} 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 00000000000..434e9a06c4e --- /dev/null +++ b/token/confidential-transfer/elgamal-registry/src/lib.rs @@ -0,0 +1,7 @@ +mod entrypoint; +pub mod error; +pub mod instruction; +pub mod processor; +pub mod state; + +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 00000000000..cd8467d4bbc --- /dev/null +++ b/token/confidential-transfer/elgamal-registry/src/processor.rs @@ -0,0 +1,64 @@ +use { + crate::{instruction::RegistryInstruction, state::ElGamalRegistry}, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + }, + 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::verify_and_extract_context, +}; + +/// Instruction processor +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + let instruction = RegistryInstruction::unpack(input)?; + let account_info_iter = &mut accounts.iter(); + let registry_account_info = next_account_info(account_info_iter)?; + let registry_account_data = &mut registry_account_info.data.borrow_mut(); + let registry_account = pod_from_bytes_mut::(registry_account_data)?; + + let proof_instruction_offset = match instruction { + RegistryInstruction::CreateRegistry { + owner, + proof_instruction_offset, + } => { + // set the owner; ElGamal pubkey is set after the zkp verification below + registry_account.owner = owner; + proof_instruction_offset + } + RegistryInstruction::UpdateRegistry { + proof_instruction_offset, + } => { + // check the owner; ElGamal pubkey is set after the zkp verification below + let owner_info = next_account_info(account_info_iter)?; + validate_owner(owner_info, ®istry_account.owner)?; + 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, proof_instruction_offset as i64, None)?; + registry_account.elgamal_pubkey = proof_context.pubkey; + + Ok(()) +} + +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(()) +} 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 00000000000..ce7a0e61b81 --- /dev/null +++ b/token/confidential-transfer/elgamal-registry/src/state.rs @@ -0,0 +1,16 @@ +use { + bytemuck::{Pod, Zeroable}, + solana_program::pubkey::Pubkey, + solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, +}; + +/// 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, +}