diff --git a/Cargo.lock b/Cargo.lock index 82fb980766c..c18cf29ce3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7275,7 +7275,7 @@ dependencies = [ "serde", "serde_json", "solana-program", - "solana-zk-token-sdk", + "solana-zk-sdk", "spl-program-error 0.5.0", ] @@ -7570,11 +7570,14 @@ dependencies = [ "solana-program-test", "solana-sdk", "solana-security-txt", - "solana-zk-token-sdk", + "solana-zk-sdk", "spl-memo 5.0.0", "spl-pod 0.3.1", "spl-tlv-account-resolution 0.7.0", "spl-token 6.0.0", + "spl-token-confidential-transfer-ciphertext-arithmetic", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", "spl-token-group-interface 0.3.0", "spl-token-metadata-interface 0.4.0", "spl-transfer-hook-interface 0.7.0", @@ -7588,6 +7591,7 @@ version = "0.0.1" dependencies = [ "async-trait", "borsh 1.5.1", + "bytemuck", "futures-util", "solana-program", "solana-program-test", @@ -7600,6 +7604,7 @@ dependencies = [ "spl-tlv-account-resolution 0.7.0", "spl-token-2022 4.0.1", "spl-token-client", + "spl-token-confidential-transfer-proof-generation", "spl-token-group-interface 0.3.0", "spl-token-metadata-interface 0.4.0", "spl-transfer-hook-example", @@ -7637,6 +7642,7 @@ dependencies = [ "spl-token 6.0.0", "spl-token-2022 4.0.1", "spl-token-client", + "spl-token-confidential-transfer-proof-generation", "spl-token-group-interface 0.3.0", "spl-token-metadata-interface 0.4.0", "strum 0.26.3", @@ -7667,6 +7673,7 @@ dependencies = [ "spl-record", "spl-token 6.0.0", "spl-token-2022 4.0.1", + "spl-token-confidential-transfer-proof-generation", "spl-token-group-interface 0.3.0", "spl-token-metadata-interface 0.4.0", "spl-transfer-hook-interface 0.7.0", diff --git a/libraries/pod/Cargo.toml b/libraries/pod/Cargo.toml index d9a838907e1..0cd60ae3d2c 100644 --- a/libraries/pod/Cargo.toml +++ b/libraries/pod/Cargo.toml @@ -8,21 +8,21 @@ license = "Apache-2.0" edition = "2021" [features] -serde-traits = ["dep:serde", "dep:base64"] +serde-traits = ["dep:serde"] borsh = ["dep:borsh"] [dependencies] -base64 = { version = "0.22.1", optional = true } borsh = { version = "1.5.1", optional = true } bytemuck = { version = "1.16.3" } bytemuck_derive = { version = "1.7.0" } serde = { version = "1.0.207", optional = true } solana-program = "2.0.3" -solana-zk-token-sdk = "2.0.3" +solana-zk-sdk = "2.0.3" spl-program-error = { version = "0.5.0", path = "../program-error" } [dev-dependencies] serde_json = "1.0.124" +base64 = { version = "0.22.1" } [lib] crate-type = ["cdylib", "lib"] diff --git a/libraries/pod/src/optional_keys.rs b/libraries/pod/src/optional_keys.rs index ad71b1bcc4d..b31494d4ed7 100644 --- a/libraries/pod/src/optional_keys.rs +++ b/libraries/pod/src/optional_keys.rs @@ -1,18 +1,17 @@ //! Optional pubkeys that can be used a `Pod`s #[cfg(feature = "borsh")] use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use { + bytemuck_derive::{Pod, Zeroable}, + solana_program::{program_error::ProgramError, program_option::COption, pubkey::Pubkey}, + solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, +}; #[cfg(feature = "serde-traits")] use { - base64::{prelude::BASE64_STANDARD, Engine}, serde::de::{Error, Unexpected, Visitor}, serde::{Deserialize, Deserializer, Serialize, Serializer}, std::{convert::TryFrom, fmt, str::FromStr}, }; -use { - bytemuck_derive::{Pod, Zeroable}, - solana_program::{program_error::ProgramError, program_option::COption, pubkey::Pubkey}, - solana_zk_token_sdk::zk_token_elgamal::pod::ElGamalPubkey, -}; /// A Pubkey that encodes `None` as all `0`, meant to be usable as a Pod type, /// similar to all NonZero* number types from the bytemuck library. @@ -131,21 +130,21 @@ impl<'de> Deserialize<'de> for OptionalNonZeroPubkey { /// type. #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] #[repr(transparent)] -pub struct OptionalNonZeroElGamalPubkey(ElGamalPubkey); +pub struct OptionalNonZeroElGamalPubkey(PodElGamalPubkey); impl OptionalNonZeroElGamalPubkey { /// Checks equality between an OptionalNonZeroElGamalPubkey and an /// ElGamalPubkey when interpreted as bytes. - pub fn equals(&self, other: &ElGamalPubkey) -> bool { + pub fn equals(&self, other: &PodElGamalPubkey) -> bool { &self.0 == other } } -impl TryFrom> for OptionalNonZeroElGamalPubkey { +impl TryFrom> for OptionalNonZeroElGamalPubkey { type Error = ProgramError; - fn try_from(p: Option) -> Result { + fn try_from(p: Option) -> Result { match p { - None => Ok(Self(ElGamalPubkey::default())), + None => Ok(Self(PodElGamalPubkey::default())), Some(elgamal_pubkey) => { - if elgamal_pubkey == ElGamalPubkey::default() { + if elgamal_pubkey == PodElGamalPubkey::default() { Err(ProgramError::InvalidArgument) } else { Ok(Self(elgamal_pubkey)) @@ -154,9 +153,9 @@ impl TryFrom> for OptionalNonZeroElGamalPubkey { } } } -impl From for Option { +impl From for Option { fn from(p: OptionalNonZeroElGamalPubkey) -> Self { - if p.0 == ElGamalPubkey::default() { + if p.0 == PodElGamalPubkey::default() { None } else { Some(p.0) @@ -164,16 +163,13 @@ impl From for Option { } } -#[cfg(any(feature = "serde-traits", test))] -const OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN: usize = 32; - #[cfg(feature = "serde-traits")] impl Serialize for OptionalNonZeroElGamalPubkey { fn serialize(&self, s: S) -> Result where S: Serializer, { - if self.0 == ElGamalPubkey::default() { + if self.0 == PodElGamalPubkey::default() { s.serialize_none() } else { s.serialize_some(&self.0.to_string()) @@ -196,18 +192,7 @@ impl<'de> Visitor<'de> for OptionalNonZeroElGamalPubkeyVisitor { where E: Error, { - let bytes = BASE64_STANDARD.decode(v).map_err(Error::custom)?; - - if bytes.len() != OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN { - return Err(Error::custom(format!( - "Length of base64 decoded bytes is not {}", - OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN - ))); - } - - let mut array = [0; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN]; - array.copy_from_slice(&bytes[0..OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN]); - let elgamal_pubkey = ElGamalPubkey(array); + let elgamal_pubkey: PodElGamalPubkey = FromStr::from_str(v).map_err(Error::custom)?; OptionalNonZeroElGamalPubkey::try_from(Some(elgamal_pubkey)).map_err(Error::custom) } @@ -231,7 +216,12 @@ impl<'de> Deserialize<'de> for OptionalNonZeroElGamalPubkey { #[cfg(test)] mod tests { - use {super::*, crate::bytemuck::pod_from_bytes, solana_program::pubkey::PUBKEY_BYTES}; + use { + super::*, + crate::bytemuck::pod_from_bytes, + base64::{prelude::BASE64_STANDARD, Engine}, + solana_program::pubkey::PUBKEY_BYTES, + }; #[test] fn test_pod_non_zero_option() { @@ -290,23 +280,41 @@ mod tests { assert_eq!(optional_non_zero_pubkey_none, deserialized_none); } + const OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN: usize = 32; + + // Unfortunately, the `solana-zk-sdk` does not expose a constructor interface + // to construct `PodRistrettoPoint` from bytes. As a work-around, encode the + // bytes as base64 string and then convert the string to a + // `PodElGamalCiphertext`. + // + // The constructor will be added (and this function removed) with + // `solana-zk-sdk` 2.1. + fn elgamal_pubkey_from_bytes(bytes: &[u8]) -> PodElGamalPubkey { + let string = BASE64_STANDARD.encode(bytes); + std::str::FromStr::from_str(&string).unwrap() + } + #[test] fn test_pod_non_zero_elgamal_option() { assert_eq!( - Some(ElGamalPubkey([1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN])), - Option::::from(OptionalNonZeroElGamalPubkey(ElGamalPubkey( - [1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN] - ))) + Some(elgamal_pubkey_from_bytes( + &[1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN] + )), + Option::::from(OptionalNonZeroElGamalPubkey( + elgamal_pubkey_from_bytes(&[1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN]) + )) ); assert_eq!( None, - Option::::from(OptionalNonZeroElGamalPubkey(ElGamalPubkey( - [0; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN] - ))) + Option::::from(OptionalNonZeroElGamalPubkey( + elgamal_pubkey_from_bytes(&[0; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN]) + )) ); assert_eq!( - OptionalNonZeroElGamalPubkey(ElGamalPubkey([1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN])), + OptionalNonZeroElGamalPubkey(elgamal_pubkey_from_bytes( + &[1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN] + )), *pod_from_bytes::( &[1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN] ) @@ -318,8 +326,9 @@ mod tests { #[cfg(feature = "serde-traits")] #[test] fn test_pod_non_zero_elgamal_option_serde_some() { - let optional_non_zero_elgamal_pubkey_some = - OptionalNonZeroElGamalPubkey(ElGamalPubkey([1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN])); + let optional_non_zero_elgamal_pubkey_some = OptionalNonZeroElGamalPubkey( + elgamal_pubkey_from_bytes(&[1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN]), + ); let serialized_some = serde_json::to_string(&optional_non_zero_elgamal_pubkey_some).unwrap(); assert_eq!( @@ -335,8 +344,9 @@ mod tests { #[cfg(feature = "serde-traits")] #[test] fn test_pod_non_zero_elgamal_option_serde_none() { - let optional_non_zero_elgamal_pubkey_none = - OptionalNonZeroElGamalPubkey(ElGamalPubkey([0; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN])); + let optional_non_zero_elgamal_pubkey_none = OptionalNonZeroElGamalPubkey( + elgamal_pubkey_from_bytes(&[0; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN]), + ); let serialized_none = serde_json::to_string(&optional_non_zero_elgamal_pubkey_none).unwrap(); assert_eq!(&serialized_none, "null"); diff --git a/token/cli/Cargo.toml b/token/cli/Cargo.toml index 43d7990fe43..cfa59841b28 100644 --- a/token/cli/Cargo.toml +++ b/token/cli/Cargo.toml @@ -35,6 +35,7 @@ spl-token-2022 = { version = "4.0.0", path = "../program-2022", features = [ "no-entrypoint", ] } spl-token-client = { version = "0.11.0", path = "../client" } +spl-token-confidential-transfer-proof-generation = { version = "0.1.0", path = "../confidential-transfer/proof-generation" } spl-token-metadata-interface = { version = "0.4.0", path = "../../token-metadata/interface" } spl-token-group-interface = { version = "0.3.0", path = "../../token-group/interface" } spl-associated-token-account = { version = "4.0.0", path = "../../associated-token-account/program", features = [ diff --git a/token/cli/src/command.rs b/token/cli/src/command.rs index b4e971c4a9d..b35084493e6 100644 --- a/token/cli/src/command.rs +++ b/token/cli/src/command.rs @@ -57,19 +57,19 @@ use { transfer_hook::TransferHook, BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned, }, - solana_zk_token_sdk::{ - encryption::{ - auth_encryption::AeKey, - elgamal::{self, ElGamalKeypair}, - }, - zk_token_elgamal::pod::ElGamalPubkey, + solana_zk_sdk::encryption::{ + auth_encryption::AeKey, + elgamal::{self, ElGamalKeypair}, + pod::elgamal::PodElGamalPubkey, }, state::{Account, AccountState, Mint}, }, spl_token_client::{ client::{ProgramRpcClientSendTransaction, RpcClientResponse}, - proof_generation::ProofAccount, - token::{ComputeUnitLimit, ExtensionInitializationParams, Token}, + token::{ComputeUnitLimit, ExtensionInitializationParams, ProofAccount, Token}, + }, + spl_token_confidential_transfer_proof_generation::{ + transfer::TransferProofData, withdraw::WithdrawProofData, }, spl_token_group_interface::state::TokenGroup, spl_token_metadata_interface::state::{Field, TokenMetadata}, @@ -1479,7 +1479,7 @@ async fn command_transfer( let auditor_elgamal_pubkey = if let Ok(confidential_transfer_mint) = mint_state.get_extension::() { - let expected_auditor_elgamal_pubkey = Option::::from( + let expected_auditor_elgamal_pubkey = Option::::from( confidential_transfer_mint.auditor_elgamal_pubkey, ); @@ -1589,35 +1589,41 @@ async fn command_transfer( .unwrap(); let transfer_account_info = TransferAccountInfo::new(extension); - let (equality_proof_data, ciphertext_validity_proof_data, range_proof_data) = - transfer_account_info - .generate_split_transfer_proof_data( - transfer_balance, - &args.sender_elgamal_keypair, - &args.sender_aes_key, - &recipient_elgamal_pubkey, - auditor_elgamal_pubkey.as_ref(), - ) - .unwrap(); + let TransferProofData { + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + } = transfer_account_info + .generate_split_transfer_proof_data( + transfer_balance, + &args.sender_elgamal_keypair, + &args.sender_aes_key, + &recipient_elgamal_pubkey, + auditor_elgamal_pubkey.as_ref(), + ) + .unwrap(); // setup proofs let _ = try_join!( - token.create_range_proof_context_state_for_transfer( + token.confidential_transfer_create_context_state_account( &range_proof_pubkey, &context_state_authority_pubkey, &range_proof_data, + true, &range_proof_context_state_account, ), - token.create_equality_proof_context_state_for_transfer( + token.confidential_transfer_create_context_state_account( &equality_proof_pubkey, &context_state_authority_pubkey, &equality_proof_data, + false, &equality_proof_context_state_account, ), - token.create_ciphertext_validity_proof_context_state_for_transfer( + token.confidential_transfer_create_context_state_account( &ciphertext_validity_proof_pubkey, &context_state_authority_pubkey, &ciphertext_validity_proof_data, + false, &ciphertext_validity_proof_context_state_account, ) )?; @@ -2944,7 +2950,7 @@ async fn command_update_confidential_transfer_settings( let new_auditor_pubkey = if let Some(auditor_pubkey) = auditor_pubkey { auditor_pubkey.into() } else { - Option::::from(confidential_transfer_mint.auditor_elgamal_pubkey) + Option::::from(confidential_transfer_mint.auditor_elgamal_pubkey) }; (new_auto_approve, new_auditor_pubkey) @@ -3335,28 +3341,47 @@ async fn command_deposit_withdraw_confidential_tokens( let withdraw_account_info = WithdrawAccountInfo::new(extension_state); let context_state_authority = config.fee_payer()?; - let context_state_keypair = Keypair::new(); - let context_state_pubkey = context_state_keypair.pubkey(); + let equality_proof_context_state_keypair = Keypair::new(); + let equality_proof_context_state_pubkey = equality_proof_context_state_keypair.pubkey(); + let range_proof_context_state_keypair = Keypair::new(); + let range_proof_context_state_pubkey = range_proof_context_state_keypair.pubkey(); - let withdraw_proof_data = - withdraw_account_info.generate_proof_data(amount, elgamal_keypair, aes_key)?; + let WithdrawProofData { + equality_proof_data, + range_proof_data, + } = withdraw_account_info.generate_proof_data(amount, elgamal_keypair, aes_key)?; - // setup proof - token - .create_withdraw_proof_context_state( - &context_state_pubkey, - &context_state_authority.pubkey(), - &withdraw_proof_data, - &context_state_keypair, + // set up context state accounts + let context_state_authority_pubkey = context_state_authority.pubkey(); + + let _ = try_join!( + token.confidential_transfer_create_context_state_account( + &equality_proof_context_state_pubkey, + &context_state_authority_pubkey, + &equality_proof_data, + false, + &equality_proof_context_state_keypair, + ), + token.confidential_transfer_create_context_state_account( + &range_proof_context_state_pubkey, + &context_state_authority_pubkey, + &range_proof_data, + true, + &range_proof_context_state_keypair, ) - .await?; + )?; // do the withdrawal - token + let withdraw_result = token .confidential_transfer_withdraw( &token_account_address, &owner, - Some(&ProofAccount::ContextAccount(context_state_pubkey)), + Some(&ProofAccount::ContextAccount( + equality_proof_context_state_pubkey, + )), + Some(&ProofAccount::ContextAccount( + range_proof_context_state_pubkey, + )), amount, decimals, Some(withdraw_account_info), @@ -3367,15 +3392,22 @@ async fn command_deposit_withdraw_confidential_tokens( .await?; // close context state account - let context_state_authority_pubkey = context_state_authority.pubkey(); - token - .confidential_transfer_close_context_state( - &context_state_pubkey, + let _ = try_join!( + token.confidential_transfer_close_context_state( + &equality_proof_context_state_pubkey, + &token_account_address, + &context_state_authority_pubkey, + &context_state_authority, + ), + token.confidential_transfer_close_context_state( + &range_proof_context_state_pubkey, &token_account_address, &context_state_authority_pubkey, &context_state_authority, ) - .await? + )?; + + withdraw_result } }; @@ -3447,8 +3479,8 @@ async fn command_apply_pending_balance( struct ConfidentialTransferArgs { sender_elgamal_keypair: ElGamalKeypair, sender_aes_key: AeKey, - recipient_elgamal_pubkey: Option, - auditor_elgamal_pubkey: Option, + recipient_elgamal_pubkey: Option, + auditor_elgamal_pubkey: Option, } pub async fn process_command<'a>( diff --git a/token/cli/src/encryption_keypair.rs b/token/cli/src/encryption_keypair.rs index f2b2d733237..3a5aecbcf76 100644 --- a/token/cli/src/encryption_keypair.rs +++ b/token/cli/src/encryption_keypair.rs @@ -5,9 +5,9 @@ use { base64::{prelude::BASE64_STANDARD, Engine}, clap::ArgMatches, - spl_token_2022::solana_zk_token_sdk::{ - encryption::elgamal::{ElGamalKeypair, ElGamalPubkey}, - zk_token_elgamal::pod::ElGamalPubkey as PodElGamalPubkey, + spl_token_2022::solana_zk_sdk::encryption::{ + elgamal::{ElGamalKeypair, ElGamalPubkey}, + pod::elgamal::PodElGamalPubkey, }, }; diff --git a/token/cli/tests/command.rs b/token/cli/tests/command.rs index 2ebf12385d6..15a6e87f01e 100644 --- a/token/cli/tests/command.rs +++ b/token/cli/tests/command.rs @@ -32,7 +32,7 @@ use { BaseStateWithExtensions, StateWithExtensionsOwned, }, instruction::create_native_mint, - solana_zk_token_sdk::zk_token_elgamal::pod::ElGamalPubkey, + solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, state::{Account, AccountState, Mint, Multisig}, }, spl_token_cli::{ @@ -2609,7 +2609,7 @@ async fn transfer_fee_basis_point(test_validator: &TestValidator, payer: &Keypai } async fn confidential_transfer(test_validator: &TestValidator, payer: &Keypair) { - use spl_token_2022::solana_zk_token_sdk::encryption::elgamal::ElGamalKeypair; + use spl_token_2022::solana_zk_sdk::encryption::elgamal::ElGamalKeypair; let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); @@ -2650,13 +2650,13 @@ async fn confidential_transfer(test_validator: &TestValidator, payer: &Keypair) auto_approve, ); assert_eq!( - Option::::from(extension.auditor_elgamal_pubkey), + Option::::from(extension.auditor_elgamal_pubkey), None, ); // update confidential transfer mint settings let auditor_keypair = ElGamalKeypair::new_rand(); - let auditor_pubkey: ElGamalPubkey = (*auditor_keypair.pubkey()).into(); + let auditor_pubkey: PodElGamalPubkey = (*auditor_keypair.pubkey()).into(); let new_auto_approve = true; process_test_command( @@ -2686,7 +2686,7 @@ async fn confidential_transfer(test_validator: &TestValidator, payer: &Keypair) new_auto_approve, ); assert_eq!( - Option::::from(extension.auditor_elgamal_pubkey), + Option::::from(extension.auditor_elgamal_pubkey), Some(auditor_pubkey), ); @@ -2946,7 +2946,7 @@ async fn confidential_transfer_with_fee(test_validator: &TestValidator, payer: & auto_approve, ); assert_eq!( - Option::::from(extension.auditor_elgamal_pubkey), + Option::::from(extension.auditor_elgamal_pubkey), None, ); diff --git a/token/client/Cargo.toml b/token/client/Cargo.toml index bca57598627..bc083bda1ad 100644 --- a/token/client/Cargo.toml +++ b/token/client/Cargo.toml @@ -32,6 +32,7 @@ spl-record = { version = "0.2.0", path = "../../record/program", features = ["no spl-token = { version = "6.0", path = "../program", features = [ "no-entrypoint", ] } +spl-token-confidential-transfer-proof-generation = { version = "0.1.0", path = "../confidential-transfer/proof-generation" } spl-token-2022 = { version = "4.0.0", path = "../program-2022" } spl-token-group-interface = { version = "0.3.0", path = "../../token-group/interface" } spl-token-metadata-interface = { version = "0.4.0", path = "../../token-metadata/interface" } diff --git a/token/client/src/lib.rs b/token/client/src/lib.rs index b0d017870bc..e2ea2307cb4 100644 --- a/token/client/src/lib.rs +++ b/token/client/src/lib.rs @@ -3,11 +3,4 @@ pub mod client; pub mod output; pub mod token; -/// Helper functions to generate split zero-knowledge proofs for confidential -/// transfers. -/// -/// The logic in this submodule should belong to the `solana-zk-token-sdk` and -/// will be removed with an upgrade to the Solana program in the future. -pub mod proof_generation; - pub use spl_token_2022; diff --git a/token/client/src/proof_generation.rs b/token/client/src/proof_generation.rs deleted file mode 100644 index 63581eb396f..00000000000 --- a/token/client/src/proof_generation.rs +++ /dev/null @@ -1,388 +0,0 @@ -//! Helper functions to generate split zero-knowledge proofs for confidential -//! transfers in the Confidential Transfer Extension. -//! -//! The logic in this submodule should belong to the `solana-zk-token-sdk` and -//! will be removed with an upgrade to the Solana program. - -use { - curve25519_dalek::scalar::Scalar, - solana_sdk::pubkey::Pubkey, - spl_token_2022::{ - error::TokenError, - extension::confidential_transfer::{ - ciphertext_extraction::transfer_amount_source_ciphertext, - processor::verify_and_split_deposit_amount, - }, - solana_zk_token_sdk::{ - encryption::{ - auth_encryption::{AeCiphertext, AeKey}, - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, - grouped_elgamal::GroupedElGamal, - pedersen::{Pedersen, PedersenCommitment, PedersenOpening}, - }, - instruction::{ - transfer::{ - try_combine_lo_hi_commitments, try_combine_lo_hi_openings, FeeEncryption, - FeeParameters, TransferAmountCiphertext, - }, - BatchedGroupedCiphertext2HandlesValidityProofData, - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU256Data, - CiphertextCommitmentEqualityProofData, FeeSigmaProofData, - }, - zk_token_elgamal::ops::subtract_with_lo_hi, - }, - }, -}; - -pub enum ProofAccount { - ContextAccount(Pubkey), - RecordAccount(Pubkey, u32), -} - -/// The main logic to create the five split proof data for a transfer with fee. -#[allow(clippy::too_many_arguments)] -pub fn transfer_with_fee_split_proof_data( - current_available_balance: &ElGamalCiphertext, - current_decryptable_available_balance: &AeCiphertext, - transfer_amount: u64, - source_elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - withdraw_withheld_authority_elgamal_pubkey: &ElGamalPubkey, - transfer_fee_parameters: &FeeParameters, -) -> Result< - ( - CiphertextCommitmentEqualityProofData, - BatchedGroupedCiphertext3HandlesValidityProofData, - FeeSigmaProofData, - BatchedGroupedCiphertext2HandlesValidityProofData, - BatchedRangeProofU256Data, - ), - TokenError, -> { - let default_auditor_pubkey = ElGamalPubkey::default(); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey); - - // Split the transfer amount into the low and high bit components. - let (transfer_amount_lo, transfer_amount_hi) = - verify_and_split_deposit_amount(transfer_amount)?; - - // Encrypt the `lo` and `hi` transfer amounts. - let (transfer_amount_grouped_ciphertext_lo, transfer_amount_opening_lo) = - TransferAmountCiphertext::new( - transfer_amount_lo, - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ); - - let (transfer_amount_grouped_ciphertext_hi, transfer_amount_opening_hi) = - TransferAmountCiphertext::new( - transfer_amount_hi, - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ); - - // Decrypt the current available balance at the source - let current_decrypted_available_balance = current_decryptable_available_balance - .decrypt(aes_key) - .ok_or(TokenError::AccountDecryption)?; - - // Compute the remaining balance at the source - let new_decrypted_available_balance = current_decrypted_available_balance - .checked_sub(transfer_amount) - .ok_or(TokenError::InsufficientFunds)?; - - // Create a new Pedersen commitment for the remaining balance at the source - let (new_available_balance_commitment, new_source_opening) = - Pedersen::new(new_decrypted_available_balance); - - // Compute the remaining balance at the source as ElGamal ciphertexts - let transfer_amount_source_ciphertext_lo = - transfer_amount_source_ciphertext(&transfer_amount_grouped_ciphertext_lo.into()); - let transfer_amount_source_ciphertext_hi = - transfer_amount_source_ciphertext(&transfer_amount_grouped_ciphertext_hi.into()); - - let current_available_balance = (*current_available_balance).into(); - let new_available_balance_ciphertext = subtract_with_lo_hi( - ¤t_available_balance, - &transfer_amount_source_ciphertext_lo, - &transfer_amount_source_ciphertext_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - let new_available_balance_ciphertext: ElGamalCiphertext = new_available_balance_ciphertext - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - - // generate equality proof data - let equality_proof_data = CiphertextCommitmentEqualityProofData::new( - source_elgamal_keypair, - &new_available_balance_ciphertext, - &new_available_balance_commitment, - &new_source_opening, - new_decrypted_available_balance, - ) - .map_err(|_| TokenError::ProofGeneration)?; - - // encrypt the transfer amount under the source, destination and auditor ElGamal - // public key - let transfer_amount_destination_auditor_ciphertext_lo = GroupedElGamal::encrypt_with( - [ - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ], - transfer_amount_lo, - &transfer_amount_opening_lo, - ); - let transfer_amount_destination_auditor_ciphertext_hi = GroupedElGamal::encrypt_with( - [ - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ], - transfer_amount_hi, - &transfer_amount_opening_hi, - ); - - // generate transfer amount ciphertext validity data - let transfer_amount_ciphertext_validity_proof_data = - BatchedGroupedCiphertext3HandlesValidityProofData::new( - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - &transfer_amount_destination_auditor_ciphertext_lo, - &transfer_amount_destination_auditor_ciphertext_hi, - transfer_amount_lo, - transfer_amount_hi, - &transfer_amount_opening_lo, - &transfer_amount_opening_hi, - ) - .map_err(|_| TokenError::ProofGeneration)?; - - // calculate fee - let transfer_fee_basis_points = transfer_fee_parameters.fee_rate_basis_points; - let transfer_fee_maximum_fee = transfer_fee_parameters.maximum_fee; - let (raw_fee_amount, delta_fee) = - calculate_raw_fee_and_delta(transfer_amount, transfer_fee_basis_points) - .ok_or(TokenError::Overflow)?; - - // if raw fee is greater than the maximum fee, then use the maximum fee for the - // fee amount - let fee_amount = std::cmp::min(transfer_fee_maximum_fee, raw_fee_amount); - - // split and encrypt fee - let (fee_amount_lo, fee_amount_hi) = verify_and_split_deposit_amount(fee_amount)?; - let (fee_ciphertext_lo, fee_opening_lo) = FeeEncryption::new( - fee_amount_lo, - destination_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - ); - let (fee_ciphertext_hi, fee_opening_hi) = FeeEncryption::new( - fee_amount_hi, - destination_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - ); - - // create combined commitments and openings to be used to generate proofs - const TRANSFER_AMOUNT_LO_BIT_LENGTH: usize = 16; - let combined_transfer_amount_commitment = try_combine_lo_hi_commitments( - transfer_amount_grouped_ciphertext_lo.get_commitment(), - transfer_amount_grouped_ciphertext_hi.get_commitment(), - TRANSFER_AMOUNT_LO_BIT_LENGTH, - ) - .map_err(|_| TokenError::ProofGeneration)?; - let combined_transfer_amount_opening = try_combine_lo_hi_openings( - &transfer_amount_opening_lo, - &transfer_amount_opening_hi, - TRANSFER_AMOUNT_LO_BIT_LENGTH, - ) - .map_err(|_| TokenError::ProofGeneration)?; - - const FEE_AMOUNT_LO_BIT_LENGTH: usize = 16; - let combined_fee_commitment = try_combine_lo_hi_commitments( - fee_ciphertext_lo.get_commitment(), - fee_ciphertext_hi.get_commitment(), - FEE_AMOUNT_LO_BIT_LENGTH, - ) - .map_err(|_| TokenError::ProofGeneration)?; - let combined_fee_opening = - try_combine_lo_hi_openings(&fee_opening_lo, &fee_opening_hi, FEE_AMOUNT_LO_BIT_LENGTH) - .map_err(|_| TokenError::ProofGeneration)?; - - // compute claimed and real delta commitment - let (claimed_commitment, claimed_opening) = Pedersen::new(delta_fee); - let (delta_commitment, delta_opening) = compute_delta_commitment_and_opening( - ( - &combined_transfer_amount_commitment, - &combined_transfer_amount_opening, - ), - (&combined_fee_commitment, &combined_fee_opening), - transfer_fee_basis_points, - ); - - // generate fee sigma proof - let fee_sigma_proof_data = FeeSigmaProofData::new( - &combined_fee_commitment, - &delta_commitment, - &claimed_commitment, - &combined_fee_opening, - &delta_opening, - &claimed_opening, - fee_amount, - delta_fee, - transfer_fee_maximum_fee, - ) - .map_err(|_| TokenError::ProofGeneration)?; - - // encrypt the fee amount under the destination and withdraw withheld authority - // ElGamal public key - let fee_destination_withdraw_withheld_authority_ciphertext_lo = GroupedElGamal::encrypt_with( - [ - destination_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - ], - fee_amount_lo, - &fee_opening_lo, - ); - let fee_destination_withdraw_withheld_authority_ciphertext_hi = GroupedElGamal::encrypt_with( - [ - destination_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - ], - fee_amount_hi, - &fee_opening_hi, - ); - - // generate fee ciphertext validity data - let fee_ciphertext_validity_proof_data = - BatchedGroupedCiphertext2HandlesValidityProofData::new( - destination_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - &fee_destination_withdraw_withheld_authority_ciphertext_lo, - &fee_destination_withdraw_withheld_authority_ciphertext_hi, - fee_amount_lo, - fee_amount_hi, - &fee_opening_lo, - &fee_opening_hi, - ) - .map_err(|_| TokenError::ProofGeneration)?; - - // generate range proof data - const REMAINING_BALANCE_BIT_LENGTH: usize = 64; - const TRANSFER_AMOUNT_HI_BIT_LENGTH: usize = 32; - const DELTA_BIT_LENGTH: usize = 48; - const FEE_AMOUNT_HI_BIT_LENGTH: usize = 32; - const MAX_FEE_BASIS_POINTS: u64 = 10_000; - - let delta_fee_complement = MAX_FEE_BASIS_POINTS - delta_fee; - - let max_fee_basis_points_commitment = - Pedersen::with(MAX_FEE_BASIS_POINTS, &PedersenOpening::default()); - let claimed_complement_commitment = max_fee_basis_points_commitment - claimed_commitment; - let claimed_complement_opening = PedersenOpening::default() - &claimed_opening; - - let range_proof_data = BatchedRangeProofU256Data::new( - vec![ - &new_available_balance_commitment, - transfer_amount_grouped_ciphertext_lo.get_commitment(), - transfer_amount_grouped_ciphertext_hi.get_commitment(), - &claimed_commitment, - &claimed_complement_commitment, - fee_ciphertext_lo.get_commitment(), - fee_ciphertext_hi.get_commitment(), - ], - vec![ - new_decrypted_available_balance, - transfer_amount_lo, - transfer_amount_hi, - delta_fee, - delta_fee_complement, - fee_amount_lo, - fee_amount_hi, - ], - vec![ - REMAINING_BALANCE_BIT_LENGTH, - TRANSFER_AMOUNT_LO_BIT_LENGTH, - TRANSFER_AMOUNT_HI_BIT_LENGTH, - DELTA_BIT_LENGTH, - DELTA_BIT_LENGTH, - FEE_AMOUNT_LO_BIT_LENGTH, - FEE_AMOUNT_HI_BIT_LENGTH, - ], - vec![ - &new_source_opening, - &transfer_amount_opening_lo, - &transfer_amount_opening_hi, - &claimed_opening, - &claimed_complement_opening, - &fee_opening_lo, - &fee_opening_hi, - ], - ) - .map_err(|_| TokenError::ProofGeneration)?; - - Ok(( - equality_proof_data, - transfer_amount_ciphertext_validity_proof_data, - fee_sigma_proof_data, - fee_ciphertext_validity_proof_data, - range_proof_data, - )) -} - -/// Calculate transfer fee and the "delta" value. The function returns the raw -/// fee, which could be greater than the maximum fee amount of a fee parameter. -/// -/// The "delta" value is a number that captures the round-off value when the fee -/// is computed. The fee is computed according to the formula `fee = -/// transfer_amount * fee_rate_basis_points / 10_000`. If no rounding occurred, -/// then we must have `fee * 10_000 - transfer_amount * fee_rate_basis_points = -/// 0`. If there is rounding involved (`10_000` does not divide cleanly), -/// then the difference `fee * 10_000 - transfer_amount * fee_rate_basis_points` -/// can be a non-zero number between `0` and `9_999` inclusively. We call this -/// number the "delta" value. -fn calculate_raw_fee_and_delta( - transfer_amount: u64, - fee_rate_basis_points: u16, -) -> Option<(u64, u64)> { - const ONE_IN_BASIS_POINTS: u128 = 10_000_u128; - - // compute `transfer_amount * fee_rate_basis_points` - let numerator = (transfer_amount as u128).checked_mul(fee_rate_basis_points as u128)?; - - // compute fee as `transfer_amount * fee_rate_basis_points / 10_000 ` - let fee = numerator - .checked_add(ONE_IN_BASIS_POINTS)? - .checked_sub(1)? - .checked_div(ONE_IN_BASIS_POINTS)?; - - // compute the delta fee as `fee * 10_000 - fee_rate_basis_points` - let delta_fee = fee - .checked_mul(ONE_IN_BASIS_POINTS)? - .checked_sub(numerator)?; - - Some((fee as u64, delta_fee as u64)) -} - -/// Calculate the "delta" commitment-opening pair from a transfer amount and fee -/// commitment-opening pairs. -fn compute_delta_commitment_and_opening( - (transfer_amount_commitment, transfer_amount_opening): (&PedersenCommitment, &PedersenOpening), - (fee_commitment, fee_opening): (&PedersenCommitment, &PedersenOpening), - fee_rate_basis_points: u16, -) -> (PedersenCommitment, PedersenOpening) { - const ONE_IN_BASIS_POINTS: u128 = 10_000_u128; - - let one_in_basis_points_scalar = Scalar::from(ONE_IN_BASIS_POINTS); - let fee_rate_scalar = Scalar::from(fee_rate_basis_points); - - let delta_commitment = - fee_commitment * one_in_basis_points_scalar - transfer_amount_commitment * fee_rate_scalar; - let delta_opening = - fee_opening * one_in_basis_points_scalar - transfer_amount_opening * fee_rate_scalar; - - (delta_commitment, delta_opening) -} diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 824875b6d31..a9d9f8a5f1c 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -1,12 +1,8 @@ use { - crate::{ - client::{ - ProgramClient, ProgramClientError, SendTransaction, SimulateTransaction, - SimulationResult, - }, - proof_generation::{transfer_with_fee_split_proof_data, ProofAccount}, + crate::client::{ + ProgramClient, ProgramClientError, SendTransaction, SimulateTransaction, SimulationResult, }, - bytemuck::bytes_of, + bytemuck::{bytes_of, Pod}, futures::future::join_all, futures_util::TryFutureExt, solana_program_test::tokio::time, @@ -51,20 +47,26 @@ use { BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsOwned, }, instruction, offchain, - proof::{ProofData, ProofLocation}, - solana_zk_token_sdk::{ + proof::{zk_proof_type_to_instruction, ProofData, ProofLocation}, + solana_zk_sdk::{ encryption::{ auth_encryption::AeKey, elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey}, + pod::elgamal::PodElGamalPubkey, + }, + zk_elgamal_proof_program::{ + self, + instruction::{close_context_state, ContextStateInfo}, + proof_data::*, + state::ProofContextState, }, - instruction::*, - zk_token_elgamal::pod::ElGamalPubkey as PodElGamalPubkey, - zk_token_proof_instruction::{self, ContextStateInfo, ProofInstruction}, - zk_token_proof_program, - zk_token_proof_state::ProofContextState, }, state::{Account, AccountState, Mint, Multisig}, }, + spl_token_confidential_transfer_proof_generation::{ + transfer::TransferProofData, transfer_with_fee::TransferWithFeeProofData, + withdraw::WithdrawProofData, + }, spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, spl_token_metadata_interface::state::{Field, TokenMetadata}, std::{ @@ -339,6 +341,11 @@ pub enum ComputeUnitLimit { Static(u32), } +pub enum ProofAccount { + ContextAccount(Pubkey), + RecordAccount(Pubkey, u32), +} + pub struct Token { client: Arc>, pubkey: Pubkey, /* token mint */ @@ -1910,7 +1917,7 @@ where None } else { Some( - confidential_transfer::instruction::PubkeyValidityData::new(elgamal_keypair) + confidential_transfer::instruction::PubkeyValidityProofData::new(elgamal_keypair) .map_err(|_| TokenError::ProofGeneration)?, ) }; @@ -1931,7 +1938,7 @@ where &self.program_id, account, &self.pubkey, - decryptable_balance, + decryptable_balance.into(), maximum_pending_balance_credit_counter, authority, &multisig_signers, @@ -2055,7 +2062,8 @@ where &self, account: &Pubkey, authority: &Pubkey, - proof_account: Option<&ProofAccount>, + equality_proof_account: Option<&ProofAccount>, + range_proof_account: Option<&ProofAccount>, withdraw_amount: u64, decimals: u8, account_info: Option, @@ -2075,25 +2083,36 @@ where WithdrawAccountInfo::new(confidential_transfer_account) }; - let proof_data = if proof_account.is_some() { - None - } else { - Some( - account_info - .generate_proof_data(withdraw_amount, elgamal_keypair, aes_key) - .map_err(|_| TokenError::ProofGeneration)?, - ) - }; + let WithdrawProofData { + equality_proof_data, + range_proof_data, + } = account_info + .generate_proof_data(withdraw_amount, elgamal_keypair, aes_key) + .map_err(|_| TokenError::ProofGeneration)?; + + // if proof accounts are none, then proof data must be included as instruction + // data + let equality_proof_data = equality_proof_account + .is_none() + .then_some(equality_proof_data); + let range_proof_data = range_proof_account.is_none().then_some(range_proof_data); // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, // which is guaranteed by the previous check - let proof_location = Self::confidential_transfer_create_proof_location( - proof_data.as_ref(), - proof_account, + let equality_proof_location = Self::confidential_transfer_create_proof_location( + equality_proof_data.as_ref(), + equality_proof_account, 1, ) .unwrap(); + let range_proof_location = Self::confidential_transfer_create_proof_location( + range_proof_data.as_ref(), + range_proof_account, + 2, + ) + .unwrap(); + let new_decryptable_available_balance = account_info .new_decryptable_available_balance(withdraw_amount, aes_key) .map_err(|_| TokenError::AccountDecryption)?; @@ -2105,73 +2124,17 @@ where &self.pubkey, withdraw_amount, decimals, - new_decryptable_available_balance, + new_decryptable_available_balance.into(), authority, &multisig_signers, - proof_location, + equality_proof_location, + range_proof_location, )?, signing_keypairs, ) .await } - /// Create withdraw proof context state account for a confidential transfer - /// withdraw instruction. - pub async fn create_withdraw_proof_context_state( - &self, - context_state_account: &Pubkey, - context_state_authority: &Pubkey, - withdraw_proof_data: &WithdrawData, - withdraw_proof_signer: &S, - ) -> TokenResult { - // create withdraw proof context state - let instruction_type = ProofInstruction::VerifyWithdraw; - let space = size_of::>(); - let rent = self - .client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?; - - let withdraw_proof_context_state_info = ContextStateInfo { - context_state_account, - context_state_authority, - }; - - self.process_ixs( - &[system_instruction::create_account( - &self.payer.pubkey(), - context_state_account, - rent, - space as u64, - &zk_token_proof_program::id(), - )], - &[withdraw_proof_signer], - ) - .await?; - - // This instruction is right at the transaction size limit, so we cannot - // add any other instructions to it - let blockhash = self - .client - .get_latest_blockhash() - .await - .map_err(TokenError::Client)?; - - let transaction = Transaction::new_signed_with_payer( - &[instruction_type - .encode_verify_proof(Some(withdraw_proof_context_state_info), withdraw_proof_data)], - Some(&self.payer.pubkey()), - &[self.payer.as_ref()], - blockhash, - ); - - self.client - .send_transaction(&transaction) - .await - .map_err(TokenError::Client) - } - /// Transfer tokens confidentially #[allow(clippy::too_many_arguments)] pub async fn confidential_transfer_transfer( @@ -2202,7 +2165,11 @@ where TransferAccountInfo::new(confidential_transfer_account) }; - let (equality_proof_data, ciphertext_validity_proof_data, range_proof_data) = account_info + let TransferProofData { + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + } = account_info .generate_split_transfer_proof_data( transfer_amount, source_elgamal_keypair, @@ -2381,143 +2348,81 @@ where .await } - /// Create an equality proof context state account for a confidential - /// transfer. - #[allow(clippy::too_many_arguments)] - pub async fn create_equality_proof_context_state_for_transfer( - &self, - context_state_account: &Pubkey, - context_state_authority: &Pubkey, - equality_proof_data: &CiphertextCommitmentEqualityProofData, - equality_proof_signer: &S, - ) -> TokenResult { - // create equality proof context state - let instruction_type = ProofInstruction::VerifyCiphertextCommitmentEquality; - let space = size_of::>(); - let rent = self - .client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?; - - let equality_proof_context_state_info = ContextStateInfo { - context_state_account, - context_state_authority, - }; - - self.process_ixs( - &[ - system_instruction::create_account( - &self.payer.pubkey(), - context_state_account, - rent, - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type.encode_verify_proof( - Some(equality_proof_context_state_info), - equality_proof_data, - ), - ], - &[equality_proof_signer], - ) - .await - } - - /// Create a ciphertext validity proof context state account for a - /// confidential transfer. - pub async fn create_ciphertext_validity_proof_context_state_for_transfer( + /// Create a context state account containing zero-knowledge proof needed + /// for a confidential transfer instruction. + pub async fn confidential_transfer_create_context_state_account< + S: Signer, + ZK: Pod + ZkProofData, + U: Pod, + >( &self, context_state_account: &Pubkey, context_state_authority: &Pubkey, - ciphertext_validity_proof_data: &BatchedGroupedCiphertext3HandlesValidityProofData, - ciphertext_validity_proof_signer: &S, + proof_data: &ZK, + split_account_creation_and_proof_verification: bool, + signer: &S, ) -> TokenResult { - // create ciphertext validity proof context state - let instruction_type = ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity; - let space = - size_of::>(); + let instruction_type = zk_proof_type_to_instruction(ZK::PROOF_TYPE)?; + let space = size_of::>(); let rent = self .client .get_minimum_balance_for_rent_exemption(space) .await .map_err(TokenError::Client)?; - let ciphertext_validity_proof_context_state_info = ContextStateInfo { + let context_state_info = ContextStateInfo { context_state_account, context_state_authority, }; - self.process_ixs( - &[ - system_instruction::create_account( + // Some proof instructions are right at the transaction size limit, but in the + // future it might be able to support the transfer too + if split_account_creation_and_proof_verification { + self.process_ixs( + &[system_instruction::create_account( &self.payer.pubkey(), context_state_account, rent, space as u64, - &zk_token_proof_program::id(), - ), - instruction_type.encode_verify_proof( - Some(ciphertext_validity_proof_context_state_info), - ciphertext_validity_proof_data, - ), - ], - &[ciphertext_validity_proof_signer], - ) - .await - } - - /// Create a range proof context state account for a confidential transfer. - pub async fn create_range_proof_context_state_for_transfer( - &self, - context_state_account: &Pubkey, - context_state_authority: &Pubkey, - range_proof_data: &BatchedRangeProofU128Data, - range_proof_keypair: &S, - ) -> TokenResult { - let instruction_type = ProofInstruction::VerifyBatchedRangeProofU128; - let space = size_of::>(); - let rent = self - .client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?; - let range_proof_context_state_info = ContextStateInfo { - context_state_account, - context_state_authority, - }; - self.process_ixs( - &[system_instruction::create_account( - &self.payer.pubkey(), - context_state_account, - rent, - space as u64, - &zk_token_proof_program::id(), - )], - &[range_proof_keypair], - ) - .await?; + &zk_elgamal_proof_program::id(), + )], + &[signer], + ) + .await?; - // This instruction is right at the transaction size limit, but in the - // future it might be able to support the transfer too - let blockhash = self - .client - .get_latest_blockhash() - .await - .map_err(TokenError::Client)?; + let blockhash = self + .client + .get_latest_blockhash() + .await + .map_err(TokenError::Client)?; - let transaction = Transaction::new_signed_with_payer( - &[instruction_type - .encode_verify_proof(Some(range_proof_context_state_info), range_proof_data)], - Some(&self.payer.pubkey()), - &[self.payer.as_ref()], - blockhash, - ); + let transaction = Transaction::new_signed_with_payer( + &[instruction_type.encode_verify_proof(Some(context_state_info), proof_data)], + Some(&self.payer.pubkey()), + &[self.payer.as_ref()], + blockhash, + ); - self.client - .send_transaction(&transaction) + self.client + .send_transaction(&transaction) + .await + .map_err(TokenError::Client) + } else { + self.process_ixs( + &[ + system_instruction::create_account( + &self.payer.pubkey(), + context_state_account, + rent, + space as u64, + &zk_elgamal_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), proof_data), + ], + &[signer], + ) .await - .map_err(TokenError::Client) + } } /// Close a ZK Token proof program context state @@ -2534,7 +2439,7 @@ where }; self.process_ixs( - &[zk_token_proof_instruction::close_context_state( + &[close_context_state( context_state_info, lamport_destination_account, )], @@ -2578,38 +2483,24 @@ where TransferAccountInfo::new(confidential_transfer_account) }; - let current_source_available_balance = account_info - .available_balance - .try_into() - .map_err(|_| TokenError::AccountDecryption)?; - let current_decryptable_available_balance = account_info - .decryptable_available_balance - .try_into() - .map_err(|_| TokenError::AccountDecryption)?; - - let fee_parameters = FeeParameters { - fee_rate_basis_points, - maximum_fee, - }; - - let ( + let TransferWithFeeProofData { equality_proof_data, transfer_amount_ciphertext_validity_proof_data, - fee_sigma_proof_data, + percentage_with_cap_proof_data, fee_ciphertext_validity_proof_data, range_proof_data, - ) = transfer_with_fee_split_proof_data( - ¤t_source_available_balance, - ¤t_decryptable_available_balance, - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - &fee_parameters, - ) - .map_err(|_| TokenError::ProofGeneration)?; + } = account_info + .generate_split_transfer_with_fee_proof_data( + transfer_amount, + source_elgamal_keypair, + source_aes_key, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + withdraw_withheld_authority_elgamal_pubkey, + fee_rate_basis_points, + maximum_fee, + ) + .map_err(|_| TokenError::ProofGeneration)?; let equality_proof_data = equality_proof_account .is_none() @@ -2620,7 +2511,7 @@ where .then_some(transfer_amount_ciphertext_validity_proof_data); let fee_sigma_proof_data = fee_sigma_proof_account .is_none() - .then_some(fee_sigma_proof_data); + .then_some(percentage_with_cap_proof_data); let fee_ciphertext_validity_proof_data = fee_ciphertext_validity_proof_account .is_none() .then_some(fee_ciphertext_validity_proof_data); @@ -2697,209 +2588,6 @@ where self.process_ixs(&instructions, signing_keypairs).await } - /// Create equality proof context state accounts for a confidential transfer - /// with fee. - #[allow(clippy::too_many_arguments)] - pub async fn create_equality_proof_context_state_for_transfer_with_fee( - &self, - context_state_account: &Pubkey, - context_state_authority: &Pubkey, - equality_proof_data: &CiphertextCommitmentEqualityProofData, - signing_keypair: &S, - ) -> TokenResult { - let instruction_type = ProofInstruction::VerifyCiphertextCommitmentEquality; - let space = size_of::>(); - let rent = self - .client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?; - let equality_proof_context_state_info = ContextStateInfo { - context_state_account, - context_state_authority, - }; - self.process_ixs( - &[ - system_instruction::create_account( - &self.payer.pubkey(), - context_state_account, - rent, - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type.encode_verify_proof( - Some(equality_proof_context_state_info), - equality_proof_data, - ), - ], - &[signing_keypair], - ) - .await - } - - /// Create a transfer amount ciphertext validity proof context state account - /// for a confidential transfer with fee. - #[allow(clippy::too_many_arguments)] - pub async fn create_transfer_amount_ciphertext_validity_proof_context_state_for_transfer_with_fee< - S: Signer, - >( - &self, - context_state_account: &Pubkey, - context_state_authority: &Pubkey, - transfer_amount_ciphertext_validity_proof_data: &BatchedGroupedCiphertext3HandlesValidityProofData, - signing_keypair: &S, - ) -> TokenResult { - let instruction_type = ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity; - let space = - size_of::>(); - let rent = self - .client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?; - let transfer_amount_ciphertext_validity_proof_context_state_info = ContextStateInfo { - context_state_account, - context_state_authority, - }; - self.process_ixs( - &[ - system_instruction::create_account( - &self.payer.pubkey(), - context_state_account, - rent, - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type.encode_verify_proof( - Some(transfer_amount_ciphertext_validity_proof_context_state_info), - transfer_amount_ciphertext_validity_proof_data, - ), - ], - &[signing_keypair], - ) - .await - } - - /// Create a fee sigma proof context state account for a confidential - /// transfer with fee. - #[allow(clippy::too_many_arguments)] - pub async fn create_fee_sigma_proof_context_state_for_transfer_with_fee( - &self, - context_state_account: &Pubkey, - context_state_authority: &Pubkey, - fee_sigma_proof_data: &FeeSigmaProofData, - signing_keypair: &S, - ) -> TokenResult { - let instruction_type = ProofInstruction::VerifyFeeSigma; - let space = size_of::>(); - let rent = self - .client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?; - let fee_sigma_proof_context_state_info = ContextStateInfo { - context_state_account, - context_state_authority, - }; - self.process_ixs( - &[ - system_instruction::create_account( - &self.payer.pubkey(), - context_state_account, - rent, - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type.encode_verify_proof( - Some(fee_sigma_proof_context_state_info), - fee_sigma_proof_data, - ), - ], - &[signing_keypair], - ) - .await - } - - /// Create a fee ciphertext validity proof context state account for a - /// confidential transfer with fee. - #[allow(clippy::too_many_arguments)] - pub async fn create_fee_ciphertext_validity_proof_context_state_for_transfer_with_fee< - S: Signer, - >( - &self, - context_state_account: &Pubkey, - context_state_authority: &Pubkey, - fee_ciphertext_validity_proof_data: &BatchedGroupedCiphertext2HandlesValidityProofData, - signing_keypair: &S, - ) -> TokenResult { - let instruction_type = ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity; - let space = - size_of::>(); - let rent = self - .client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?; - let fee_ciphertext_validity_proof_context_state_info = ContextStateInfo { - context_state_account, - context_state_authority, - }; - self.process_ixs( - &[ - system_instruction::create_account( - &self.payer.pubkey(), - context_state_account, - rent, - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type.encode_verify_proof( - Some(fee_ciphertext_validity_proof_context_state_info), - fee_ciphertext_validity_proof_data, - ), - ], - &[signing_keypair], - ) - .await - } - - /// Create a range proof context state for a confidential transfer with fee. - #[allow(clippy::too_many_arguments)] - pub async fn create_range_proof_context_state_for_transfer_with_fee( - &self, - context_state_account: &Pubkey, - context_state_authority: &Pubkey, - range_proof_data: &BatchedRangeProofU256Data, - signing_keypair: &S, - ) -> TokenResult { - let instruction_type = ProofInstruction::VerifyBatchedRangeProofU256; - let space = size_of::>(); - let rent = self - .client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?; - let range_proof_context_state_info = ContextStateInfo { - context_state_account, - context_state_authority, - }; - self.process_ixs( - &[ - system_instruction::create_account( - &self.payer.pubkey(), - context_state_account, - rent, - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type - .encode_verify_proof(Some(range_proof_context_state_info), range_proof_data), - ], - &[signing_keypair], - ) - .await - } - /// Applies the confidential transfer pending balance to the available /// balance pub async fn confidential_transfer_apply_pending_balance( @@ -2933,7 +2621,7 @@ where &self.program_id, account, expected_pending_balance_credit_counter, - new_decryptable_available_balance, + new_decryptable_available_balance.into(), authority, &multisig_signers, )?], diff --git a/token/confidential-transfer/proof-extraction/src/encryption.rs b/token/confidential-transfer/proof-extraction/src/encryption.rs index 49db2b61cec..34ca2fff156 100644 --- a/token/confidential-transfer/proof-extraction/src/encryption.rs +++ b/token/confidential-transfer/proof-extraction/src/encryption.rs @@ -1,11 +1,48 @@ -use solana_zk_sdk::encryption::pod::grouped_elgamal::{ - PodGroupedElGamalCiphertext2Handles, PodGroupedElGamalCiphertext3Handles, +use { + crate::errors::TokenProofExtractionError, + solana_zk_sdk::encryption::pod::{ + elgamal::PodElGamalCiphertext, + grouped_elgamal::{ + PodGroupedElGamalCiphertext2Handles, PodGroupedElGamalCiphertext3Handles, + }, + pedersen::PodPedersenCommitment, + }, }; #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(C)] 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, + ) -> Result { + self.0 + .try_extract_ciphertext(index) + .map_err(|_| TokenProofExtractionError::CiphertextExtraction) + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(C)] 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, + ) -> Result { + self.0 + .try_extract_ciphertext(index) + .map_err(|_| TokenProofExtractionError::CiphertextExtraction) + } +} diff --git a/token/confidential-transfer/proof-extraction/src/errors.rs b/token/confidential-transfer/proof-extraction/src/errors.rs index 4e05a11a29b..e710dec7159 100644 --- a/token/confidential-transfer/proof-extraction/src/errors.rs +++ b/token/confidential-transfer/proof-extraction/src/errors.rs @@ -12,4 +12,6 @@ pub enum TokenProofExtractionError { FeeParametersMismatch, #[error("Curve arithmetic failed")] CurveArithmetic, + #[error("Ciphertext extraction failed")] + CiphertextExtraction, } diff --git a/token/program-2022-test/Cargo.toml b/token/program-2022-test/Cargo.toml index bb9ec0caed8..88652873717 100644 --- a/token/program-2022-test/Cargo.toml +++ b/token/program-2022-test/Cargo.toml @@ -18,6 +18,7 @@ walkdir = "2" [dev-dependencies] async-trait = "0.1" borsh = "1.5.1" +bytemuck = "1.16.3" futures-util = "0.3" solana-program = "2.0.3" solana-program-test = "2.0.3" @@ -33,6 +34,7 @@ spl-record = { version = "0.2.0", path = "../../record/program", features = [ spl-token-2022 = { version = "4.0.0", path = "../program-2022", features = [ "no-entrypoint", ] } +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 c2e8a657f8f..604cdd5dcc4 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -2,6 +2,7 @@ mod program_test; use { + bytemuck::Zeroable, program_test::{ ConfidentialTokenAccountBalances, ConfidentialTokenAccountMeta, TestContext, TokenContext, }, @@ -26,21 +27,27 @@ use { }, BaseStateWithExtensions, ExtensionType, }, - solana_zk_token_sdk::{ - encryption::{auth_encryption::*, elgamal::*}, - zk_token_elgamal::pod::{self, Zeroable}, - zk_token_proof_instruction::*, - zk_token_proof_program, - zk_token_proof_state::ProofContextState, + solana_zk_sdk::{ + encryption::{auth_encryption::*, elgamal::*, pod::elgamal::PodElGamalCiphertext}, + zk_elgamal_proof_program::{ + self, + instruction::{ContextStateInfo, ProofInstruction}, + proof_data::*, + state::ProofContextState, + }, }, }, spl_token_client::{ client::ProgramBanksClientProcessTransaction, - proof_generation::{transfer_with_fee_split_proof_data, ProofAccount}, token::{ - ExtensionInitializationParams, Token, TokenError as TokenClientError, TokenResult, + ExtensionInitializationParams, ProofAccount, Token, TokenError as TokenClientError, + TokenResult, }, }, + spl_token_confidential_transfer_proof_generation::{ + transfer::TransferProofData, transfer_with_fee::TransferWithFeeProofData, + withdraw::WithdrawProofData, + }, std::{convert::TryInto, mem::size_of}, }; @@ -575,18 +582,9 @@ async fn confidential_transfer_deposit() { assert_eq!(extension.pending_balance_credit_counter, 0.into()); assert_eq!(extension.expected_pending_balance_credit_counter, 0.into()); assert_eq!(extension.actual_pending_balance_credit_counter, 0.into()); - assert_eq!( - extension.pending_balance_lo, - pod::ElGamalCiphertext::zeroed() - ); - assert_eq!( - extension.pending_balance_hi, - pod::ElGamalCiphertext::zeroed() - ); - assert_eq!( - extension.available_balance, - pod::ElGamalCiphertext::zeroed() - ); + assert_eq!(extension.pending_balance_lo, PodElGamalCiphertext::zeroed()); + assert_eq!(extension.pending_balance_hi, PodElGamalCiphertext::zeroed()); + assert_eq!(extension.available_balance, PodElGamalCiphertext::zeroed()); token .confidential_transfer_deposit( @@ -741,157 +739,220 @@ async fn confidential_transfer_deposit() { assert_eq!(extension.actual_pending_balance_credit_counter, 2.into()); } +#[allow(clippy::too_many_arguments)] #[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_withdraw() { - let authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); +async fn withdraw_with_option( + token: &Token, + source_account: &Pubkey, + source_authority: &Pubkey, + withdraw_amount: u64, + decimals: u8, + source_elgamal_keypair: &ElGamalKeypair, + source_aes_key: &AeKey, + signing_keypairs: &S, + option: ConfidentialTransferOption, +) -> TokenResult<()> { + match option { + ConfidentialTransferOption::InstructionData => { + token + .confidential_transfer_withdraw( + source_account, + source_authority, + None, + None, + withdraw_amount, + decimals, + None, + source_elgamal_keypair, + source_aes_key, + signing_keypairs, + ) + .await + } + ConfidentialTransferOption::RecordAccount => { + let state = token.get_account_info(source_account).await.unwrap(); + let extension = state + .get_extension::() + .unwrap(); + let withdraw_account_info = WithdrawAccountInfo::new(extension); - 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 WithdrawProofData { + equality_proof_data, + range_proof_data, + } = withdraw_account_info + .generate_proof_data(withdraw_amount, source_elgamal_keypair, source_aes_key) + .unwrap(); - let TokenContext { - token, - alice, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( - &token, - &alice, - None, - false, - false, - &mint_authority, - 42, - decimals, - ) - .await; + let equality_proof_record_account = Keypair::new(); + let range_proof_record_account = Keypair::new(); + let record_account_authority = Keypair::new(); - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - assert_eq!(state.base.amount, 0); - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 42, - decryptable_available_balance: 42, - }, - ) - .await; + token + .confidential_transfer_create_record_account( + &equality_proof_record_account.pubkey(), + &record_account_authority.pubkey(), + &equality_proof_data, + &equality_proof_record_account, + &record_account_authority, + ) + .await + .unwrap(); - // withdraw zero amount - token - .confidential_transfer_withdraw( - &alice_meta.token_account, - &alice.pubkey(), - None, - 0, - decimals, - None, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - &[&alice], - ) - .await - .unwrap(); + let equality_proof_account = ProofAccount::RecordAccount( + equality_proof_record_account.pubkey(), + RecordData::WRITABLE_START_INDEX as u32, + ); - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 42, - decryptable_available_balance: 42, - }, - ) - .await; + token + .confidential_transfer_create_record_account( + &range_proof_record_account.pubkey(), + &record_account_authority.pubkey(), + &range_proof_data, + &range_proof_record_account, + &record_account_authority, + ) + .await + .unwrap(); - // withdraw entire balance - token - .confidential_transfer_withdraw( - &alice_meta.token_account, - &alice.pubkey(), - None, - 42, - decimals, - None, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - &[&alice], - ) - .await - .unwrap(); + let range_proof_account = ProofAccount::RecordAccount( + range_proof_record_account.pubkey(), + RecordData::WRITABLE_START_INDEX as u32, + ); - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - assert_eq!(state.base.amount, 42); - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; + let result = token + .confidential_transfer_withdraw( + source_account, + source_authority, + Some(&equality_proof_account), + Some(&range_proof_account), + withdraw_amount, + decimals, + None, + source_elgamal_keypair, + source_aes_key, + signing_keypairs, + ) + .await; - // attempt to withdraw without enough funds - let err = token - .confidential_transfer_withdraw( - &alice_meta.token_account, - &alice.pubkey(), - None, - 1, - decimals, - None, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - &[&alice], - ) - .await - .unwrap_err(); + token + .confidential_transfer_close_record_account( + &equality_proof_record_account.pubkey(), + &record_account_authority.pubkey(), + source_account, + &record_account_authority, + ) + .await + .unwrap(); + + token + .confidential_transfer_close_record_account( + &range_proof_record_account.pubkey(), + &record_account_authority.pubkey(), + source_account, + &record_account_authority, + ) + .await + .unwrap(); - assert_eq!(err, TokenClientError::ProofGeneration); + result + } + ConfidentialTransferOption::ContextStateAccount => { + let state = token.get_account_info(source_account).await.unwrap(); + let extension = state + .get_extension::() + .unwrap(); + let withdraw_account_info = WithdrawAccountInfo::new(extension); - token - .confidential_transfer_empty_account( - &alice_meta.token_account, - &alice.pubkey(), - None, - None, - &alice_meta.elgamal_keypair, - &[&alice], - ) - .await - .unwrap(); + let WithdrawProofData { + equality_proof_data, + range_proof_data, + } = withdraw_account_info + .generate_proof_data(withdraw_amount, source_elgamal_keypair, source_aes_key) + .unwrap(); + + let equality_proof_context_account = Keypair::new(); + let range_proof_context_account = Keypair::new(); + let context_account_authority = Keypair::new(); + + token + .confidential_transfer_create_context_state_account( + &equality_proof_context_account.pubkey(), + &context_account_authority.pubkey(), + &equality_proof_data, + false, + &equality_proof_context_account, + ) + .await + .unwrap(); + + let equality_proof_account = + ProofAccount::ContextAccount(equality_proof_context_account.pubkey()); + + token + .confidential_transfer_create_context_state_account( + &range_proof_context_account.pubkey(), + &context_account_authority.pubkey(), + &range_proof_data, + false, + &range_proof_context_account, + ) + .await + .unwrap(); + + let range_proof_account = + ProofAccount::ContextAccount(range_proof_context_account.pubkey()); + + let result = token + .confidential_transfer_withdraw( + source_account, + source_authority, + Some(&equality_proof_account), + Some(&range_proof_account), + withdraw_amount, + decimals, + None, + source_elgamal_keypair, + source_aes_key, + signing_keypairs, + ) + .await; + + token + .confidential_transfer_close_context_state( + &equality_proof_context_account.pubkey(), + source_account, + &context_account_authority.pubkey(), + &context_account_authority, + ) + .await + .unwrap(); + + token + .confidential_transfer_close_context_state( + &range_proof_context_account.pubkey(), + source_account, + &context_account_authority.pubkey(), + &context_account_authority, + ) + .await + .unwrap(); + + result + } + } } #[cfg(feature = "zk-ops")] #[tokio::test] -async fn confidential_transfer_withdraw_with_record_account() { +async fn confidential_transfer_withdraw() { + confidential_transfer_withdraw_with_option(ConfidentialTransferOption::InstructionData).await; + confidential_transfer_withdraw_with_option(ConfidentialTransferOption::RecordAccount).await; + confidential_transfer_withdraw_with_option(ConfidentialTransferOption::ContextStateAccount) + .await; +} + +#[cfg(feature = "zk-ops")] +async fn confidential_transfer_withdraw_with_option(option: ConfidentialTransferOption) { let authority = Keypair::new(); let auto_approve_new_accounts = true; let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); @@ -945,52 +1006,47 @@ async fn confidential_transfer_withdraw_with_record_account() { ) .await; - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let account_info = WithdrawAccountInfo::new(extension); - - let withdraw_proof = account_info - .generate_proof_data(42, &alice_meta.elgamal_keypair, &alice_meta.aes_key) - .unwrap(); - - let record_account = Keypair::new(); - let record_account_authority = Keypair::new(); - - token - .confidential_transfer_create_record_account( - &record_account.pubkey(), - &record_account_authority.pubkey(), - &withdraw_proof, - &record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let proof_account = ProofAccount::RecordAccount( - record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); + // withdraw zero amount + withdraw_with_option( + &token, + &alice_meta.token_account, + &alice.pubkey(), + 0, + decimals, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key, + &[&alice], + option, + ) + .await + .unwrap(); - token - .confidential_transfer_withdraw( - &alice_meta.token_account, - &alice.pubkey(), - Some(&proof_account), - 42, - decimals, - None, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - &[&alice], + alice_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 0, + pending_balance_hi: 0, + available_balance: 42, + decryptable_available_balance: 42, + }, ) - .await - .unwrap(); + .await; + + // withdraw entire balance + withdraw_with_option( + &token, + &alice_meta.token_account, + &alice.pubkey(), + 42, + decimals, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key, + &[&alice], + option, + ) + .await + .unwrap(); let state = token .get_account_info(&alice_meta.token_account) @@ -1008,16 +1064,6 @@ async fn confidential_transfer_withdraw_with_record_account() { }, ) .await; - - token - .confidential_transfer_close_record_account( - &record_account.pubkey(), - &record_account_authority.pubkey(), - &alice.pubkey(), - &record_account_authority, - ) - .await - .unwrap(); } #[derive(Clone, Copy)] @@ -1076,16 +1122,19 @@ async fn confidential_transfer_with_option( .unwrap(); let transfer_account_info = TransferAccountInfo::new(extension); - let (equality_proof_data, ciphertext_validity_proof_data, range_proof_data) = - transfer_account_info - .generate_split_transfer_proof_data( - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ) - .unwrap(); + let TransferProofData { + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + } = transfer_account_info + .generate_split_transfer_proof_data( + transfer_amount, + source_elgamal_keypair, + source_aes_key, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + ) + .unwrap(); let equality_proof_record_account = Keypair::new(); let ciphertext_validity_proof_record_account = Keypair::new(); @@ -1203,16 +1252,19 @@ async fn confidential_transfer_with_option( .unwrap(); let transfer_account_info = TransferAccountInfo::new(extension); - let (equality_proof_data, ciphertext_validity_proof_data, range_proof_data) = - transfer_account_info - .generate_split_transfer_proof_data( - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ) - .unwrap(); + let TransferProofData { + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + } = transfer_account_info + .generate_split_transfer_proof_data( + transfer_amount, + source_elgamal_keypair, + source_aes_key, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + ) + .unwrap(); let equality_proof_context_account = Keypair::new(); let ciphertext_validity_proof_context_account = Keypair::new(); @@ -1220,10 +1272,11 @@ async fn confidential_transfer_with_option( let context_account_authority = Keypair::new(); token - .create_equality_proof_context_state_for_transfer( + .confidential_transfer_create_context_state_account( &equality_proof_context_account.pubkey(), &context_account_authority.pubkey(), &equality_proof_data, + false, &equality_proof_context_account, ) .await @@ -1233,10 +1286,11 @@ async fn confidential_transfer_with_option( ProofAccount::ContextAccount(equality_proof_context_account.pubkey()); token - .create_ciphertext_validity_proof_context_state_for_transfer( + .confidential_transfer_create_context_state_account( &ciphertext_validity_proof_context_account.pubkey(), &context_account_authority.pubkey(), &ciphertext_validity_proof_data, + false, &ciphertext_validity_proof_context_account, ) .await @@ -1246,10 +1300,11 @@ async fn confidential_transfer_with_option( ProofAccount::ContextAccount(ciphertext_validity_proof_context_account.pubkey()); token - .create_range_proof_context_state_for_transfer( + .confidential_transfer_create_context_state_account( &range_proof_context_account.pubkey(), &context_account_authority.pubkey(), &range_proof_data, + false, &range_proof_context_account, ) .await @@ -1621,36 +1676,24 @@ async fn confidential_transfer_with_fee_with_option( .unwrap(); let transfer_account_info = TransferAccountInfo::new(extension); - let current_source_available_balance = - transfer_account_info.available_balance.try_into().unwrap(); - let current_source_decryptable_balance = transfer_account_info - .decryptable_available_balance - .try_into() - .unwrap(); - - let fee_parameters = FeeParameters { - fee_rate_basis_points, - maximum_fee, - }; - - let ( + let TransferWithFeeProofData { equality_proof_data, transfer_amount_ciphertext_validity_proof_data, - fee_sigma_proof_data, + percentage_with_cap_proof_data, fee_ciphertext_validity_proof_data, range_proof_data, - ) = transfer_with_fee_split_proof_data( - ¤t_source_available_balance, - ¤t_source_decryptable_balance, - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - &fee_parameters, - ) - .unwrap(); + } = transfer_account_info + .generate_split_transfer_with_fee_proof_data( + transfer_amount, + source_elgamal_keypair, + source_aes_key, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + withdraw_withheld_authority_elgamal_pubkey, + fee_rate_basis_points, + maximum_fee, + ) + .unwrap(); let equality_proof_record_account = Keypair::new(); let transfer_amount_ciphertext_validity_proof_record_account = Keypair::new(); @@ -1695,7 +1738,7 @@ async fn confidential_transfer_with_fee_with_option( .confidential_transfer_create_record_account( &fee_sigma_proof_record_account.pubkey(), &record_account_authority.pubkey(), - &fee_sigma_proof_data, + &percentage_with_cap_proof_data, &fee_sigma_proof_record_account, &record_account_authority, ) @@ -1827,49 +1870,38 @@ async fn confidential_transfer_with_fee_with_option( .unwrap(); let transfer_account_info = TransferAccountInfo::new(extension); - let current_source_available_balance = - transfer_account_info.available_balance.try_into().unwrap(); - let current_source_decryptable_balance = transfer_account_info - .decryptable_available_balance - .try_into() - .unwrap(); - - let fee_parameters = FeeParameters { - fee_rate_basis_points, - maximum_fee, - }; - - let ( + let TransferWithFeeProofData { equality_proof_data, transfer_amount_ciphertext_validity_proof_data, - fee_sigma_proof_data, + percentage_with_cap_proof_data, fee_ciphertext_validity_proof_data, range_proof_data, - ) = transfer_with_fee_split_proof_data( - ¤t_source_available_balance, - ¤t_source_decryptable_balance, - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - &fee_parameters, - ) - .unwrap(); + } = transfer_account_info + .generate_split_transfer_with_fee_proof_data( + transfer_amount, + source_elgamal_keypair, + source_aes_key, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + withdraw_withheld_authority_elgamal_pubkey, + fee_rate_basis_points, + maximum_fee, + ) + .unwrap(); let equality_proof_context_account = Keypair::new(); let transfer_amount_ciphertext_validity_proof_context_account = Keypair::new(); - let fee_sigma_proof_context_account = Keypair::new(); + let percentage_with_cap_proof_context_account = Keypair::new(); let fee_ciphertext_validity_proof_context_account = Keypair::new(); let range_proof_context_account = Keypair::new(); let context_account_authority = Keypair::new(); token - .create_equality_proof_context_state_for_transfer_with_fee( + .confidential_transfer_create_context_state_account( &equality_proof_context_account.pubkey(), &context_account_authority.pubkey(), &equality_proof_data, + false, &equality_proof_context_account, ) .await @@ -1879,10 +1911,11 @@ async fn confidential_transfer_with_fee_with_option( ProofAccount::ContextAccount(equality_proof_context_account.pubkey()); token - .create_transfer_amount_ciphertext_validity_proof_context_state_for_transfer_with_fee( + .confidential_transfer_create_context_state_account( &transfer_amount_ciphertext_validity_proof_context_account.pubkey(), &context_account_authority.pubkey(), &transfer_amount_ciphertext_validity_proof_data, + false, &transfer_amount_ciphertext_validity_proof_context_account, ) .await @@ -1894,23 +1927,25 @@ async fn confidential_transfer_with_fee_with_option( ); token - .create_fee_sigma_proof_context_state_for_transfer_with_fee( - &fee_sigma_proof_context_account.pubkey(), + .confidential_transfer_create_context_state_account( + &percentage_with_cap_proof_context_account.pubkey(), &context_account_authority.pubkey(), - &fee_sigma_proof_data, - &fee_sigma_proof_context_account, + &percentage_with_cap_proof_data, + false, + &percentage_with_cap_proof_context_account, ) .await .unwrap(); let fee_sigma_proof_context_proof_account = - ProofAccount::ContextAccount(fee_sigma_proof_context_account.pubkey()); + ProofAccount::ContextAccount(percentage_with_cap_proof_context_account.pubkey()); token - .create_fee_ciphertext_validity_proof_context_state_for_transfer_with_fee( + .confidential_transfer_create_context_state_account( &fee_ciphertext_validity_proof_context_account.pubkey(), &context_account_authority.pubkey(), &fee_ciphertext_validity_proof_data, + false, &fee_ciphertext_validity_proof_context_account, ) .await @@ -1921,10 +1956,11 @@ async fn confidential_transfer_with_fee_with_option( ); token - .create_range_proof_context_state_for_transfer_with_fee( + .confidential_transfer_create_context_state_account( &range_proof_context_account.pubkey(), &context_account_authority.pubkey(), &range_proof_data, + false, &range_proof_context_account, ) .await @@ -1984,7 +2020,7 @@ async fn confidential_transfer_with_fee_with_option( token .confidential_transfer_close_context_state( - &fee_sigma_proof_context_account.pubkey(), + &percentage_with_cap_proof_context_account.pubkey(), source_account, &context_account_authority.pubkey(), &context_account_authority, @@ -2616,7 +2652,8 @@ async fn confidential_transfer_configure_token_account_with_proof_context() { }; let proof_data = - confidential_transfer::instruction::PubkeyValidityData::new(&elgamal_keypair).unwrap(); + confidential_transfer::instruction::PubkeyValidityProofData::new(&elgamal_keypair) + .unwrap(); let mut ctx = context.context.lock().await; let rent = ctx.banks_client.get_rent().await.unwrap(); @@ -2627,7 +2664,7 @@ async fn confidential_transfer_configure_token_account_with_proof_context() { &context_state_account.pubkey(), rent.minimum_balance(space), space as u64, - &zk_token_proof_program::id(), + &zk_elgamal_proof_program::id(), ), instruction_type.encode_verify_proof(Some(context_state_info), &proof_data), ]; @@ -2692,16 +2729,16 @@ async fn confidential_transfer_configure_token_account_with_proof_context() { { let context_state_authority = Keypair::new(); - let space = size_of::>(); + let space = size_of::>(); - let instruction_type = ProofInstruction::VerifyZeroBalance; + let instruction_type = ProofInstruction::VerifyZeroCiphertext; let context_state_info = ContextStateInfo { context_state_account: &context_state_account.pubkey(), context_state_authority: &context_state_authority.pubkey(), }; let ciphertext = elgamal_keypair.pubkey().encrypt(0_u64); - let proof_data = confidential_transfer::instruction::ZeroBalanceProofData::new( + let proof_data = confidential_transfer::instruction::ZeroCiphertextProofData::new( &elgamal_keypair, &ciphertext, ) @@ -2716,7 +2753,7 @@ async fn confidential_transfer_configure_token_account_with_proof_context() { &context_state_account.pubkey(), rent.minimum_balance(space), space as u64, - &zk_token_proof_program::id(), + &zk_elgamal_proof_program::id(), ), instruction_type.encode_verify_proof(Some(context_state_info), &proof_data), ]; @@ -2781,16 +2818,16 @@ async fn confidential_transfer_empty_account_with_proof_context() { // create context state { let context_state_authority = Keypair::new(); - let space = size_of::>(); + let space = size_of::>(); - let instruction_type = ProofInstruction::VerifyZeroBalance; + let instruction_type = ProofInstruction::VerifyZeroCiphertext; let context_state_info = ContextStateInfo { context_state_account: &context_state_account.pubkey(), context_state_authority: &context_state_authority.pubkey(), }; - let proof_data = confidential_transfer::instruction::ZeroBalanceProofData::new( + let proof_data = confidential_transfer::instruction::ZeroCiphertextProofData::new( &alice_meta.elgamal_keypair, &ElGamalCiphertext::default(), ) @@ -2805,7 +2842,7 @@ async fn confidential_transfer_empty_account_with_proof_context() { &context_state_account.pubkey(), rent.minimum_balance(space), space as u64, - &zk_token_proof_program::id(), + &zk_elgamal_proof_program::id(), ), instruction_type.encode_verify_proof(Some(context_state_info), &proof_data), ]; @@ -2849,185 +2886,11 @@ async fn confidential_transfer_empty_account_with_proof_context() { context_state_authority: &context_state_authority.pubkey(), }; - let proof_data = - confidential_transfer::instruction::PubkeyValidityData::new(&bob_meta.elgamal_keypair) - .unwrap(); - - let mut ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &context_state_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type.encode_verify_proof(Some(context_state_info), &proof_data), - ]; - - let last_blockhash = ctx.get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &context_state_account], - last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - } - - let err = token - .confidential_transfer_empty_account( - &bob_meta.token_account, - &bob.pubkey(), - Some(&ProofAccount::ContextAccount( - context_state_account.pubkey(), - )), - None, + let proof_data = confidential_transfer::instruction::PubkeyValidityProofData::new( &bob_meta.elgamal_keypair, - &[&bob], - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidArgument,) - ))) - ); -} - -#[tokio::test] -async fn confidential_transfer_withdraw_with_proof_context() { - let authority = Keypair::new(); - let auto_approve_new_accounts = true; - - 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: None, - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( - &token, - &alice, - None, - false, - false, - &mint_authority, - 42, - decimals, - ) - .await; - - let context_state_account = Keypair::new(); - - // create context state - { - let context_state_authority = Keypair::new(); - let space = size_of::>(); - - let instruction_type = ProofInstruction::VerifyWithdraw; - - let context_state_info = ContextStateInfo { - context_state_account: &context_state_account.pubkey(), - context_state_authority: &context_state_authority.pubkey(), - }; - - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let current_ciphertext = extension.available_balance.try_into().unwrap(); - - let proof_data = confidential_transfer::instruction::WithdrawData::new( - 0, - &alice_meta.elgamal_keypair, - 42, - ¤t_ciphertext, - ) - .unwrap(); - - let mut ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &context_state_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type.encode_verify_proof(Some(context_state_info), &proof_data), - ]; - - let last_blockhash = ctx.get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &context_state_account], - last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - } - - token - .confidential_transfer_withdraw( - &alice_meta.token_account, - &alice.pubkey(), - Some(&ProofAccount::ContextAccount( - context_state_account.pubkey(), - )), - 0, - decimals, - None, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - &[&alice], ) - .await .unwrap(); - // attempt to create an account with a wrong proof type context state - let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, None, false, false).await; - let context_state_account = Keypair::new(); - - { - let context_state_authority = Keypair::new(); - let space = size_of::>(); - - let instruction_type = ProofInstruction::VerifyPubkeyValidity; - - let context_state_info = ContextStateInfo { - context_state_account: &context_state_account.pubkey(), - context_state_authority: &context_state_authority.pubkey(), - }; - - let proof_data = - confidential_transfer::instruction::PubkeyValidityData::new(&bob_meta.elgamal_keypair) - .unwrap(); - let mut ctx = context.context.lock().await; let rent = ctx.banks_client.get_rent().await.unwrap(); @@ -3037,7 +2900,7 @@ async fn confidential_transfer_withdraw_with_proof_context() { &context_state_account.pubkey(), rent.minimum_balance(space), space as u64, - &zk_token_proof_program::id(), + &zk_elgamal_proof_program::id(), ), instruction_type.encode_verify_proof(Some(context_state_info), &proof_data), ]; @@ -3053,17 +2916,14 @@ async fn confidential_transfer_withdraw_with_proof_context() { } let err = token - .confidential_transfer_withdraw( + .confidential_transfer_empty_account( &bob_meta.token_account, &bob.pubkey(), Some(&ProofAccount::ContextAccount( context_state_account.pubkey(), )), - 0, - decimals, None, &bob_meta.elgamal_keypair, - &bob_meta.aes_key, &[&bob], ) .await diff --git a/token/program-2022-test/tests/confidential_transfer_fee.rs b/token/program-2022-test/tests/confidential_transfer_fee.rs index 83b1921b24d..1b0ad1a876f 100644 --- a/token/program-2022-test/tests/confidential_transfer_fee.rs +++ b/token/program-2022-test/tests/confidential_transfer_fee.rs @@ -2,6 +2,7 @@ mod program_test; use { + bytemuck::Zeroable, program_test::{TestContext, TokenContext}, solana_program_test::tokio, solana_sdk::{ @@ -26,18 +27,21 @@ use { BaseStateWithExtensions, ExtensionType, }, instruction, - solana_zk_token_sdk::{ - encryption::{auth_encryption::*, elgamal::*}, - zk_token_elgamal::pod::{self, Zeroable}, - zk_token_proof_instruction::*, - zk_token_proof_program, - zk_token_proof_state::ProofContextState, + solana_zk_sdk::{ + encryption::{auth_encryption::*, elgamal::*, pod::elgamal::PodElGamalCiphertext}, + zk_elgamal_proof_program::{ + self, + instruction::{ContextStateInfo, ProofInstruction}, + proof_data::CiphertextCiphertextEqualityProofContext, + state::ProofContextState, + }, }, }, spl_token_client::{ client::{SendTransaction, SimulateTransaction}, - proof_generation::ProofAccount, - token::{ExtensionInitializationParams, Token, TokenError as TokenClientError}, + token::{ + ExtensionInitializationParams, ProofAccount, Token, TokenError as TokenClientError, + }, }, std::{convert::TryInto, mem::size_of}, }; @@ -148,23 +152,23 @@ impl ConfidentialTokenAccountMeta { .unwrap(); assert_eq!( - extension - .pending_balance_lo - .decrypt(self.elgamal_keypair.secret()) + self.elgamal_keypair + .secret() + .decrypt_u32(&extension.pending_balance_lo.try_into().unwrap()) .unwrap(), expected.pending_balance_lo, ); assert_eq!( - extension - .pending_balance_hi - .decrypt(self.elgamal_keypair.secret()) + self.elgamal_keypair + .secret() + .decrypt_u32(&extension.pending_balance_hi.try_into().unwrap()) .unwrap(), expected.pending_balance_hi, ); assert_eq!( - extension - .available_balance - .decrypt(self.elgamal_keypair.secret()) + self.elgamal_keypair + .secret() + .decrypt_u32(&extension.available_balance.try_into().unwrap()) .unwrap(), expected.available_balance, ); @@ -197,10 +201,12 @@ async fn check_withheld_amount_in_mint( let extension = state .get_extension::() .unwrap(); - let decrypted_amount = extension - .withheld_amount - .decrypt(withdraw_withheld_authority_elgamal_keypair.secret()) + + let decrypted_amount = withdraw_withheld_authority_elgamal_keypair + .secret() + .decrypt_u32(&extension.withheld_amount.try_into().unwrap()) .unwrap(); + assert_eq!(decrypted_amount, expected); } @@ -579,7 +585,7 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_mint() { let extension = state .get_extension::() .unwrap(); - assert_eq!(extension.withheld_amount, pod::ElGamalCiphertext::zeroed()); + assert_eq!(extension.withheld_amount, PodElGamalCiphertext::zeroed()); // calculate and encrypt fee to attach to the `WithdrawWithheldTokensFromMint` // instruction data @@ -713,7 +719,7 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_mint_with_record_ac let extension = state .get_extension::() .unwrap(); - assert_eq!(extension.withheld_amount, pod::ElGamalCiphertext::zeroed()); + assert_eq!(extension.withheld_amount, PodElGamalCiphertext::zeroed()); // calculate and encrypt fee to attach to the `WithdrawWithheldTokensFromMint` // instruction data @@ -915,7 +921,7 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_accounts() { let extension = state .get_extension::() .unwrap(); - assert_eq!(extension.withheld_amount, pod::ElGamalCiphertext::zeroed()); + assert_eq!(extension.withheld_amount, PodElGamalCiphertext::zeroed()); } #[cfg(feature = "zk-ops")] @@ -1085,7 +1091,7 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_accounts_with_recor let extension = state .get_extension::() .unwrap(); - assert_eq!(extension.withheld_amount, pod::ElGamalCiphertext::zeroed()); + assert_eq!(extension.withheld_amount, PodElGamalCiphertext::zeroed()); } #[cfg(feature = "zk-ops")] @@ -1212,7 +1218,7 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_mint_with_proof_con &context_state_account.pubkey(), rent.minimum_balance(space), space as u64, - &zk_token_proof_program::id(), + &zk_elgamal_proof_program::id(), ), instruction_type.encode_verify_proof(Some(context_state_info), &proof_data), ]; @@ -1386,7 +1392,7 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_accounts_with_proof &context_state_account.pubkey(), rent.minimum_balance(space), space as u64, - &zk_token_proof_program::id(), + &zk_elgamal_proof_program::id(), ), instruction_type.encode_verify_proof(Some(context_state_info), &proof_data), ]; @@ -1451,7 +1457,7 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_accounts_with_proof let extension = state .get_extension::() .unwrap(); - assert_eq!(extension.withheld_amount, pod::ElGamalCiphertext::zeroed()); + assert_eq!(extension.withheld_amount, PodElGamalCiphertext::zeroed()); } #[cfg(feature = "zk-ops")] @@ -1590,7 +1596,7 @@ async fn confidential_transfer_harvest_withheld_tokens_to_mint() { let extension = state .get_extension::() .unwrap(); - assert_eq!(extension.withheld_amount, pod::ElGamalCiphertext::zeroed()); + assert_eq!(extension.withheld_amount, PodElGamalCiphertext::zeroed()); // calculate and encrypt fee to attach to the `WithdrawWithheldTokensFromMint` // instruction data diff --git a/token/program-2022-test/tests/initialize_mint.rs b/token/program-2022-test/tests/initialize_mint.rs index 898ecd4ea19..70cd4649e3a 100644 --- a/token/program-2022-test/tests/initialize_mint.rs +++ b/token/program-2022-test/tests/initialize_mint.rs @@ -22,7 +22,7 @@ use { ExtensionType, }, instruction, native_mint, - solana_zk_token_sdk::zk_token_elgamal::pod::ElGamalPubkey, + solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, state::Mint, }, spl_token_client::token::ExtensionInitializationParams, @@ -523,7 +523,7 @@ async fn fail_invalid_extensions_combination() { &spl_token_2022::id(), &mint_account.pubkey(), Some(Pubkey::new_unique()), - ElGamalPubkey::default(), + PodElGamalPubkey::default(), ) .unwrap(); diff --git a/token/program-2022-test/tests/program_test.rs b/token/program-2022-test/tests/program_test.rs index 25fd28abd79..b5d155f2331 100644 --- a/token/program-2022-test/tests/program_test.rs +++ b/token/program-2022-test/tests/program_test.rs @@ -13,7 +13,7 @@ use { }, id, native_mint, processor::Processor, - solana_zk_token_sdk::encryption::{auth_encryption::*, elgamal::*}, + solana_zk_sdk::encryption::{auth_encryption::*, elgamal::*}, }, spl_token_client::{ client::{ @@ -326,23 +326,23 @@ impl ConfidentialTokenAccountMeta { .unwrap(); assert_eq!( - extension - .pending_balance_lo - .decrypt(self.elgamal_keypair.secret()) + self.elgamal_keypair + .secret() + .decrypt_u32(&extension.pending_balance_lo.try_into().unwrap()) .unwrap(), expected.pending_balance_lo, ); assert_eq!( - extension - .pending_balance_hi - .decrypt(self.elgamal_keypair.secret()) + self.elgamal_keypair + .secret() + .decrypt_u32(&extension.pending_balance_hi.try_into().unwrap()) .unwrap(), expected.pending_balance_hi, ); assert_eq!( - extension - .available_balance - .decrypt(self.elgamal_keypair.secret()) + self.elgamal_keypair + .secret() + .decrypt_u32(&extension.available_balance.try_into().unwrap()) .unwrap(), expected.available_balance, ); diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index 226bcb5c4b0..f07425c988b 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -24,9 +24,11 @@ num-traits = "0.2" num_enum = "0.7.3" solana-program = "2.0.3" solana-security-txt = "1.1.1" -solana-zk-token-sdk = "2.0.3" +solana-zk-sdk = "2.0.3" 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" } +spl-token-confidential-transfer-proof-extraction = { version = "0.1.0", path = "../confidential-transfer/proof-extraction" } spl-token-group-interface = { version = "0.3.0", path = "../../token-group/interface" } spl-token-metadata-interface = { version = "0.4.0", path = "../../token-metadata/interface" } spl-transfer-hook-interface = { version = "0.7.0", path = "../transfer-hook/interface" } @@ -37,6 +39,9 @@ serde = { version = "1.0.207", optional = true } serde_with = { version = "3.9.0", optional = true } base64 = { version = "0.22.1", optional = true } +[target.'cfg(not(target_os = "solana"))'.dependencies] +spl-token-confidential-transfer-proof-generation = { version = "0.1.0", path = "../confidential-transfer/proof-generation"} + [dev-dependencies] lazy_static = "1.5.0" proptest = "1.5" diff --git a/token/program-2022/src/error.rs b/token/program-2022/src/error.rs index ff004f0ffb7..a886eb29e4e 100644 --- a/token/program-2022/src/error.rs +++ b/token/program-2022/src/error.rs @@ -1,5 +1,7 @@ //! Error types +#[cfg(not(target_os = "solana"))] +use spl_token_confidential_transfer_proof_generation::errors::TokenProofGenerationError; use { num_derive::FromPrimitive, solana_program::{ @@ -7,6 +9,7 @@ use { msg, program_error::{PrintProgramError, ProgramError}, }, + spl_token_confidential_transfer_proof_extraction::errors::TokenProofExtractionError, thiserror::Error, }; @@ -243,6 +246,18 @@ pub enum TokenError { /// Ciphertext arithmetic failed #[error("Ciphertext arithmetic failed")] CiphertextArithmeticFailed, + /// Pedersen commitments did not match + #[error("Pedersen commitment mismatch")] + PedersenCommitmentMismatch, + /// Range proof length did not match + #[error("Range proof length mismatch")] + RangeProofLengthMismatch, + /// Illegal transfer amount bit length + #[error("Illegal transfer amount bit length")] + IllegalBitLength, + /// Fee calculation failed + #[error("Fee calculation failed")] + FeeCalculation, } impl From for ProgramError { fn from(e: TokenError) -> Self { @@ -418,6 +433,49 @@ impl PrintProgramError for TokenError { TokenError::CiphertextArithmeticFailed => { msg!("Ciphertext arithmetic failed") } + TokenError::PedersenCommitmentMismatch => { + msg!("Pedersen commitments did not match") + } + TokenError::RangeProofLengthMismatch => { + msg!("Range proof lengths did not match") + } + TokenError::IllegalBitLength => { + msg!("Illegal transfer amount bit length") + } + TokenError::FeeCalculation => { + msg!("Transfer fee calculation failed") + } + } + } +} + +#[cfg(not(target_os = "solana"))] +impl From for TokenError { + fn from(e: TokenProofGenerationError) -> Self { + match e { + TokenProofGenerationError::ProofGeneration(_) => TokenError::ProofGeneration, + TokenProofGenerationError::NotEnoughFunds => TokenError::InsufficientFunds, + TokenProofGenerationError::IllegalAmountBitLength => TokenError::IllegalBitLength, + TokenProofGenerationError::FeeCalculation => TokenError::FeeCalculation, + } + } +} + +impl From for TokenError { + fn from(e: TokenProofExtractionError) -> Self { + match e { + TokenProofExtractionError::ElGamalPubkeyMismatch => { + TokenError::ConfidentialTransferElGamalPubkeyMismatch + } + TokenProofExtractionError::PedersenCommitmentMismatch => { + TokenError::PedersenCommitmentMismatch + } + TokenProofExtractionError::RangeProofLengthMismatch => { + TokenError::RangeProofLengthMismatch + } + TokenProofExtractionError::FeeParametersMismatch => TokenError::FeeParametersMismatch, + TokenProofExtractionError::CurveArithmetic => TokenError::CiphertextArithmeticFailed, + TokenProofExtractionError::CiphertextExtraction => TokenError::MalformedCiphertext, } } } 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 eebc84e9238..ed9a94e68dd 100644 --- a/token/program-2022/src/extension/confidential_transfer/account_info.rs +++ b/token/program-2022/src/extension/confidential_transfer/account_info.rs @@ -2,25 +2,24 @@ use { crate::{ error::TokenError, extension::confidential_transfer::{ - split_proof_generation::transfer_split_proof_data, ConfidentialTransferAccount, - DecryptableBalance, EncryptedBalance, PENDING_BALANCE_LO_BIT_LENGTH, + ConfidentialTransferAccount, DecryptableBalance, EncryptedBalance, + PENDING_BALANCE_LO_BIT_LENGTH, }, }, bytemuck::{Pod, Zeroable}, - solana_zk_token_sdk::{ + solana_zk_sdk::{ encryption::{ auth_encryption::{AeCiphertext, AeKey}, elgamal::{ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey}, }, - instruction::{ - transfer::{FeeParameters, TransferData, TransferWithFeeData}, - withdraw::WithdrawData, - zero_balance::ZeroBalanceProofData, - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, - CiphertextCommitmentEqualityProofData, - }, + zk_elgamal_proof_program::proof_data::ZeroCiphertextProofData, }, spl_pod::primitives::PodU64, + spl_token_confidential_transfer_proof_generation::{ + transfer::{transfer_split_proof_data, TransferProofData}, + transfer_with_fee::{transfer_with_fee_split_proof_data, TransferWithFeeProofData}, + withdraw::{withdraw_proof_data, WithdrawProofData}, + }, }; /// Confidential transfer extension information needed to construct an @@ -44,13 +43,13 @@ impl EmptyAccountAccountInfo { pub fn generate_proof_data( &self, elgamal_keypair: &ElGamalKeypair, - ) -> Result { + ) -> Result { let available_balance = self .available_balance .try_into() .map_err(|_| TokenError::MalformedCiphertext)?; - ZeroBalanceProofData::new(elgamal_keypair, &available_balance) + ZeroCiphertextProofData::new(elgamal_keypair, &available_balance) .map_err(|_| TokenError::ProofGeneration) } } @@ -179,20 +178,20 @@ impl WithdrawAccountInfo { withdraw_amount: u64, elgamal_keypair: &ElGamalKeypair, aes_key: &AeKey, - ) -> Result { + ) -> Result { let current_available_balance = self .available_balance .try_into() .map_err(|_| TokenError::MalformedCiphertext)?; let current_decrypted_available_balance = self.decrypted_available_balance(aes_key)?; - WithdrawData::new( + withdraw_proof_data( + ¤t_available_balance, + current_decrypted_available_balance, withdraw_amount, elgamal_keypair, - current_decrypted_available_balance, - ¤t_available_balance, ) - .map_err(|_| TokenError::ProofGeneration) + .map_err(|e| -> TokenError { e.into() }) } /// Update the decryptable available balance. @@ -240,37 +239,6 @@ impl TransferAccountInfo { .ok_or(TokenError::AccountDecryption) } - /// Create a transfer proof data. - pub fn generate_transfer_proof_data( - &self, - transfer_amount: u64, - elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - ) -> Result { - let current_source_available_balance = self - .available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - let current_source_decrypted_available_balance = - self.decrypted_available_balance(aes_key)?; - - let default_auditor_pubkey = ElGamalPubkey::default(); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey); - - TransferData::new( - transfer_amount, - ( - current_source_decrypted_available_balance, - ¤t_source_available_balance, - ), - elgamal_keypair, - (destination_elgamal_pubkey, auditor_elgamal_pubkey), - ) - .map_err(|_| TokenError::ProofGeneration) - } - /// Create a transfer proof data that is split into equality, ciphertext /// validity, and range proofs. pub fn generate_split_transfer_proof_data( @@ -280,14 +248,7 @@ impl TransferAccountInfo { aes_key: &AeKey, destination_elgamal_pubkey: &ElGamalPubkey, auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - ) -> Result< - ( - CiphertextCommitmentEqualityProofData, - BatchedGroupedCiphertext3HandlesValidityProofData, - BatchedRangeProofU128Data, - ), - TokenError, - > { + ) -> Result { let current_available_balance = self .available_balance .try_into() @@ -306,48 +267,46 @@ impl TransferAccountInfo { destination_elgamal_pubkey, auditor_elgamal_pubkey, ) + .map_err(|e| -> TokenError { e.into() }) } - /// Create a transfer with fee proof data + /// Create a transfer proof data that is split into equality, ciphertext + /// validity (transfer amount), percentage-with-cap, ciphertext validity + /// (fee), and range proofs. #[allow(clippy::too_many_arguments)] - pub fn generate_transfer_with_fee_proof_data( + pub fn generate_split_transfer_with_fee_proof_data( &self, transfer_amount: u64, - elgamal_keypair: &ElGamalKeypair, + source_elgamal_keypair: &ElGamalKeypair, aes_key: &AeKey, destination_elgamal_pubkey: &ElGamalPubkey, auditor_elgamal_pubkey: Option<&ElGamalPubkey>, withdraw_withheld_authority_elgamal_pubkey: &ElGamalPubkey, fee_rate_basis_points: u16, maximum_fee: u64, - ) -> Result { - let current_source_available_balance = self + ) -> Result { + let current_available_balance = self .available_balance .try_into() .map_err(|_| TokenError::MalformedCiphertext)?; - let current_source_decrypted_available_balance = - self.decrypted_available_balance(aes_key)?; - - let default_auditor_pubkey = ElGamalPubkey::default(); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey); - - let fee_parameters = FeeParameters { - fee_rate_basis_points, - maximum_fee, - }; + let current_decryptable_available_balance = self + .decryptable_available_balance + .try_into() + .map_err(|_| TokenError::MalformedCiphertext)?; - TransferWithFeeData::new( + transfer_with_fee_split_proof_data( + ¤t_available_balance, + ¤t_decryptable_available_balance, transfer_amount, - ( - current_source_decrypted_available_balance, - ¤t_source_available_balance, - ), - elgamal_keypair, - (destination_elgamal_pubkey, auditor_elgamal_pubkey), - fee_parameters, + source_elgamal_keypair, + aes_key, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, withdraw_withheld_authority_elgamal_pubkey, + fee_rate_basis_points, + maximum_fee, ) - .map_err(|_| TokenError::ProofGeneration) + .map_err(|e| -> TokenError { e.into() }) } /// Update the decryptable available balance. diff --git a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs deleted file mode 100644 index d4bfd4ebb9d..00000000000 --- a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs +++ /dev/null @@ -1,680 +0,0 @@ -//! Ciphertext extraction and proof related helper logic -//! -//! This submodule should be removed with the next upgrade to the Solana program - -use crate::{ - extension::{ - confidential_transfer::*, confidential_transfer_fee::EncryptedFee, - transfer_fee::TransferFee, - }, - solana_program::program_error::ProgramError, - solana_zk_token_sdk::{ - curve25519::{ - ristretto::{self, PodRistrettoPoint}, - scalar::PodScalar, - }, - instruction::{ - transfer::{TransferProofContext, TransferWithFeeProofContext}, - BatchedGroupedCiphertext2HandlesValidityProofContext, - BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext, - CiphertextCommitmentEqualityProofContext, FeeSigmaProofContext, - }, - zk_token_elgamal::pod::{ - DecryptHandle, FeeEncryption, GroupedElGamalCiphertext2Handles, - GroupedElGamalCiphertext3Handles, PedersenCommitment, TransferAmountCiphertext, - }, - }, -}; -#[cfg(feature = "serde-traits")] -use { - crate::serialization::decrypthandle_fromstr, - serde::{Deserialize, Serialize}, -}; - -/// Extract the commitment component from a grouped ciphertext with 2 handles. -/// -/// A grouped ciphertext with 2 handles consists of the following 32-bytes -/// components that are serialized in order: -/// 1. The `commitment` component that encodes the fee amount. -/// 2. The `decryption handle` component with respect to the destination -/// public key. -/// 3. The `decryption handle` component with respect to the withdraw withheld -/// authority public key. -/// -/// The fee commitment component consists of the first 32-byte. -pub(crate) fn extract_commitment_from_grouped_ciphertext_2_handles( - transfer_amount_ciphertext: &GroupedElGamalCiphertext2Handles, -) -> PedersenCommitment { - let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); - let transfer_amount_commitment_bytes = - transfer_amount_ciphertext_bytes[..32].try_into().unwrap(); - PedersenCommitment(transfer_amount_commitment_bytes) -} - -/// Extract the commitment component from a grouped ciphertext with 3 handles. -/// -/// A grouped ciphertext with 3 handles consists of the following 32-bytes -/// components that are serialized in order: -/// 1. The `commitment` component that encodes the fee amount. -/// 2. The `source handle` component with respect to the source public key. -/// 3. The `decryption handle` component with respect to the destination -/// public key. -/// 4. The `decryption handle` component with respect to the withdraw withheld -/// authority public key. -/// -/// The fee commitment component consists of the first 32-byte. -pub(crate) fn extract_commitment_from_grouped_ciphertext_3_handles( - transfer_amount_ciphertext: &GroupedElGamalCiphertext3Handles, -) -> PedersenCommitment { - let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); - let transfer_amount_commitment_bytes = - transfer_amount_ciphertext_bytes[..32].try_into().unwrap(); - PedersenCommitment(transfer_amount_commitment_bytes) -} - -/// Extract the transfer amount ciphertext encrypted under the source ElGamal -/// public key. -/// -/// A transfer amount ciphertext consists of the following 32-byte components -/// that are serialized in order: -/// 1. The `commitment` component that encodes the transfer amount. -/// 2. The `decryption handle` component with respect to the source public -/// key. -/// 3. The `decryption handle` component with respect to the destination -/// public key. -/// 4. The `decryption handle` component with respect to the auditor public -/// key. -/// -/// An ElGamal ciphertext for the source consists of the `commitment` component -/// and the `decryption handle` component with respect to the source. -pub fn transfer_amount_source_ciphertext( - transfer_amount_ciphertext: &TransferAmountCiphertext, -) -> ElGamalCiphertext { - let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); - - let mut source_ciphertext_bytes = [0u8; 64]; - source_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); - source_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[32..64]); - - ElGamalCiphertext(source_ciphertext_bytes) -} - -/// Extract the transfer amount ciphertext encrypted under the destination -/// ElGamal public key. -/// -/// A transfer amount ciphertext consists of the following 32-byte components -/// that are serialized in order: -/// 1. The `commitment` component that encodes the transfer amount. -/// 2. The `decryption handle` component with respect to the source public -/// key. -/// 3. The `decryption handle` component with respect to the destination -/// public key. -/// 4. The `decryption handle` component with respect to the auditor public -/// key. -/// -/// An ElGamal ciphertext for the destination consists of the `commitment` -/// component and the `decryption handle` component with respect to the -/// destination public key. -#[cfg(feature = "zk-ops")] -pub(crate) fn transfer_amount_destination_ciphertext( - transfer_amount_ciphertext: &TransferAmountCiphertext, -) -> ElGamalCiphertext { - let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); - - let mut destination_ciphertext_bytes = [0u8; 64]; - destination_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); - destination_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[64..96]); - - ElGamalCiphertext(destination_ciphertext_bytes) -} - -/// Extract the fee amount ciphertext encrypted under the destination ElGamal -/// public key. -/// -/// A fee encryption amount consists of the following 32-byte components that -/// are serialized in order: -/// 1. The `commitment` component that encodes the fee amount. -/// 2. The `decryption handle` component with respect to the destination -/// public key. -/// 3. The `decryption handle` component with respect to the withdraw withheld -/// authority public key. -/// -/// An ElGamal ciphertext for the destination consists of the `commitment` -/// component and the `decryption handle` component with respect to the -/// destination public key. -#[cfg(feature = "zk-ops")] -pub(crate) fn fee_amount_destination_ciphertext( - transfer_amount_ciphertext: &EncryptedFee, -) -> ElGamalCiphertext { - let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); - - let mut source_ciphertext_bytes = [0u8; 64]; - source_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); - source_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[32..64]); - - ElGamalCiphertext(source_ciphertext_bytes) -} - -/// Extract the transfer amount ciphertext encrypted under the withdraw withheld -/// authority ElGamal public key. -/// -/// A fee encryption amount consists of the following 32-byte components that -/// are serialized in order: -/// 1. The `commitment` component that encodes the fee amount. -/// 2. The `decryption handle` component with respect to the destination -/// public key. -/// 3. The `decryption handle` component with respect to the withdraw withheld -/// authority public key. -/// -/// An ElGamal ciphertext for the destination consists of the `commitment` -/// component and the `decryption handle` component with respect to the withdraw -/// withheld authority public key. -#[cfg(feature = "zk-ops")] -pub(crate) fn fee_amount_withdraw_withheld_authority_ciphertext( - transfer_amount_ciphertext: &EncryptedFee, -) -> ElGamalCiphertext { - let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); - - let mut destination_ciphertext_bytes = [0u8; 64]; - destination_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); - destination_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[64..96]); - - ElGamalCiphertext(destination_ciphertext_bytes) -} - -/// The transfer public keys associated with a transfer. -#[cfg(feature = "zk-ops")] -pub struct TransferPubkeysInfo { - /// Source ElGamal public key - pub source: ElGamalPubkey, - /// Destination ElGamal public key - pub destination: ElGamalPubkey, - /// Auditor ElGamal public key - pub auditor: ElGamalPubkey, -} - -/// The proof context information needed to process a [Transfer] instruction. -#[cfg(feature = "zk-ops")] -pub struct TransferProofContextInfo { - /// Ciphertext containing the low 16 bits of the transafer amount - pub ciphertext_lo: TransferAmountCiphertext, - /// Ciphertext containing the high 32 bits of the transafer amount - pub ciphertext_hi: TransferAmountCiphertext, - /// The transfer public keys associated with a transfer - pub transfer_pubkeys: TransferPubkeysInfo, - /// The new source available balance ciphertext - pub new_source_ciphertext: ElGamalCiphertext, -} - -#[cfg(feature = "zk-ops")] -impl From for TransferProofContextInfo { - fn from(context: TransferProofContext) -> Self { - let transfer_pubkeys = TransferPubkeysInfo { - source: context.transfer_pubkeys.source, - destination: context.transfer_pubkeys.destination, - auditor: context.transfer_pubkeys.auditor, - }; - - TransferProofContextInfo { - ciphertext_lo: context.ciphertext_lo, - ciphertext_hi: context.ciphertext_hi, - transfer_pubkeys, - new_source_ciphertext: context.new_source_ciphertext, - } - } -} - -#[cfg(feature = "zk-ops")] -impl TransferProofContextInfo { - /// Create a transfer proof context information needed to process a - /// [Transfer] instruction from split proof contexts after verifying - /// their consistency. - pub fn verify_and_extract( - equality_proof_context: &CiphertextCommitmentEqualityProofContext, - ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, - range_proof_context: &BatchedRangeProofContext, - ) -> Result { - // The equality proof context consists of the source ElGamal public key, the new - // source available balance ciphertext, and the new source available - // commitment. The public key and ciphertext should be returned as parts - // of `TransferProofContextInfo` and the commitment should be checked - // with range proof for consistency. - let CiphertextCommitmentEqualityProofContext { - pubkey: source_pubkey_from_equality_proof, - ciphertext: new_source_ciphertext, - commitment: new_source_commitment, - } = equality_proof_context; - - // The ciphertext validity proof context consists of the source ElGamal public - // key, destination ElGamal - // public key, auditor ElGamal public key, and the transfer amount - // ciphertexts. All of these fields should be returned as part of - // `TransferProofContextInfo`. In addition, the commitments pertaining - // to the transfer amount ciphertexts should be checked with range proof for - // consistency. - let BatchedGroupedCiphertext3HandlesValidityProofContext { - source_pubkey: source_pubkey_from_ciphertext_validity_proof, - destination_pubkey, - auditor_pubkey, - grouped_ciphertext_lo: transfer_amount_ciphertext_lo, - grouped_ciphertext_hi: transfer_amount_ciphertext_hi, - } = ciphertext_validity_proof_context; - - if source_pubkey_from_equality_proof != source_pubkey_from_ciphertext_validity_proof { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - // The range proof context consists of the Pedersen commitments and bit-lengths - // for which the range proof is proved. The commitments must consist of - // three commitments pertaining to the new source available balance, the - // low bits of the transfer amount, and high bits of the transfer - // amount. These commitments must be checked for bit lengths `64`, `16`, - // and `32`. - let BatchedRangeProofContext { - commitments: range_proof_commitments, - bit_lengths: range_proof_bit_lengths, - } = range_proof_context; - - // check that the range proof was created for the correct set of Pedersen - // commitments - let transfer_amount_commitment_lo = - extract_commitment_from_grouped_ciphertext_3_handles(transfer_amount_ciphertext_lo); - let transfer_amount_commitment_hi = - extract_commitment_from_grouped_ciphertext_3_handles(transfer_amount_ciphertext_hi); - - let expected_commitments = [ - *new_source_commitment, - transfer_amount_commitment_lo, - transfer_amount_commitment_hi, - // the fourth dummy commitment can be any commitment - ]; - - if !range_proof_commitments - .iter() - .zip(expected_commitments.iter()) - .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) - { - return Err(ProgramError::InvalidInstructionData); - } - - // check that the range proof was created for the correct number of bits - const REMAINING_BALANCE_BIT_LENGTH: u8 = 64; - const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16; - const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32; - const PADDING_BIT_LENGTH: u8 = 16; - let expected_bit_lengths = [ - REMAINING_BALANCE_BIT_LENGTH, - TRANSFER_AMOUNT_LO_BIT_LENGTH, - TRANSFER_AMOUNT_HI_BIT_LENGTH, - PADDING_BIT_LENGTH, - ] - .iter(); - - if !range_proof_bit_lengths - .iter() - .zip(expected_bit_lengths) - .all(|(proof_len, expected_len)| proof_len == expected_len) - { - return Err(ProgramError::InvalidInstructionData); - } - - let transfer_pubkeys = TransferPubkeysInfo { - source: *source_pubkey_from_equality_proof, - destination: *destination_pubkey, - auditor: *auditor_pubkey, - }; - - Ok(Self { - ciphertext_lo: TransferAmountCiphertext(*transfer_amount_ciphertext_lo), - ciphertext_hi: TransferAmountCiphertext(*transfer_amount_ciphertext_hi), - transfer_pubkeys, - new_source_ciphertext: *new_source_ciphertext, - }) - } -} - -/// The transfer public keys associated with a transfer with fee. -#[cfg(feature = "zk-ops")] -pub struct TransferWithFeePubkeysInfo { - /// Source ElGamal public key - pub source: ElGamalPubkey, - /// Destination ElGamal public key - pub destination: ElGamalPubkey, - /// Auditor ElGamal public key - pub auditor: ElGamalPubkey, - /// Withdraw withheld authority public key - pub withdraw_withheld_authority: ElGamalPubkey, -} - -/// The proof context information needed to process a [Transfer] instruction -/// with fee. -#[cfg(feature = "zk-ops")] -pub struct TransferWithFeeProofContextInfo { - /// Group encryption of the low 16 bits of the transfer amount - pub ciphertext_lo: TransferAmountCiphertext, - /// Group encryption of the high 48 bits of the transfer amount - pub ciphertext_hi: TransferAmountCiphertext, - /// The public encryption keys associated with the transfer: source, dest, - /// auditor, and withdraw withheld authority - pub transfer_with_fee_pubkeys: TransferWithFeePubkeysInfo, - /// The final spendable ciphertext after the transfer, - pub new_source_ciphertext: ElGamalCiphertext, - /// The transfer fee encryption of the low 16 bits of the transfer fee - /// amount - pub fee_ciphertext_lo: EncryptedFee, - /// The transfer fee encryption of the hi 32 bits of the transfer fee amount - pub fee_ciphertext_hi: EncryptedFee, -} - -#[cfg(feature = "zk-ops")] -impl From for TransferWithFeeProofContextInfo { - fn from(context: TransferWithFeeProofContext) -> Self { - let transfer_with_fee_pubkeys = TransferWithFeePubkeysInfo { - source: context.transfer_with_fee_pubkeys.source, - destination: context.transfer_with_fee_pubkeys.destination, - auditor: context.transfer_with_fee_pubkeys.auditor, - withdraw_withheld_authority: context - .transfer_with_fee_pubkeys - .withdraw_withheld_authority, - }; - - TransferWithFeeProofContextInfo { - ciphertext_lo: context.ciphertext_lo, - ciphertext_hi: context.ciphertext_hi, - transfer_with_fee_pubkeys, - new_source_ciphertext: context.new_source_ciphertext, - fee_ciphertext_lo: context.fee_ciphertext_lo, - fee_ciphertext_hi: context.fee_ciphertext_hi, - } - } -} - -#[cfg(feature = "zk-ops")] -impl TransferWithFeeProofContextInfo { - /// Create a transfer proof context information needed to process a - /// [Transfer] instruction from split proof contexts after verifying - /// their consistency. - pub fn verify_and_extract( - equality_proof_context: &CiphertextCommitmentEqualityProofContext, - transfer_amount_ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, - fee_sigma_proof_context: &FeeSigmaProofContext, - fee_ciphertext_validity_proof_context: &BatchedGroupedCiphertext2HandlesValidityProofContext, - range_proof_context: &BatchedRangeProofContext, - fee_parameters: &TransferFee, - ) -> Result { - // The equality proof context consists of the source ElGamal public key, the new - // source available balance ciphertext, and the new source available - // commitment. The public key and ciphertext should be returned as part - // of `TransferWithFeeProofContextInfo` and the commitment should be - // checked with range proof for consistency. - let CiphertextCommitmentEqualityProofContext { - pubkey: source_pubkey_from_equality_proof, - ciphertext: new_source_ciphertext, - commitment: new_source_commitment, - } = equality_proof_context; - - // The transfer amount ciphertext validity proof context consists of the - // destination ElGamal public key, auditor ElGamal public key, and the - // transfer amount ciphertexts. All of these fields should be returned - // as part of `TransferWithFeeProofContextInfo`. In addition, the - // commitments pertaining to the transfer amount ciphertexts should be - // checked with range proof for consistency. - let BatchedGroupedCiphertext3HandlesValidityProofContext { - source_pubkey: source_pubkey_from_ciphertext_validity_proof, - destination_pubkey, - auditor_pubkey, - grouped_ciphertext_lo: transfer_amount_ciphertext_lo, - grouped_ciphertext_hi: transfer_amount_ciphertext_hi, - } = transfer_amount_ciphertext_validity_proof_context; - - if source_pubkey_from_equality_proof != source_pubkey_from_ciphertext_validity_proof { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - // The fee sigma proof context consists of the fee commitment, delta commitment, - // claimed commitment, and max fee. The fee and claimed commitment - // should be checked with range proof for consistency. The delta - // commitment should be checked whether it is properly generated with - // respect to the fee parameters. The max fee should be checked for - // consistency with the fee parameters. - let FeeSigmaProofContext { - fee_commitment, - delta_commitment, - claimed_commitment, - max_fee, - } = fee_sigma_proof_context; - - let expected_maximum_fee: u64 = fee_parameters.maximum_fee.into(); - let proof_maximum_fee: u64 = (*max_fee).into(); - if expected_maximum_fee != proof_maximum_fee { - return Err(ProgramError::InvalidInstructionData); - } - - // The transfer fee ciphertext validity proof context consists of the - // destination ElGamal public key, withdraw withheld authority ElGamal - // public key, and the transfer fee ciphertexts. The rest of the fields - // should be return as part of `TransferWithFeeProofContextInfo`. In - // addition, the destination public key should be checked for - // consistency with the destination public key contained in the transfer amount - // ciphertext validity proof, and the commitments pertaining to the transfer fee - // amount ciphertexts should be checked with range proof for - // consistency. - let BatchedGroupedCiphertext2HandlesValidityProofContext { - destination_pubkey: destination_pubkey_from_transfer_fee_validity_proof, - auditor_pubkey: withdraw_withheld_authority_pubkey, - grouped_ciphertext_lo: fee_ciphertext_lo, - grouped_ciphertext_hi: fee_ciphertext_hi, - } = fee_ciphertext_validity_proof_context; - - if destination_pubkey != destination_pubkey_from_transfer_fee_validity_proof { - return Err(ProgramError::InvalidInstructionData); - } - - // The range proof context consists of the Pedersen commitments and bit-lengths - // for which the range proof is proved. The commitments must consist of - // seven commitments pertaining to - // - the new source available balance (64 bits) - // - the low bits of the transfer amount (16 bits) - // - the high bits of the transfer amount (32 bits) - // - the delta amount for the fee (48 bits) - // - the complement of the delta amount for the fee (48 bits) - // - the low bits of the fee amount (16 bits) - // - the high bits of the fee amount (32 bits) - let BatchedRangeProofContext { - commitments: range_proof_commitments, - bit_lengths: range_proof_bit_lengths, - } = range_proof_context; - - // check that the range proof was created for the correct set of Pedersen - // commitments - let transfer_amount_commitment_lo = - extract_commitment_from_grouped_ciphertext_3_handles(transfer_amount_ciphertext_lo); - let transfer_amount_commitment_hi = - extract_commitment_from_grouped_ciphertext_3_handles(transfer_amount_ciphertext_hi); - - let fee_commitment_lo = - extract_commitment_from_grouped_ciphertext_2_handles(fee_ciphertext_lo); - let fee_commitment_hi = - extract_commitment_from_grouped_ciphertext_2_handles(fee_ciphertext_hi); - - const MAX_FEE_BASIS_POINTS: u64 = 10_000; - let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS); - let max_fee_basis_points_commitment = - ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &G) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - let claimed_complement_commitment = ristretto::subtract_ristretto( - &max_fee_basis_points_commitment, - &(*claimed_commitment).into(), - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - let expected_commitments = [ - *new_source_commitment, - transfer_amount_commitment_lo, - transfer_amount_commitment_hi, - *claimed_commitment, - claimed_complement_commitment.into(), - fee_commitment_lo, - fee_commitment_hi, - ]; - - if !range_proof_commitments - .iter() - .zip(expected_commitments.iter()) - .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) - { - return Err(ProgramError::InvalidInstructionData); - } - - // check that the range proof was created for the correct number of bits - const REMAINING_BALANCE_BIT_LENGTH: u8 = 64; - const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16; - const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32; - const DELTA_BIT_LENGTH: u8 = 48; - const FEE_AMOUNT_LO_BIT_LENGTH: u8 = 16; - const FEE_AMOUNT_HI_BIT_LENGTH: u8 = 32; - - let expected_bit_lengths = [ - REMAINING_BALANCE_BIT_LENGTH, - TRANSFER_AMOUNT_LO_BIT_LENGTH, - TRANSFER_AMOUNT_HI_BIT_LENGTH, - DELTA_BIT_LENGTH, - DELTA_BIT_LENGTH, - FEE_AMOUNT_LO_BIT_LENGTH, - FEE_AMOUNT_HI_BIT_LENGTH, - ] - .iter(); - - if !range_proof_bit_lengths - .iter() - .zip(expected_bit_lengths) - .all(|(proof_len, expected_len)| proof_len == expected_len) - { - return Err(ProgramError::InvalidInstructionData); - } - - // check consistency between fee sigma and fee ciphertext validity proofs - let sigma_proof_fee_commitment_point: PodRistrettoPoint = (*fee_commitment).into(); - let validity_proof_fee_point = - combine_lo_hi_pedersen_points(&fee_commitment_lo.into(), &fee_commitment_hi.into()) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - if validity_proof_fee_point != sigma_proof_fee_commitment_point { - return Err(ProgramError::InvalidInstructionData); - } - - verify_delta_commitment( - &transfer_amount_commitment_lo, - &transfer_amount_commitment_hi, - fee_commitment, - delta_commitment, - fee_parameters.transfer_fee_basis_points.into(), - )?; - - // create transfer with fee proof context info and return - let transfer_with_fee_pubkeys = TransferWithFeePubkeysInfo { - source: *source_pubkey_from_equality_proof, - destination: *destination_pubkey, - auditor: *auditor_pubkey, - withdraw_withheld_authority: *withdraw_withheld_authority_pubkey, - }; - - Ok(Self { - ciphertext_lo: TransferAmountCiphertext(*transfer_amount_ciphertext_lo), - ciphertext_hi: TransferAmountCiphertext(*transfer_amount_ciphertext_hi), - transfer_with_fee_pubkeys, - new_source_ciphertext: *new_source_ciphertext, - fee_ciphertext_lo: FeeEncryption(*fee_ciphertext_lo), - fee_ciphertext_hi: FeeEncryption(*fee_ciphertext_hi), - }) - } -} - -/// The ElGamal ciphertext decryption handle pertaining to the low and high bits -/// of the transfer amount under the source public key of the transfer. -/// -/// The `TransferProofContext` contains decryption handles for the low and high -/// bits of the transfer amount. However, these decryption handles were -/// (mistakenly) removed from the split proof contexts as a form of -/// optimization. These components should be added back into these split proofs -/// in `zk-token-sdk`. Until this modifications is made, include -/// `SourceDecryptHandle` in the transfer instruction data. -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct SourceDecryptHandles { - /// The ElGamal decryption handle pertaining to the low 16 bits of the - /// transfer amount. - #[cfg_attr(feature = "serde-traits", serde(with = "decrypthandle_fromstr"))] - pub lo: DecryptHandle, - /// The ElGamal decryption handle pertaining to the low 32 bits of the - /// transfer amount. - #[cfg_attr(feature = "serde-traits", serde(with = "decrypthandle_fromstr"))] - pub hi: DecryptHandle, -} - -/// Ristretto generator point for curve25519 -const G: PodRistrettoPoint = PodRistrettoPoint([ - 226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165, - 130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118, -]); - -/// Convert a `u16` amount into a curve25519 scalar -fn u16_to_scalar(amount: u16) -> PodScalar { - let mut bytes = [0u8; 32]; - bytes[..2].copy_from_slice(&amount.to_le_bytes()); - PodScalar(bytes) -} - -/// Convert a `u64` amount into a curve25519 scalar -fn u64_to_scalar(amount: u64) -> PodScalar { - let mut bytes = [0u8; 32]; - bytes[..8].copy_from_slice(&amount.to_le_bytes()); - PodScalar(bytes) -} - -/// Combine lo and hi Pedersen commitment points -fn combine_lo_hi_pedersen_points( - point_lo: &PodRistrettoPoint, - point_hi: &PodRistrettoPoint, -) -> Option { - const SCALING_CONSTANT: u64 = 65536; - let scaling_constant_scalar = u64_to_scalar(SCALING_CONSTANT); - let scaled_point_hi = ristretto::multiply_ristretto(&scaling_constant_scalar, point_hi)?; - ristretto::add_ristretto(point_lo, &scaled_point_hi) -} - -/// Compute fee delta commitment -fn verify_delta_commitment( - transfer_amount_commitment_lo: &PedersenCommitment, - transfer_amount_commitment_hi: &PedersenCommitment, - fee_commitment: &PedersenCommitment, - proof_delta_commitment: &PedersenCommitment, - transfer_fee_basis_points: u16, -) -> Result<(), ProgramError> { - let transfer_amount_point = combine_lo_hi_pedersen_points( - &(*transfer_amount_commitment_lo).into(), - &(*transfer_amount_commitment_hi).into(), - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - let transfer_fee_basis_points_scalar = u16_to_scalar(transfer_fee_basis_points); - let scaled_transfer_amount_point = - ristretto::multiply_ristretto(&transfer_fee_basis_points_scalar, &transfer_amount_point) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - const MAX_FEE_BASIS_POINTS: u64 = 10_000; - let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS); - let fee_point: PodRistrettoPoint = (*fee_commitment).into(); - let scaled_fee_point = ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &fee_point) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - let expected_delta_commitment_point = - ristretto::subtract_ristretto(&scaled_fee_point, &scaled_transfer_amount_point) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - let proof_delta_commitment_point = (*proof_delta_commitment).into(); - if expected_delta_commitment_point != proof_delta_commitment_point { - return Err(ProgramError::InvalidInstructionData); - } - Ok(()) -} diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index f1807cfaeec..8a3aff7ba0d 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -1,7 +1,5 @@ -#[cfg(not(target_os = "solana"))] -use solana_zk_token_sdk::encryption::auth_encryption::AeCiphertext; -pub use solana_zk_token_sdk::{ - zk_token_proof_instruction::*, zk_token_proof_state::ProofContextState, +pub use solana_zk_sdk::zk_elgamal_proof_program::{ + instruction::ProofInstruction, proof_data::*, state::ProofContextState, }; #[cfg(feature = "serde-traits")] use { @@ -206,36 +204,44 @@ pub enum ConfidentialTransferInstruction { /// Withdraw SPL Tokens from the available balance of a confidential token /// account. /// + /// In order for this instruction to be successfully processed, it must be + /// accompanied by the following list of `zk_elgamal_proof` program + /// instructions: + /// - `VerifyCiphertextCommitmentEquality` + /// - `VerifyBatchedRangeProofU64` + /// These instructions can be accompanied in the same transaction or can be + /// pre-verified into a context state account, in which case, only their + /// context state account address need to be provided. + /// /// Fails if the source or destination accounts are frozen. /// Fails if the associated mint is extended as `NonTransferable`. /// - /// In order for this instruction to be successfully processed, it must be - /// accompanied by the `VerifyWithdraw` instruction of the - /// `zk_token_proof` program in the same transaction or the address of a - /// context state account for the proof must be provided. - /// /// Accounts expected by this instruction: /// /// * Single owner/delegate /// 0. `[writable]` The SPL Token account. /// 1. `[]` The token mint. - /// 2. `[]` Instructions sysvar if `VerifyWithdraw` is included in the - /// same transaction or context state account if `VerifyWithdraw` is - /// pre-verified into a context state account. - /// 3. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 4. `[signer]` The single source account owner. + /// 2. `[]` (Optional) Instructions sysvar if at least one of the + /// `zk_elgamal_proof` instructions are included in the same + /// transaction. + /// 3. `[]` (Optional) Equality proof record account or context state + /// account. + /// 4. `[]` (Optional) Range proof record account or context state + /// account. + /// 5. `[signer]` The single source account owner. /// /// * Multisignature owner/delegate /// 0. `[writable]` The SPL Token account. /// 1. `[]` The token mint. - /// 2. `[]` Instructions sysvar if `VerifyWithdraw` is included in the - /// same transaction or context state account if `VerifyWithdraw` is - /// pre-verified into a context state account. - /// 3. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 4. `[]` The multisig source account owner. - /// 5.. `[signer]` Required M signer accounts for the SPL Token Multisig + /// 2. `[]` (Optional) Instructions sysvar if at least one of the + /// `zk_elgamal_proof` instructions are included in the same + /// transaction. + /// 3. `[]` (Optional) Equality proof record account or context state + /// account. + /// 4. `[]` (Optional) Range proof record account or context state + /// account. + /// 5. `[]` The multisig source account owner. + /// 6.. `[signer]` Required M signer accounts for the SPL Token Multisig /// account. /// /// Data expected by this instruction: @@ -549,10 +555,15 @@ pub struct WithdrawInstructionData { /// The new decryptable balance if the withdrawal succeeds #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] pub new_decryptable_available_balance: DecryptableBalance, - /// Relative location of the `ProofInstruction::VerifyWithdraw` instruction + /// Relative location of the + /// `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction /// to the `Withdraw` instruction in the transaction. If the offset is /// `0`, then use a context state account for the proof. - pub proof_instruction_offset: i8, + pub equality_proof_instruction_offset: i8, + /// Relative location of the `ProofInstruction::BatchedRangeProofU64` + /// instruction to the `Withdraw` instruction in the transaction. If the + /// offset is `0`, then use a context state account for the proof. + pub range_proof_instruction_offset: i8, } /// Data expected by `ConfidentialTransferInstruction::Transfer` @@ -638,7 +649,7 @@ pub fn initialize_mint( mint: &Pubkey, authority: Option, auto_approve_new_accounts: bool, - auditor_elgamal_pubkey: Option, + auditor_elgamal_pubkey: Option, ) -> Result { check_program_account(token_program_id)?; let accounts = vec![AccountMeta::new(*mint, false)]; @@ -663,7 +674,7 @@ pub fn update_mint( authority: &Pubkey, multisig_signers: &[&Pubkey], auto_approve_new_accounts: bool, - auditor_elgamal_pubkey: Option, + auditor_elgamal_pubkey: Option, ) -> Result { check_program_account(token_program_id)?; let mut accounts = vec![ @@ -693,11 +704,11 @@ pub fn inner_configure_account( token_program_id: &Pubkey, token_account: &Pubkey, mint: &Pubkey, - decryptable_zero_balance: AeCiphertext, + decryptable_zero_balance: PodAeCiphertext, maximum_pending_balance_credit_counter: u64, authority: &Pubkey, multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, + proof_data_location: ProofLocation, ) -> Result { check_program_account(token_program_id)?; @@ -735,7 +746,7 @@ pub fn inner_configure_account( TokenInstruction::ConfidentialTransferExtension, ConfidentialTransferInstruction::ConfigureAccount, &ConfigureAccountInstructionData { - decryptable_zero_balance: decryptable_zero_balance.into(), + decryptable_zero_balance, maximum_pending_balance_credit_counter: maximum_pending_balance_credit_counter.into(), proof_instruction_offset, }, @@ -748,11 +759,11 @@ pub fn configure_account( token_program_id: &Pubkey, token_account: &Pubkey, mint: &Pubkey, - decryptable_zero_balance: AeCiphertext, + decryptable_zero_balance: PodAeCiphertext, maximum_pending_balance_credit_counter: u64, authority: &Pubkey, multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, + proof_data_location: ProofLocation, ) -> Result, ProgramError> { let mut instructions = vec![inner_configure_account( token_program_id, @@ -777,9 +788,8 @@ pub fn configure_account( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::InstructionData(data) => { - instructions.push(verify_pubkey_validity(None, 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), @@ -824,7 +834,7 @@ pub fn inner_empty_account( token_account: &Pubkey, authority: &Pubkey, multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, + proof_data_location: ProofLocation, ) -> Result { check_program_account(token_program_id)?; let mut accounts = vec![AccountMeta::new(*token_account, false)]; @@ -869,7 +879,7 @@ pub fn empty_account( token_account: &Pubkey, authority: &Pubkey, multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, + proof_data_location: ProofLocation, ) -> Result, ProgramError> { let mut instructions = vec![inner_empty_account( token_program_id, @@ -891,9 +901,10 @@ pub fn empty_account( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::InstructionData(data) => instructions.push(verify_zero_balance(None, data)), + ProofData::InstructionData(data) => instructions + .push(ProofInstruction::VerifyZeroCiphertext.encode_verify_proof(None, data)), ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyZeroBalance + ProofInstruction::VerifyZeroCiphertext .encode_verify_proof_from_account(None, address, offset), ), }; @@ -949,7 +960,8 @@ pub fn inner_withdraw( new_decryptable_available_balance: DecryptableBalance, authority: &Pubkey, multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, + equality_proof_data_location: ProofLocation, + range_proof_data_location: ProofLocation, ) -> Result { check_program_account(token_program_id)?; let mut accounts = vec![ @@ -957,9 +969,29 @@ pub fn inner_withdraw( AccountMeta::new_readonly(*mint, false), ]; - let proof_instruction_offset = match proof_data_location { + // if at least one of the proof locations is an instruction offset, sysvar + // account is needed + if equality_proof_data_location.is_instruction_offset() + || range_proof_data_location.is_instruction_offset() + { + accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); + } + + let equality_proof_instruction_offset = match equality_proof_data_location { + ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { + 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 + } + }; + + let range_proof_instruction_offset = match range_proof_data_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)); } @@ -989,7 +1021,8 @@ pub fn inner_withdraw( amount: amount.into(), decimals, new_decryptable_available_balance, - proof_instruction_offset, + equality_proof_instruction_offset, + range_proof_instruction_offset, }, )) } @@ -1002,10 +1035,11 @@ pub fn withdraw( mint: &Pubkey, amount: u64, decimals: u8, - new_decryptable_available_balance: AeCiphertext, + new_decryptable_available_balance: PodAeCiphertext, authority: &Pubkey, multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, + equality_proof_data_location: ProofLocation, + range_proof_data_location: ProofLocation, ) -> Result, ProgramError> { let mut instructions = vec![inner_withdraw( token_program_id, @@ -1013,27 +1047,48 @@ pub fn withdraw( mint, amount, decimals, - new_decryptable_available_balance.into(), + new_decryptable_available_balance, authority, multisig_signers, - proof_data_location, + equality_proof_data_location, + range_proof_data_location, )?]; + let mut expected_instruction_offset = 1; + if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - proof_data_location + equality_proof_data_location { - // This constructor appends the proof instruction right after the `Withdraw` - // instruction. This means that the proof instruction offset must be - // always be 1. To use an arbitrary proof instruction offset, use the - // `inner_withdraw` constructor. let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 1 { + if proof_instruction_offset != expected_instruction_offset { return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::InstructionData(data) => instructions.push(verify_withdraw(None, data)), + ProofData::InstructionData(data) => instructions.push( + ProofInstruction::VerifyCiphertextCommitmentEquality + .encode_verify_proof(None, data), + ), ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyWithdraw + ProofInstruction::VerifyCiphertextCommitmentEquality + .encode_verify_proof_from_account(None, address, offset), + ), + }; + + expected_instruction_offset += 1; + }; + + if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = + range_proof_data_location + { + let proof_instruction_offset: i8 = proof_instruction_offset.into(); + if proof_instruction_offset != expected_instruction_offset { + return Err(TokenError::InvalidProofInstructionOffset.into()); + } + match proof_data { + ProofData::InstructionData(data) => instructions + .push(ProofInstruction::VerifyBatchedRangeProofU64.encode_verify_proof(None, data)), + ProofData::RecordAccount(address, offset) => instructions.push( + ProofInstruction::VerifyBatchedRangeProofU64 .encode_verify_proof_from_account(None, address, offset), ), }; @@ -1121,6 +1176,10 @@ pub fn inner_transfer( 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, @@ -1164,11 +1223,13 @@ pub fn transfer( range_proof_data_location, )?]; + let mut expected_instruction_offset = 1; + if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = equality_proof_data_location { let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 1 { + if proof_instruction_offset != expected_instruction_offset { return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { @@ -1181,13 +1242,15 @@ pub fn transfer( .encode_verify_proof_from_account(None, address, offset), ), }; + + expected_instruction_offset += 1; } if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = ciphertext_validity_proof_data_location { let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 2 { + if proof_instruction_offset != expected_instruction_offset { return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { @@ -1200,13 +1263,15 @@ pub fn transfer( .encode_verify_proof_from_account(None, address, offset), ), }; + + expected_instruction_offset += 1; } if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = range_proof_data_location { let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 3 { + if proof_instruction_offset != expected_instruction_offset { return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { @@ -1261,7 +1326,7 @@ pub fn apply_pending_balance( token_program_id: &Pubkey, token_account: &Pubkey, pending_balance_instructions: u64, - new_decryptable_available_balance: AeCiphertext, + new_decryptable_available_balance: PodAeCiphertext, authority: &Pubkey, multisig_signers: &[&Pubkey], ) -> Result { @@ -1269,7 +1334,7 @@ pub fn apply_pending_balance( token_program_id, token_account, pending_balance_instructions, - new_decryptable_available_balance.into(), + new_decryptable_available_balance, authority, multisig_signers, ) // calls check_program_account @@ -1381,7 +1446,7 @@ pub fn inner_transfer_with_fee( transfer_amount_ciphertext_validity_proof_data_location: ProofLocation< BatchedGroupedCiphertext3HandlesValidityProofData, >, - fee_sigma_proof_data_location: ProofLocation, + fee_sigma_proof_data_location: ProofLocation, fee_ciphertext_validity_proof_data_location: ProofLocation< BatchedGroupedCiphertext2HandlesValidityProofData, >, @@ -1477,6 +1542,10 @@ pub fn inner_transfer_with_fee( 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, @@ -1507,7 +1576,7 @@ pub fn transfer_with_fee( transfer_amount_ciphertext_validity_proof_data_location: ProofLocation< BatchedGroupedCiphertext3HandlesValidityProofData, >, - fee_sigma_proof_data_location: ProofLocation, + fee_sigma_proof_data_location: ProofLocation, fee_ciphertext_validity_proof_data_location: ProofLocation< BatchedGroupedCiphertext2HandlesValidityProofData, >, @@ -1528,11 +1597,13 @@ pub fn transfer_with_fee( range_proof_data_location, )?]; + let mut expected_instruction_offset = 1; + if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = equality_proof_data_location { let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 1 { + if proof_instruction_offset != expected_instruction_offset { return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { @@ -1545,13 +1616,14 @@ pub fn transfer_with_fee( .encode_verify_proof_from_account(None, address, offset), ), }; + expected_instruction_offset += 1; } if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = transfer_amount_ciphertext_validity_proof_data_location { let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 2 { + if proof_instruction_offset != expected_instruction_offset { return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { @@ -1564,31 +1636,32 @@ pub fn transfer_with_fee( .encode_verify_proof_from_account(None, address, offset), ), }; + expected_instruction_offset += 1; } if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = fee_sigma_proof_data_location { let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 3 { + if proof_instruction_offset != expected_instruction_offset { return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::InstructionData(data) => { - instructions.push(ProofInstruction::VerifyFeeSigma.encode_verify_proof(None, data)) - } + ProofData::InstructionData(data) => instructions + .push(ProofInstruction::VerifyPercentageWithCap.encode_verify_proof(None, data)), ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyFeeSigma + ProofInstruction::VerifyPercentageWithCap .encode_verify_proof_from_account(None, address, offset), ), }; + expected_instruction_offset += 1; } if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = fee_ciphertext_validity_proof_data_location { let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 4 { + if proof_instruction_offset != expected_instruction_offset { return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { @@ -1601,13 +1674,14 @@ pub fn transfer_with_fee( .encode_verify_proof_from_account(None, address, offset), ), }; + expected_instruction_offset += 1; } if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = range_proof_data_location { let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 5 { + if proof_instruction_offset != expected_instruction_offset { return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { diff --git a/token/program-2022/src/extension/confidential_transfer/mod.rs b/token/program-2022/src/extension/confidential_transfer/mod.rs index 635a1c56bdc..f779db36938 100644 --- a/token/program-2022/src/extension/confidential_transfer/mod.rs +++ b/token/program-2022/src/extension/confidential_transfer/mod.rs @@ -5,7 +5,10 @@ use { }, bytemuck::{Pod, Zeroable}, solana_program::entrypoint::ProgramResult, - solana_zk_token_sdk::zk_token_elgamal::pod::{AeCiphertext, ElGamalCiphertext, ElGamalPubkey}, + solana_zk_sdk::encryption::pod::{ + auth_encryption::PodAeCiphertext, + elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + }, spl_pod::{ optional_keys::{OptionalNonZeroElGamalPubkey, OptionalNonZeroPubkey}, primitives::{PodBool, PodU64}, @@ -30,27 +33,14 @@ pub mod processor; /// Transfer Extension pub mod verify_proof; -/// Helper functions to generate split zero-knowledge proofs for confidential -/// transfers in the Confidential Transfer Extension. -/// -/// The logic in this submodule should belong to the `solana-zk-token-sdk` and -/// will be removed with the next upgrade to the Solana program. -#[cfg(not(target_os = "solana"))] -pub mod split_proof_generation; - /// Confidential Transfer Extension account information needed for instructions #[cfg(not(target_os = "solana"))] pub mod account_info; -/// Ciphertext extraction and proof related helper logic -/// -/// This submodule should be removed with the next upgrade to the Solana program -pub mod ciphertext_extraction; - /// ElGamal ciphertext containing an account balance -pub type EncryptedBalance = ElGamalCiphertext; +pub type EncryptedBalance = PodElGamalCiphertext; /// Authenticated encryption containing an account balance -pub type DecryptableBalance = AeCiphertext; +pub type DecryptableBalance = PodAeCiphertext; /// Confidential transfer mint configuration #[repr(C)] @@ -89,7 +79,7 @@ pub struct ConfidentialTransferAccount { pub approved: PodBool, /// The public key associated with ElGamal encryption - pub elgamal_pubkey: ElGamalPubkey, + pub elgamal_pubkey: PodElGamalPubkey, /// The low 16 bits of the pending balance (encrypted by `elgamal_pubkey`) pub pending_balance_lo: EncryptedBalance, diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 57ccc9f28f9..49f6ba2d8f7 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -2,14 +2,14 @@ #[cfg(feature = "zk-ops")] use { crate::extension::non_transferable::NonTransferableAccount, - solana_zk_token_sdk::zk_token_elgamal::ops as syscall, + spl_token_confidential_transfer_ciphertext_arithmetic as ciphertext_arithmetic, }; use { crate::{ check_program_account, error::TokenError, extension::{ - confidential_transfer::{ciphertext_extraction::*, instruction::*, verify_proof::*, *}, + confidential_transfer::{instruction::*, verify_proof::*, *}, confidential_transfer_fee::{ ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, EncryptedWithheldAmount, @@ -33,6 +33,9 @@ use { pubkey::Pubkey, sysvar::Sysvar, }, + spl_token_confidential_transfer_proof_extraction::{ + transfer::TransferProofContext, transfer_with_fee::TransferWithFeeProofContext, + }, }; /// Processes an [InitializeMint] instruction. @@ -102,11 +105,10 @@ fn process_configure_account( 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::( - account_info_iter, - proof_instruction_offset, - None, - )?; + 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(); @@ -210,11 +212,10 @@ fn process_empty_account( // zero-knowledge proof certifies that the available balance ciphertext holds // the balance of 0. - let proof_context = verify_and_extract_context::( - account_info_iter, - proof_instruction_offset, - None, - )?; + let proof_context = verify_and_extract_context::< + ZeroCiphertextProofData, + ZeroCiphertextProofContext, + >(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(); @@ -319,14 +320,18 @@ fn process_deposit( // Prevent unnecessary ciphertext arithmetic syscalls if `amount_lo` or // `amount_hi` is zero if amount_lo > 0 { - confidential_transfer_account.pending_balance_lo = - syscall::add_to(&confidential_transfer_account.pending_balance_lo, amount_lo) - .ok_or(TokenError::CiphertextArithmeticFailed)?; + confidential_transfer_account.pending_balance_lo = ciphertext_arithmetic::add_to( + &confidential_transfer_account.pending_balance_lo, + amount_lo, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; } if amount_hi > 0 { - confidential_transfer_account.pending_balance_hi = - syscall::add_to(&confidential_transfer_account.pending_balance_hi, amount_hi) - .ok_or(TokenError::CiphertextArithmeticFailed)?; + confidential_transfer_account.pending_balance_hi = ciphertext_arithmetic::add_to( + &confidential_transfer_account.pending_balance_hi, + amount_hi, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; } confidential_transfer_account.increment_pending_balance_credit_counter()?; @@ -354,7 +359,8 @@ fn process_withdraw( amount: u64, expected_decimals: u8, new_decryptable_available_balance: DecryptableBalance, - proof_instruction_offset: i64, + equality_proof_instruction_offset: i64, + range_proof_instruction_offset: i64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let token_account_info = next_account_info(account_info_iter)?; @@ -362,10 +368,10 @@ fn process_withdraw( // zero-knowledge proof certifies that the account has enough available balance // to withdraw the amount. - let proof_context = verify_and_extract_context::( + let proof_context = verify_withdraw_proof( account_info_iter, - proof_instruction_offset, - None, + equality_proof_instruction_offset, + range_proof_instruction_offset, )?; let authority_info = next_account_info(account_info_iter)?; @@ -416,20 +422,23 @@ fn process_withdraw( // Check that the encryption public key associated with the confidential // extension is consistent with the public key that was actually used to // generate the zkp. - if confidential_transfer_account.elgamal_pubkey != proof_context.pubkey { + if confidential_transfer_account.elgamal_pubkey != proof_context.source_pubkey { return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); } // Prevent unnecessary ciphertext arithmetic syscalls if the withdraw amount is // zero if amount > 0 { - confidential_transfer_account.available_balance = - syscall::subtract_from(&confidential_transfer_account.available_balance, amount) - .ok_or(TokenError::CiphertextArithmeticFailed)?; + confidential_transfer_account.available_balance = ciphertext_arithmetic::subtract_from( + &confidential_transfer_account.available_balance, + amount, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; } // Check that the final available balance ciphertext is consistent with the // actual ciphertext for which the zero-knowledge proof was generated for. - if confidential_transfer_account.available_balance != proof_context.final_ciphertext { + if confidential_transfer_account.available_balance != proof_context.remaining_balance_ciphertext + { return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); } @@ -632,7 +641,7 @@ fn process_source_for_transfer( mint_info: &AccountInfo, authority_info: &AccountInfo, signers: &[AccountInfo], - proof_context: &TransferProofContextInfo, + proof_context: &TransferProofContext, new_source_decryptable_available_balance: DecryptableBalance, ) -> ProgramResult { check_program_account(source_account_info.owner)?; @@ -672,10 +681,16 @@ fn process_source_for_transfer( return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); } - let source_transfer_amount_lo = transfer_amount_source_ciphertext(&proof_context.ciphertext_lo); - let source_transfer_amount_hi = transfer_amount_source_ciphertext(&proof_context.ciphertext_hi); + let source_transfer_amount_lo = proof_context + .ciphertext_lo + .try_extract_ciphertext(0) + .map_err(|e| -> TokenError { e.into() })?; + let source_transfer_amount_hi = proof_context + .ciphertext_hi + .try_extract_ciphertext(0) + .map_err(|e| -> TokenError { e.into() })?; - let new_source_available_balance = syscall::subtract_with_lo_hi( + let new_source_available_balance = ciphertext_arithmetic::subtract_with_lo_hi( &confidential_transfer_account.available_balance, &source_transfer_amount_lo, &source_transfer_amount_hi, @@ -699,7 +714,7 @@ fn process_source_for_transfer( fn process_destination_for_transfer( destination_account_info: &AccountInfo, mint_info: &AccountInfo, - proof_context: &TransferProofContextInfo, + proof_context: &TransferProofContext, ) -> ProgramResult { check_program_account(destination_account_info.owner)?; let destination_token_account_data = &mut destination_account_info.data.borrow_mut(); @@ -728,18 +743,22 @@ fn process_destination_for_transfer( return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); } - let destination_ciphertext_lo = - transfer_amount_destination_ciphertext(&proof_context.ciphertext_lo); - let destination_ciphertext_hi = - transfer_amount_destination_ciphertext(&proof_context.ciphertext_hi); + let destination_ciphertext_lo = proof_context + .ciphertext_lo + .try_extract_ciphertext(1) + .map_err(|e| -> TokenError { e.into() })?; + let destination_ciphertext_hi = proof_context + .ciphertext_hi + .try_extract_ciphertext(1) + .map_err(|e| -> TokenError { e.into() })?; - destination_confidential_transfer_account.pending_balance_lo = syscall::add( + destination_confidential_transfer_account.pending_balance_lo = ciphertext_arithmetic::add( &destination_confidential_transfer_account.pending_balance_lo, &destination_ciphertext_lo, ) .ok_or(TokenError::CiphertextArithmeticFailed)?; - destination_confidential_transfer_account.pending_balance_hi = syscall::add( + destination_confidential_transfer_account.pending_balance_hi = ciphertext_arithmetic::add( &destination_confidential_transfer_account.pending_balance_hi, &destination_ciphertext_hi, ) @@ -758,7 +777,7 @@ fn process_source_for_transfer_with_fee( mint_info: &AccountInfo, authority_info: &AccountInfo, signers: &[AccountInfo], - proof_context: &TransferWithFeeProofContextInfo, + proof_context: &TransferWithFeeProofContext, new_source_decryptable_available_balance: DecryptableBalance, ) -> ProgramResult { check_program_account(source_account_info.owner)?; @@ -800,10 +819,16 @@ fn process_source_for_transfer_with_fee( return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); } - let source_transfer_amount_lo = transfer_amount_source_ciphertext(&proof_context.ciphertext_lo); - let source_transfer_amount_hi = transfer_amount_source_ciphertext(&proof_context.ciphertext_hi); + let source_transfer_amount_lo = proof_context + .ciphertext_lo + .try_extract_ciphertext(0) + .map_err(|e| -> TokenError { e.into() })?; + let source_transfer_amount_hi = proof_context + .ciphertext_hi + .try_extract_ciphertext(0) + .map_err(|e| -> TokenError { e.into() })?; - let new_source_available_balance = syscall::subtract_with_lo_hi( + let new_source_available_balance = ciphertext_arithmetic::subtract_with_lo_hi( &confidential_transfer_account.available_balance, &source_transfer_amount_lo, &source_transfer_amount_hi, @@ -827,7 +852,7 @@ fn process_source_for_transfer_with_fee( fn process_destination_for_transfer_with_fee( destination_account_info: &AccountInfo, mint_info: &AccountInfo, - proof_context: &TransferWithFeeProofContextInfo, + proof_context: &TransferWithFeeProofContext, is_self_transfer: bool, ) -> ProgramResult { check_program_account(destination_account_info.owner)?; @@ -857,18 +882,22 @@ fn process_destination_for_transfer_with_fee( return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); } - let destination_transfer_amount_lo = - transfer_amount_destination_ciphertext(&proof_context.ciphertext_lo); - let destination_transfer_amount_hi = - transfer_amount_destination_ciphertext(&proof_context.ciphertext_hi); + let destination_transfer_amount_lo = proof_context + .ciphertext_lo + .try_extract_ciphertext(1) + .map_err(|e| -> TokenError { e.into() })?; + let destination_transfer_amount_hi = proof_context + .ciphertext_hi + .try_extract_ciphertext(1) + .map_err(|e| -> TokenError { e.into() })?; - destination_confidential_transfer_account.pending_balance_lo = syscall::add( + destination_confidential_transfer_account.pending_balance_lo = ciphertext_arithmetic::add( &destination_confidential_transfer_account.pending_balance_lo, &destination_transfer_amount_lo, ) .ok_or(TokenError::CiphertextArithmeticFailed)?; - destination_confidential_transfer_account.pending_balance_hi = syscall::add( + destination_confidential_transfer_account.pending_balance_hi = ciphertext_arithmetic::add( &destination_confidential_transfer_account.pending_balance_hi, &destination_transfer_amount_hi, ) @@ -880,40 +909,51 @@ fn process_destination_for_transfer_with_fee( if !is_self_transfer { // Decode lo and hi fee amounts encrypted under the destination encryption // public key - let destination_fee_lo = - fee_amount_destination_ciphertext(&proof_context.fee_ciphertext_lo); - let destination_fee_hi = - fee_amount_destination_ciphertext(&proof_context.fee_ciphertext_hi); + let destination_fee_lo = proof_context + .fee_ciphertext_lo + .try_extract_ciphertext(0) + .map_err(|e| -> TokenError { e.into() })?; + let destination_fee_hi = proof_context + .fee_ciphertext_hi + .try_extract_ciphertext(0) + .map_err(|e| -> TokenError { e.into() })?; // Subtract the fee amount from the destination pending balance - destination_confidential_transfer_account.pending_balance_lo = syscall::subtract( - &destination_confidential_transfer_account.pending_balance_lo, - &destination_fee_lo, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - destination_confidential_transfer_account.pending_balance_hi = syscall::subtract( - &destination_confidential_transfer_account.pending_balance_hi, - &destination_fee_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; + destination_confidential_transfer_account.pending_balance_lo = + ciphertext_arithmetic::subtract( + &destination_confidential_transfer_account.pending_balance_lo, + &destination_fee_lo, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; + destination_confidential_transfer_account.pending_balance_hi = + ciphertext_arithmetic::subtract( + &destination_confidential_transfer_account.pending_balance_hi, + &destination_fee_hi, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; // Decode lo and hi fee amounts encrypted under the withdraw authority // encryption public key - let withdraw_withheld_authority_fee_lo = - fee_amount_withdraw_withheld_authority_ciphertext(&proof_context.fee_ciphertext_lo); - let withdraw_withheld_authority_fee_hi = - fee_amount_withdraw_withheld_authority_ciphertext(&proof_context.fee_ciphertext_hi); + let withdraw_withheld_authority_fee_lo = proof_context + .fee_ciphertext_lo + .try_extract_ciphertext(1) + .map_err(|e| -> TokenError { e.into() })?; + let withdraw_withheld_authority_fee_hi = proof_context + .fee_ciphertext_hi + .try_extract_ciphertext(1) + .map_err(|e| -> TokenError { e.into() })?; let destination_confidential_transfer_fee_amount = destination_token_account.get_extension_mut::()?; // Add the fee amount to the destination withheld fee - destination_confidential_transfer_fee_amount.withheld_amount = syscall::add_with_lo_hi( - &destination_confidential_transfer_fee_amount.withheld_amount, - &withdraw_withheld_authority_fee_lo, - &withdraw_withheld_authority_fee_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; + destination_confidential_transfer_fee_amount.withheld_amount = + ciphertext_arithmetic::add_with_lo_hi( + &destination_confidential_transfer_fee_amount.withheld_amount, + &withdraw_withheld_authority_fee_lo, + &withdraw_withheld_authority_fee_hi, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; } Ok(()) @@ -949,7 +989,7 @@ fn process_apply_pending_balance( let confidential_transfer_account = token_account.get_extension_mut::()?; - confidential_transfer_account.available_balance = syscall::add_with_lo_hi( + confidential_transfer_account.available_balance = ciphertext_arithmetic::add_with_lo_hi( &confidential_transfer_account.available_balance, &confidential_transfer_account.pending_balance_lo, &confidential_transfer_account.pending_balance_hi, @@ -1101,7 +1141,8 @@ pub(crate) fn process_instruction( data.amount.into(), data.decimals, data.new_decryptable_available_balance, - data.proof_instruction_offset as i64, + data.equality_proof_instruction_offset as i64, + data.range_proof_instruction_offset as i64, ) } #[cfg(not(feature = "zk-ops"))] diff --git a/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs b/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs deleted file mode 100644 index 46be6e6701d..00000000000 --- a/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs +++ /dev/null @@ -1,185 +0,0 @@ -//! Helper functions to generate split zero-knowledge proofs for confidential -//! transfers in the Confidential Transfer Extension. -//! -//! The logic in this submodule should belong to the `solana-zk-token-sdk` and -//! will be removed with the next upgrade to the Solana program. - -use crate::{ - extension::confidential_transfer::{ - ciphertext_extraction::transfer_amount_source_ciphertext, - processor::verify_and_split_deposit_amount, *, - }, - solana_zk_token_sdk::{ - encryption::{ - auth_encryption::{AeCiphertext, AeKey}, - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, - grouped_elgamal::GroupedElGamal, - pedersen::Pedersen, - }, - instruction::{ - transfer::TransferAmountCiphertext, BatchedGroupedCiphertext3HandlesValidityProofData, - BatchedRangeProofU128Data, CiphertextCommitmentEqualityProofData, - }, - zk_token_elgamal::ops::subtract_with_lo_hi, - }, -}; - -/// The main logic to create the three split proof data for a transfer. -pub fn transfer_split_proof_data( - current_available_balance: &ElGamalCiphertext, - current_decryptable_available_balance: &AeCiphertext, - transfer_amount: u64, - source_elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, -) -> Result< - ( - CiphertextCommitmentEqualityProofData, - BatchedGroupedCiphertext3HandlesValidityProofData, - BatchedRangeProofU128Data, - ), - TokenError, -> { - let default_auditor_pubkey = ElGamalPubkey::default(); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey); - - // Split the transfer amount into the low and high bit components. - let (transfer_amount_lo, transfer_amount_hi) = - verify_and_split_deposit_amount(transfer_amount)?; - - // Encrypt the `lo` and `hi` transfer amounts. - let (transfer_amount_grouped_ciphertext_lo, transfer_amount_opening_lo) = - TransferAmountCiphertext::new( - transfer_amount_lo, - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ); - - let (transfer_amount_grouped_ciphertext_hi, transfer_amount_opening_hi) = - TransferAmountCiphertext::new( - transfer_amount_hi, - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ); - - // Decrypt the current available balance at the source - let current_decrypted_available_balance = current_decryptable_available_balance - .decrypt(aes_key) - .ok_or(TokenError::AccountDecryption)?; - - // Compute the remaining balance at the source - let new_decrypted_available_balance = current_decrypted_available_balance - .checked_sub(transfer_amount) - .ok_or(TokenError::InsufficientFunds)?; - - // Create a new Pedersen commitment for the remaining balance at the source - let (new_available_balance_commitment, new_source_opening) = - Pedersen::new(new_decrypted_available_balance); - - // Compute the remaining balance at the source as ElGamal ciphertexts - let transfer_amount_source_ciphertext_lo = - transfer_amount_source_ciphertext(&transfer_amount_grouped_ciphertext_lo.into()); - let transfer_amount_source_ciphertext_hi = - transfer_amount_source_ciphertext(&transfer_amount_grouped_ciphertext_hi.into()); - - let current_available_balance = (*current_available_balance).into(); - let new_available_balance_ciphertext = subtract_with_lo_hi( - ¤t_available_balance, - &transfer_amount_source_ciphertext_lo, - &transfer_amount_source_ciphertext_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - let new_available_balance_ciphertext: ElGamalCiphertext = new_available_balance_ciphertext - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - - // generate equality proof data - let equality_proof_data = CiphertextCommitmentEqualityProofData::new( - source_elgamal_keypair, - &new_available_balance_ciphertext, - &new_available_balance_commitment, - &new_source_opening, - new_decrypted_available_balance, - ) - .map_err(|_| TokenError::ProofGeneration)?; - - // encrypt the transfer amount under the destination and auditor ElGamal public - // key - let transfer_amount_destination_auditor_ciphertext_lo = GroupedElGamal::encrypt_with( - [ - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ], - transfer_amount_lo, - &transfer_amount_opening_lo, - ); - let transfer_amount_destination_auditor_ciphertext_hi = GroupedElGamal::encrypt_with( - [ - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ], - transfer_amount_hi, - &transfer_amount_opening_hi, - ); - - // generate ciphertext validity data - let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new( - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - &transfer_amount_destination_auditor_ciphertext_lo, - &transfer_amount_destination_auditor_ciphertext_hi, - transfer_amount_lo, - transfer_amount_hi, - &transfer_amount_opening_lo, - &transfer_amount_opening_hi, - ) - .map_err(|_| TokenError::ProofGeneration)?; - - // generate range proof data - const REMAINING_BALANCE_BIT_LENGTH: usize = 64; - const TRANSFER_AMOUNT_LO_BIT_LENGTH: usize = 16; - const TRANSFER_AMOUNT_HI_BIT_LENGTH: usize = 32; - const PADDING_BIT_LENGTH: usize = 16; - - let (padding_commitment, padding_opening) = Pedersen::new(0_u64); - - let range_proof_data = BatchedRangeProofU128Data::new( - vec![ - &new_available_balance_commitment, - transfer_amount_grouped_ciphertext_lo.get_commitment(), - transfer_amount_grouped_ciphertext_hi.get_commitment(), - &padding_commitment, - ], - vec![ - new_decrypted_available_balance, - transfer_amount_lo, - transfer_amount_hi, - 0, - ], - vec![ - REMAINING_BALANCE_BIT_LENGTH, - TRANSFER_AMOUNT_LO_BIT_LENGTH, - TRANSFER_AMOUNT_HI_BIT_LENGTH, - PADDING_BIT_LENGTH, - ], - vec![ - &new_source_opening, - &transfer_amount_opening_lo, - &transfer_amount_opening_hi, - &padding_opening, - ], - ) - .map_err(|_| TokenError::ProofGeneration)?; - - Ok(( - equality_proof_data, - ciphertext_validity_proof_data, - range_proof_data, - )) -} 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 e48baba6053..912d3c66d3d 100644 --- a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs +++ b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs @@ -1,18 +1,61 @@ use { crate::{ - extension::{ - confidential_transfer::{ciphertext_extraction::*, instruction::*}, - transfer_fee::TransferFee, - }, + 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, + }, std::slice::Iter, }; +/// Verify zero-knowledge proofs needed for a [Withdraw] instruction and return +/// the corresponding proof context. +#[cfg(feature = "zk-ops")] +pub fn verify_withdraw_proof( + account_info_iter: &mut Iter, + equality_proof_instruction_offset: i64, + range_proof_instruction_offset: i64, +) -> Result { + let sysvar_account_info = + if equality_proof_instruction_offset != 0 || range_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, + sysvar_account_info, + )?; + + let range_proof_context = + verify_and_extract_context::( + account_info_iter, + range_proof_instruction_offset, + sysvar_account_info, + )?; + + // The `WithdrawProofContext` constructor verifies the consistency of the + // individual proof context and generates a `WithdrawProofContext` struct + // that is used to process the rest of the token-2022 logic. + let transfer_proof_context = + WithdrawProofContext::verify_and_extract(&equality_proof_context, &range_proof_context) + .map_err(|e| -> TokenError { e.into() })?; + + Ok(transfer_proof_context) +} + /// Verify zero-knowledge proof needed for a [Transfer] instruction without fee /// and return the corresponding proof context. #[cfg(feature = "zk-ops")] @@ -21,7 +64,7 @@ pub fn verify_transfer_proof( equality_proof_instruction_offset: i64, ciphertext_validity_proof_instruction_offset: i64, range_proof_instruction_offset: i64, -) -> Result { +) -> Result { let sysvar_account_info = if equality_proof_instruction_offset != 0 || ciphertext_validity_proof_instruction_offset != 0 || range_proof_instruction_offset != 0 @@ -56,14 +99,15 @@ pub fn verify_transfer_proof( sysvar_account_info, )?; - // The `TransferProofContextInfo` constructor verifies the consistency of the + // The `TransferProofContext` constructor verifies the consistency of the // individual proof context and generates a `TransferWithFeeProofInfo` struct // that is used to process the rest of the token-2022 logic. - let transfer_proof_context = TransferProofContextInfo::verify_and_extract( + let transfer_proof_context = TransferProofContext::verify_and_extract( &equality_proof_context, &ciphertext_validity_proof_context, &range_proof_context, - )?; + ) + .map_err(|e| -> TokenError { e.into() })?; Ok(transfer_proof_context) } @@ -80,7 +124,7 @@ pub fn verify_transfer_with_fee_proof( fee_ciphertext_validity_proof_instruction_offset: i64, range_proof_instruction_offset: i64, fee_parameters: &TransferFee, -) -> Result { +) -> Result { let sysvar_account_info = if equality_proof_instruction_offset != 0 || transfer_amount_ciphertext_validity_proof_instruction_offset != 0 || fee_sigma_proof_instruction_offset != 0 @@ -111,7 +155,7 @@ pub fn verify_transfer_with_fee_proof( )?; let fee_sigma_proof_context = - verify_and_extract_context::( + verify_and_extract_context::( account_info_iter, fee_sigma_proof_instruction_offset, sysvar_account_info, @@ -133,20 +177,22 @@ pub fn verify_transfer_with_fee_proof( sysvar_account_info, )?; - // The `TransferWithFeeProofContextInfo` constructor verifies the consistency of + // The `TransferWithFeeProofContext` constructor verifies the consistency of // the individual proof context and generates a // `TransferWithFeeProofInfo` struct that is used to process the rest of // the token-2022 logic. The consistency check includes verifying // whether the fee-related zkps were generated with respect to the correct fee // parameter that is stored in the mint extension. - let transfer_with_fee_proof_context = TransferWithFeeProofContextInfo::verify_and_extract( + let transfer_with_fee_proof_context = TransferWithFeeProofContext::verify_and_extract( &equality_proof_context, &transfer_amount_ciphertext_validity_proof_context, &fee_sigma_proof_context, &fee_ciphertext_validity_proof_context, &range_proof_context, - fee_parameters, - )?; + fee_parameters.transfer_fee_basis_points.into(), + fee_parameters.maximum_fee.into(), + ) + .map_err(|e| -> TokenError { e.into() })?; Ok(transfer_with_fee_proof_context) } diff --git a/token/program-2022/src/extension/confidential_transfer_fee/account_info.rs b/token/program-2022/src/extension/confidential_transfer_fee/account_info.rs index 40a8919ae9c..ee87dd2f5a8 100644 --- a/token/program-2022/src/extension/confidential_transfer_fee/account_info.rs +++ b/token/program-2022/src/extension/confidential_transfer_fee/account_info.rs @@ -1,12 +1,12 @@ use { crate::{error::TokenError, extension::confidential_transfer_fee::EncryptedWithheldAmount}, bytemuck::{Pod, Zeroable}, - solana_zk_token_sdk::{ + solana_zk_sdk::{ encryption::{ elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, pedersen::PedersenOpening, }, - instruction::ciphertext_ciphertext_equality::CiphertextCiphertextEqualityProofData, + zk_elgamal_proof_program::proof_data::ciphertext_ciphertext_equality::CiphertextCiphertextEqualityProofData, }, }; 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 9bad6aec922..8a7a48d8626 100644 --- a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs @@ -8,15 +8,13 @@ use { check_program_account, error::TokenError, extension::confidential_transfer::{ - instruction::{ - verify_ciphertext_ciphertext_equality, CiphertextCiphertextEqualityProofData, - }, - DecryptableBalance, + instruction::CiphertextCiphertextEqualityProofData, DecryptableBalance, }, instruction::{encode_instruction, TokenInstruction}, proof::{ProofData, ProofLocation}, - solana_zk_token_sdk::{ - zk_token_elgamal::pod::ElGamalPubkey, zk_token_proof_instruction::ProofInstruction, + solana_zk_sdk::{ + encryption::pod::elgamal::PodElGamalPubkey, + zk_elgamal_proof_program::instruction::ProofInstruction, }, }, bytemuck::{Pod, Zeroable}, @@ -233,7 +231,7 @@ pub struct InitializeConfidentialTransferFeeConfigData { /// ElGamal public key used to encrypt withheld fees. #[cfg_attr(feature = "serde-traits", serde(with = "elgamalpubkey_fromstr"))] - pub withdraw_withheld_authority_elgamal_pubkey: ElGamalPubkey, + pub withdraw_withheld_authority_elgamal_pubkey: PodElGamalPubkey, } /// Data expected by @@ -277,7 +275,7 @@ pub fn initialize_confidential_transfer_fee_config( token_program_id: &Pubkey, mint: &Pubkey, authority: Option, - withdraw_withheld_authority_elgamal_pubkey: ElGamalPubkey, + withdraw_withheld_authority_elgamal_pubkey: PodElGamalPubkey, ) -> Result { check_program_account(token_program_id)?; let accounts = vec![AccountMeta::new(*mint, false)]; @@ -380,9 +378,10 @@ pub fn withdraw_withheld_tokens_from_mint( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::InstructionData(data) => { - instructions.push(verify_ciphertext_ciphertext_equality(None, data)) - } + ProofData::InstructionData(data) => instructions.push( + ProofInstruction::VerifyCiphertextCiphertextEquality + .encode_verify_proof(None, data), + ), ProofData::RecordAccount(address, offset) => instructions.push( ProofInstruction::VerifyCiphertextCiphertextEquality .encode_verify_proof_from_account(None, address, offset), @@ -491,9 +490,10 @@ pub fn withdraw_withheld_tokens_from_accounts( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::InstructionData(data) => { - instructions.push(verify_ciphertext_ciphertext_equality(None, data)) - } + ProofData::InstructionData(data) => instructions.push( + ProofInstruction::VerifyCiphertextCiphertextEquality + .encode_verify_proof(None, data), + ), ProofData::RecordAccount(address, offset) => instructions.push( ProofInstruction::VerifyCiphertextCiphertextEquality .encode_verify_proof_from_account(None, address, offset), diff --git a/token/program-2022/src/extension/confidential_transfer_fee/mod.rs b/token/program-2022/src/extension/confidential_transfer_fee/mod.rs index 64576b9a720..f48eb2dceb1 100644 --- a/token/program-2022/src/extension/confidential_transfer_fee/mod.rs +++ b/token/program-2022/src/extension/confidential_transfer_fee/mod.rs @@ -5,8 +5,9 @@ use { }, bytemuck::{Pod, Zeroable}, solana_program::entrypoint::ProgramResult, - solana_zk_token_sdk::zk_token_elgamal::pod::{ElGamalCiphertext, ElGamalPubkey, FeeEncryption}, + solana_zk_sdk::encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodBool}, + spl_token_confidential_transfer_proof_extraction::encryption::PodFeeCiphertext, }; /// Confidential transfer fee extension instructions @@ -21,9 +22,9 @@ pub mod processor; pub mod account_info; /// ElGamal ciphertext containing a transfer fee -pub type EncryptedFee = FeeEncryption; +pub type EncryptedFee = PodFeeCiphertext; /// ElGamal ciphertext containing a withheld fee in an account -pub type EncryptedWithheldAmount = ElGamalCiphertext; +pub type EncryptedWithheldAmount = PodElGamalCiphertext; /// Confidential transfer fee extension data for mints #[repr(C)] @@ -38,7 +39,7 @@ pub struct ConfidentialTransferFeeConfig { /// key has the ability to decode any withheld fee amount that are /// associated with accounts. When combined with the fee parameters, the /// withheld fee amounts can reveal information about transfer amounts. - pub withdraw_withheld_authority_elgamal_pubkey: ElGamalPubkey, + pub withdraw_withheld_authority_elgamal_pubkey: PodElGamalPubkey, /// If `false`, the harvest of withheld tokens to mint is rejected. pub harvest_to_mint_enabled: PodBool, 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 b835da72f2c..82549130f0e 100644 --- a/token/program-2022/src/extension/confidential_transfer_fee/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer_fee/processor.rs @@ -1,6 +1,6 @@ // Remove feature once zk ops syscalls are enabled on all networks #[cfg(feature = "zk-ops")] -use solana_zk_token_sdk::zk_token_elgamal::ops as syscall; +use spl_token_confidential_transfer_ciphertext_arithmetic as ciphertext_arithmetic; use { crate::{ check_program_account, @@ -28,7 +28,7 @@ use { pod::{PodAccount, PodMint}, processor::Processor, proof::verify_and_extract_context, - solana_zk_token_sdk::zk_token_elgamal::pod::ElGamalPubkey, + solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, }, bytemuck::Zeroable, solana_program::{ @@ -45,7 +45,7 @@ use { fn process_initialize_confidential_transfer_fee_config( accounts: &[AccountInfo], authority: &OptionalNonZeroPubkey, - withdraw_withheld_authority_elgamal_pubkey: &ElGamalPubkey, + withdraw_withheld_authority_elgamal_pubkey: &PodElGamalPubkey, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let mint_account_info = next_account_info(account_info_iter)?; @@ -134,28 +134,27 @@ fn process_withdraw_withheld_tokens_from_mint( // Check that the withdraw authority ElGamal public key associated with the mint // is consistent with what was actually used to generate the zkp. - if proof_context.source_pubkey + if proof_context.first_pubkey != confidential_transfer_fee_config.withdraw_withheld_authority_elgamal_pubkey { return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); } // Check that the ElGamal public key associated with the destination account is // consistent with what was actually used to generate the zkp. - if proof_context.destination_pubkey != destination_confidential_transfer_account.elgamal_pubkey - { + if proof_context.second_pubkey != destination_confidential_transfer_account.elgamal_pubkey { return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); } // Check that the withheld amount ciphertext is consistent with the ciphertext // data that was actually used to generate the zkp. - if proof_context.source_ciphertext != confidential_transfer_fee_config.withheld_amount { + if proof_context.first_ciphertext != confidential_transfer_fee_config.withheld_amount { return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); } // The proof data contains the mint withheld amount encrypted under the // destination ElGamal pubkey. Add this amount to the available balance. - destination_confidential_transfer_account.available_balance = syscall::add( + destination_confidential_transfer_account.available_balance = ciphertext_arithmetic::add( &destination_confidential_transfer_account.available_balance, - &proof_context.destination_ciphertext, + &proof_context.second_ciphertext, ) .ok_or(ProgramError::InvalidInstructionData)?; @@ -232,7 +231,7 @@ fn process_withdraw_withheld_tokens_from_accounts( .get_extension_mut::() .map_err(|_| TokenError::InvalidState)?; - aggregate_withheld_amount = syscall::add( + aggregate_withheld_amount = ciphertext_arithmetic::add( &aggregate_withheld_amount, &destination_confidential_transfer_fee_amount.withheld_amount, ) @@ -243,9 +242,11 @@ fn process_withdraw_withheld_tokens_from_accounts( } else { match harvest_from_account(mint_account_info.key, account_info) { Ok(encrypted_withheld_amount) => { - aggregate_withheld_amount = - syscall::add(&aggregate_withheld_amount, &encrypted_withheld_amount) - .ok_or(ProgramError::InvalidInstructionData)?; + aggregate_withheld_amount = ciphertext_arithmetic::add( + &aggregate_withheld_amount, + &encrypted_withheld_amount, + ) + .ok_or(ProgramError::InvalidInstructionData)?; } Err(e) => { msg!("Error harvesting from {}: {}", account_info.key, e); @@ -266,29 +267,28 @@ fn process_withdraw_withheld_tokens_from_accounts( // mint is consistent with what was actually used to generate the zkp. let confidential_transfer_fee_config = mint.get_extension_mut::()?; - if proof_context.source_pubkey + if proof_context.first_pubkey != confidential_transfer_fee_config.withdraw_withheld_authority_elgamal_pubkey { return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); } // Checks that the ElGamal public key associated with the destination account is // consistent with what was actually used to generate the zkp. - if proof_context.destination_pubkey != destination_confidential_transfer_account.elgamal_pubkey - { + if proof_context.second_pubkey != destination_confidential_transfer_account.elgamal_pubkey { return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); } // Checks that the withheld amount ciphertext is consistent with the ciphertext // data that was actually used to generate the zkp. - if proof_context.source_ciphertext != aggregate_withheld_amount { + if proof_context.first_ciphertext != aggregate_withheld_amount { return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); } // The proof data contains the mint withheld amount encrypted under the // destination ElGamal pubkey. This amount is added to the destination // available balance. - destination_confidential_transfer_account.available_balance = syscall::add( + destination_confidential_transfer_account.available_balance = ciphertext_arithmetic::add( &destination_confidential_transfer_account.available_balance, - &proof_context.destination_ciphertext, + &proof_context.second_ciphertext, ) .ok_or(ProgramError::InvalidInstructionData)?; @@ -345,7 +345,7 @@ fn process_harvest_withheld_tokens_to_mint(accounts: &[AccountInfo]) -> ProgramR for token_account_info in token_account_infos { match harvest_from_account(mint_account_info.key, token_account_info) { Ok(withheld_amount) => { - let new_mint_withheld_amount = syscall::add( + let new_mint_withheld_amount = ciphertext_arithmetic::add( &confidential_transfer_fee_mint.withheld_amount, &withheld_amount, ) diff --git a/token/program-2022/src/lib.rs b/token/program-2022/src/lib.rs index 7561c6b5d8e..dc2985b7b0e 100644 --- a/token/program-2022/src/lib.rs +++ b/token/program-2022/src/lib.rs @@ -27,7 +27,7 @@ mod entrypoint; use solana_program::{ entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey, system_program, }; -pub use {solana_program, solana_zk_token_sdk}; +pub use {solana_program, solana_zk_sdk}; /// Convert the UI representation of a token amount (using the decimals field /// defined in its mint) to the raw amount @@ -115,7 +115,7 @@ pub fn check_spl_token_program_account(spl_token_program_id: &Pubkey) -> Program /// Checks that the supplied program ID is correct for the ZK Token proof /// program pub fn check_zk_token_proof_program_account(zk_token_proof_program_id: &Pubkey) -> ProgramResult { - if zk_token_proof_program_id != &solana_zk_token_sdk::zk_token_proof_program::id() { + if zk_token_proof_program_id != &solana_zk_sdk::zk_elgamal_proof_program::id() { return Err(ProgramError::IncorrectProgramId); } Ok(()) diff --git a/token/program-2022/src/proof.rs b/token/program-2022/src/proof.rs index e1a4ccf1b1c..ac93788c7d3 100644 --- a/token/program-2022/src/proof.rs +++ b/token/program-2022/src/proof.rs @@ -11,11 +11,11 @@ use { pubkey::Pubkey, sysvar::instructions::get_instruction_relative, }, - solana_zk_token_sdk::{ - instruction::{ProofType, ZkProofData}, - zk_token_proof_instruction::ProofInstruction, - zk_token_proof_program, - zk_token_proof_state::ProofContextState, + solana_zk_sdk::zk_elgamal_proof_program::{ + self, + instruction::ProofInstruction, + proof_data::{ProofType, ZkProofData}, + state::ProofContextState, }, spl_pod::bytemuck::pod_from_bytes, std::{num::NonZeroI8, slice::Iter}, @@ -32,7 +32,7 @@ pub fn decode_proof_instruction_context, U: Pod>( expected: ProofInstruction, instruction: &Instruction, ) -> Result { - if instruction.program_id != zk_token_proof_program::id() + if instruction.program_id != zk_elgamal_proof_program::id() || ProofInstruction::instruction_type(&instruction.data) != Some(expected) { msg!("Unexpected proof instruction"); @@ -134,17 +134,17 @@ pub fn verify_and_extract_context<'a, T: Pod + ZkProofData, U: Pod>( } } -fn zk_proof_type_to_instruction(proof_type: ProofType) -> Result { +/// Converts a zk proof type to a corresponding ZK ElGamal proof program +/// instruction that verifies the proof. +pub fn zk_proof_type_to_instruction( + proof_type: ProofType, +) -> Result { match proof_type { - ProofType::ZeroBalance => Ok(ProofInstruction::VerifyZeroBalance), - ProofType::Withdraw => Ok(ProofInstruction::VerifyWithdraw), + ProofType::ZeroCiphertext => Ok(ProofInstruction::VerifyZeroCiphertext), ProofType::CiphertextCiphertextEquality => { Ok(ProofInstruction::VerifyCiphertextCiphertextEquality) } - ProofType::Transfer => Ok(ProofInstruction::VerifyTransfer), - ProofType::TransferWithFee => Ok(ProofInstruction::VerifyTransferWithFee), ProofType::PubkeyValidity => Ok(ProofInstruction::VerifyPubkeyValidity), - ProofType::RangeProofU64 => Ok(ProofInstruction::VerifyRangeProofU64), ProofType::BatchedRangeProofU64 => Ok(ProofInstruction::VerifyBatchedRangeProofU64), ProofType::BatchedRangeProofU128 => Ok(ProofInstruction::VerifyBatchedRangeProofU128), ProofType::BatchedRangeProofU256 => Ok(ProofInstruction::VerifyBatchedRangeProofU256), @@ -157,7 +157,7 @@ fn zk_proof_type_to_instruction(proof_type: ProofType) -> Result { Ok(ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity) } - ProofType::FeeSigma => Ok(ProofInstruction::VerifyFeeSigma), + ProofType::PercentageWithCap => Ok(ProofInstruction::VerifyPercentageWithCap), ProofType::GroupedCiphertext3HandlesValidity => { Ok(ProofInstruction::VerifyGroupedCiphertext3HandlesValidity) } diff --git a/token/program-2022/src/serialization.rs b/token/program-2022/src/serialization.rs index 9287caacab8..b246f1abc80 100644 --- a/token/program-2022/src/serialization.rs +++ b/token/program-2022/src/serialization.rs @@ -6,22 +6,6 @@ use { serde::de::Error, }; -/// helper function to convert base64 encoded string into a bytes array -fn base64_to_bytes(v: &str) -> Result<[u8; N], E> { - let bytes = BASE64_STANDARD.decode(v).map_err(Error::custom)?; - - if bytes.len() != N { - return Err(Error::custom(format!( - "Length of base64 decoded bytes is not {}", - N - ))); - } - - let mut array = [0; N]; - array.copy_from_slice(&bytes[0..N]); - Ok(array) -} - /// helper function to ser/deser COption wrapped values pub mod coption_fromstr { use { @@ -106,14 +90,12 @@ pub mod aeciphertext_fromstr { de::{Error, Visitor}, Deserializer, Serializer, }, - solana_zk_token_sdk::zk_token_elgamal::pod::AeCiphertext, - std::fmt, + solana_zk_sdk::encryption::pod::auth_encryption::PodAeCiphertext, + std::{fmt, str::FromStr}, }; - const AE_CIPHERTEXT_LEN: usize = 36; - /// serialize AeCiphertext values supporting Display trait - pub fn serialize(x: &AeCiphertext, s: S) -> Result + pub fn serialize(x: &PodAeCiphertext, s: S) -> Result where S: Serializer, { @@ -123,7 +105,7 @@ pub mod aeciphertext_fromstr { struct AeCiphertextVisitor; impl<'de> Visitor<'de> for AeCiphertextVisitor { - type Value = AeCiphertext; + type Value = PodAeCiphertext; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a FromStr type") @@ -133,13 +115,12 @@ pub mod aeciphertext_fromstr { where E: Error, { - let array = super::base64_to_bytes::(v)?; - Ok(AeCiphertext(array)) + FromStr::from_str(v).map_err(Error::custom) } } /// deserialize AeCiphertext values from str - pub fn deserialize<'de, D>(d: D) -> Result + pub fn deserialize<'de, D>(d: D) -> Result where D: Deserializer<'de>, { @@ -154,14 +135,12 @@ pub mod elgamalpubkey_fromstr { de::{Error, Visitor}, Deserializer, Serializer, }, - solana_zk_token_sdk::zk_token_elgamal::pod::ElGamalPubkey, - std::fmt, + solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, + std::{fmt, str::FromStr}, }; - const ELGAMAL_PUBKEY_LEN: usize = 32; - /// serialize ElGamalPubkey values supporting Display trait - pub fn serialize(x: &ElGamalPubkey, s: S) -> Result + pub fn serialize(x: &PodElGamalPubkey, s: S) -> Result where S: Serializer, { @@ -171,7 +150,7 @@ pub mod elgamalpubkey_fromstr { struct ElGamalPubkeyVisitor; impl<'de> Visitor<'de> for ElGamalPubkeyVisitor { - type Value = ElGamalPubkey; + type Value = PodElGamalPubkey; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a FromStr type") @@ -181,65 +160,15 @@ pub mod elgamalpubkey_fromstr { where E: Error, { - let array = super::base64_to_bytes::(v)?; - Ok(ElGamalPubkey(array)) + FromStr::from_str(v).map_err(Error::custom) } } /// deserialize ElGamalPubkey values from str - pub fn deserialize<'de, D>(d: D) -> Result + pub fn deserialize<'de, D>(d: D) -> Result where D: Deserializer<'de>, { d.deserialize_str(ElGamalPubkeyVisitor) } } - -/// helper to ser/deser pod::DecryptHandle values -pub mod decrypthandle_fromstr { - use { - base64::{prelude::BASE64_STANDARD, Engine}, - serde::{ - de::{Error, Visitor}, - Deserializer, Serializer, - }, - solana_zk_token_sdk::zk_token_elgamal::pod::DecryptHandle, - std::fmt, - }; - - const DECRYPT_HANDLE_LEN: usize = 32; - - /// Serialize a decrypt handle as a base64 string - pub fn serialize(x: &DecryptHandle, s: S) -> Result - where - S: Serializer, - { - s.serialize_str(&BASE64_STANDARD.encode(x.0)) - } - - struct DecryptHandleVisitor; - - impl<'de> Visitor<'de> for DecryptHandleVisitor { - type Value = DecryptHandle; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a FromStr type") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - let array = super::base64_to_bytes::(v)?; - Ok(DecryptHandle(array)) - } - } - - /// Deserialize a DecryptHandle from a base64 string - pub fn deserialize<'de, D>(d: D) -> Result - where - D: Deserializer<'de>, - { - d.deserialize_str(DecryptHandleVisitor) - } -} diff --git a/token/program-2022/tests/serialization.rs b/token/program-2022/tests/serialization.rs index 9645daadb83..1d8d3b5021f 100644 --- a/token/program-2022/tests/serialization.rs +++ b/token/program-2022/tests/serialization.rs @@ -1,13 +1,16 @@ #![cfg(feature = "serde-traits")] use { + base64::{engine::general_purpose::STANDARD, Engine}, solana_program::program_option::COption, solana_sdk::pubkey::Pubkey, spl_pod::optional_keys::{OptionalNonZeroElGamalPubkey, OptionalNonZeroPubkey}, spl_token_2022::{ extension::confidential_transfer, instruction, - solana_zk_token_sdk::zk_token_elgamal::pod::{AeCiphertext, ElGamalPubkey}, + solana_zk_sdk::encryption::pod::{ + auth_encryption::PodAeCiphertext, elgamal::PodElGamalPubkey, + }, }, std::str::FromStr, }; @@ -51,10 +54,12 @@ fn serde_instruction_optional_nonzero_pubkeys_podbool() { Some(Pubkey::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap()); let authority: OptionalNonZeroPubkey = authority_option.try_into().unwrap(); - let elgamal_pubkey_pod_option: Option = Some(ElGamalPubkey([ + let pubkey_string = STANDARD.encode([ 162, 23, 108, 36, 130, 143, 18, 219, 196, 134, 242, 145, 179, 49, 229, 193, 74, 64, 3, 158, 68, 235, 124, 88, 247, 144, 164, 254, 228, 12, 173, 85, - ])); + ]); + let elgamal_pubkey_pod_option = Some(FromStr::from_str(&pubkey_string).unwrap()); + let auditor_elgamal_pubkey: OptionalNonZeroElGamalPubkey = elgamal_pubkey_pod_option.try_into().unwrap(); @@ -105,10 +110,11 @@ fn serde_instruction_optional_nonzero_pubkeys_podbool_with_none() { #[test] fn serde_instruction_decryptable_balance_podu64() { - let decryptable_zero_balance = AeCiphertext([ + let ciphertext_string = STANDARD.encode([ 56, 22, 102, 48, 112, 106, 58, 25, 25, 244, 194, 217, 73, 137, 73, 38, 24, 26, 36, 25, 235, 234, 68, 181, 11, 82, 170, 163, 89, 205, 113, 160, 55, 16, 35, 151, ]); + let decryptable_zero_balance = FromStr::from_str(&ciphertext_string).unwrap(); let inst = confidential_transfer::instruction::ConfigureAccountInstructionData { decryptable_zero_balance, @@ -131,10 +137,11 @@ fn serde_instruction_decryptable_balance_podu64() { fn serde_instruction_elgamal_pubkey() { use spl_token_2022::extension::confidential_transfer_fee::instruction::InitializeConfidentialTransferFeeConfigData; - let withdraw_withheld_authority_elgamal_pubkey = ElGamalPubkey([ + let pubkey_string = STANDARD.encode([ 162, 23, 108, 36, 130, 143, 18, 219, 196, 134, 242, 145, 179, 49, 229, 193, 74, 64, 3, 158, 68, 235, 124, 88, 247, 144, 164, 254, 228, 12, 173, 85, ]); + let withdraw_withheld_authority_elgamal_pubkey = FromStr::from_str(&pubkey_string).unwrap(); let inst = InitializeConfidentialTransferFeeConfigData { authority: OptionalNonZeroPubkey::default(),