From e0528e7c84d4c34cbe65cdcbaef28a7e969f0596 Mon Sep 17 00:00:00 2001 From: Dario Bargel Date: Wed, 19 Jun 2024 09:30:59 -0300 Subject: [PATCH] confidential permanent-delegate extension --- Cargo.lock | 141 ++++++- token/cli/Cargo.toml | 3 + token/cli/src/clap_app.rs | 164 +++++++- token/cli/src/command.rs | 252 ++++++++++++- token/client/Cargo.toml | 2 + token/client/src/token.rs | 187 +++++++++- token/program-2022/Cargo.toml | 9 + .../instruction.rs | 350 ++++++++++++++++++ .../confidential_permanent_delegate/mod.rs | 156 ++++++++ .../processor.rs | 297 +++++++++++++++ .../confidential_transfer/account_info.rs | 4 +- .../ciphertext_extraction.rs | 27 ++ .../confidential_transfer/instruction.rs | 31 +- .../confidential_transfer/processor.rs | 31 +- .../split_proof_generation.rs | 1 + token/program-2022/src/extension/mod.rs | 13 + token/program-2022/src/instruction.rs | 7 + token/program-2022/src/pod_instruction.rs | 1 + token/program-2022/src/processor.rs | 14 +- 19 files changed, 1646 insertions(+), 44 deletions(-) create mode 100644 token/program-2022/src/extension/confidential_permanent_delegate/instruction.rs create mode 100644 token/program-2022/src/extension/confidential_permanent_delegate/mod.rs create mode 100644 token/program-2022/src/extension/confidential_permanent_delegate/processor.rs diff --git a/Cargo.lock b/Cargo.lock index 5e300fb514c..105b70916e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -641,9 +641,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.5.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" +checksum = "e6b4d9b1225d28d360ec6a231d65af1fd99a2a095154c8040689617290569c5c" [[package]] name = "binary-option" @@ -1304,6 +1304,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "const-oid" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" + [[package]] name = "const-oid" version = "0.7.1" @@ -1413,6 +1419,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "crypto-common" version = "0.1.3" @@ -1555,13 +1572,23 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +[[package]] +name = "der" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" +dependencies = [ + "const-oid 0.6.2", + "crypto-bigint", +] + [[package]] name = "der" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" dependencies = [ - "const-oid", + "const-oid 0.7.1", ] [[package]] @@ -1767,7 +1794,7 @@ dependencies = [ "ed25519", "rand 0.7.3", "serde", - "sha2 0.9.8", + "sha2 0.9.9", "zeroize", ] @@ -2872,6 +2899,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "lazycell" @@ -2931,7 +2961,7 @@ dependencies = [ "libsecp256k1-gen-genmult", "rand 0.7.3", "serde", - "sha2 0.9.8", + "sha2 0.9.9", "typenum", ] @@ -3326,6 +3356,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9bc3e36fd683e004fd59c64a425e0e991616f5a8b617c3b9a933a93c168facc" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.2.4" @@ -3684,6 +3731,15 @@ dependencies = [ "base64 0.13.0", ] +[[package]] +name = "pem-rfc7468" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e93a3b1cc0510b03020f33f21e62acdde3dcaef432edc95bea377fbd4c2cd4" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "1.0.1" @@ -3790,14 +3846,38 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "116bee8279d783c0cf370efa1a94632f2108e5ef0bb32df31f051647810a4e2c" +dependencies = [ + "der 0.4.5", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447" +dependencies = [ + "der 0.4.5", + "pem-rfc7468", + "pkcs1", + "spki 0.4.1", + "zeroize", +] + [[package]] name = "pkcs8" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" dependencies = [ - "der", - "spki", + "der 0.5.1", + "spki 0.5.4", "zeroize", ] @@ -4457,6 +4537,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rsa" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d" +dependencies = [ + "byteorder", + "digest 0.9.0", + "lazy_static", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8 0.7.6", + "rand 0.8.5", + "subtle", + "zeroize", +] + [[package]] name = "rtoolbox" version = "0.0.1" @@ -4891,9 +4991,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", @@ -6489,7 +6589,7 @@ dependencies = [ "nix", "pem", "percentage", - "pkcs8", + "pkcs8 0.8.0", "quinn", "quinn-proto", "rand 0.8.5", @@ -6838,6 +6938,15 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" +[[package]] +name = "spki" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32" +dependencies = [ + "der 0.4.5", +] + [[package]] name = "spki" version = "0.5.4" @@ -6845,7 +6954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" dependencies = [ "base64ct", - "der", + "der 0.5.1", ] [[package]] @@ -7569,10 +7678,13 @@ dependencies = [ "num-traits", "num_enum 0.7.2", "proptest", + "rand_core 0.6.4", + "rsa", "serde", "serde_json", "serde_with 3.8.1", "serial_test", + "sha2 0.9.9", "solana-program", "solana-program-test", "solana-sdk", @@ -7624,6 +7736,9 @@ dependencies = [ "console", "futures 0.3.30", "libtest-mimic", + "pem", + "rand_core 0.6.4", + "rsa", "serde", "serde_derive", "serde_json", @@ -7660,6 +7775,8 @@ dependencies = [ "curve25519-dalek", "futures 0.3.30", "futures-util", + "rsa", + "sha2 0.9.9", "solana-banks-interface", "solana-cli-output", "solana-program-test", @@ -8411,7 +8528,7 @@ dependencies = [ "pbkdf2 0.4.0", "rand 0.7.3", "rustc-hash", - "sha2 0.9.8", + "sha2 0.9.9", "thiserror", "unicode-normalization", "wasm-bindgen", diff --git a/token/cli/Cargo.toml b/token/cli/Cargo.toml index 44819e19e55..6d88291380e 100644 --- a/token/cli/Cargo.toml +++ b/token/cli/Cargo.toml @@ -46,6 +46,9 @@ spl-memo = { version = "4.0", path = "../../memo/program", features = [ strum = "0.26" strum_macros = "0.26" tokio = "1.38" +rsa = "0.5.0" +pem = "1.1.1" +rand_core = "0.6.3" [dev-dependencies] solana-test-validator = ">=1.18.11,<=2" diff --git a/token/cli/src/clap_app.rs b/token/cli/src/clap_app.rs index 94d67ef87c7..3206830d23c 100644 --- a/token/cli/src/clap_app.rs +++ b/token/cli/src/clap_app.rs @@ -133,6 +133,10 @@ pub enum CommandName { ApplyPendingBalance, UpdateGroupAddress, UpdateMemberAddress, + ConfigureRSA, + PostEncryptedKeys, + ApproveWithConfidentialPermanentDelegate, + GenerateRSA, } impl fmt::Display for CommandName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -196,7 +200,6 @@ pub enum CliAuthorityType { WithheldWithdraw, InterestRate, PermanentDelegate, - ConfidentialTransferMint, TransferHookProgramId, ConfidentialTransferFee, MetadataPointer, @@ -218,9 +221,6 @@ impl TryFrom for AuthorityType { CliAuthorityType::WithheldWithdraw => Ok(AuthorityType::WithheldWithdraw), CliAuthorityType::InterestRate => Ok(AuthorityType::InterestRate), CliAuthorityType::PermanentDelegate => Ok(AuthorityType::PermanentDelegate), - CliAuthorityType::ConfidentialTransferMint => { - Ok(AuthorityType::ConfidentialTransferMint) - } CliAuthorityType::TransferHookProgramId => Ok(AuthorityType::TransferHookProgramId), CliAuthorityType::ConfidentialTransferFee => { Ok(AuthorityType::ConfidentialTransferFeeConfig) @@ -819,6 +819,14 @@ pub fn app<'a, 'b>( .takes_value(false) .help("Enables group member configurations in the mint. The mint authority must initialize the member."), ) + .arg( + Arg::with_name("enable_confidential_permanent_delegate") + .long("enable-confidential-permanent-delegate") + .takes_value(false) + .help( + "Enables permanent delegate for confidential transfers" + ), + ) .nonce_args(true) .arg(memo_arg()) ) @@ -2616,4 +2624,152 @@ pub fn app<'a, 'b>( .arg(multisig_signer_arg()) .nonce_args(true) ) + .subcommand( + SubCommand::with_name(CommandName::PostEncryptedKeys.into()) + .about("Post encrypted ElGamalKeypair and AeKey to chain for mint with confidential permanent delegate enabled") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(2) + .help("The address of the token account to for which to fetch the confidential balance") + ) + .arg( + Arg::with_name("authority") + .long("authority") + .alias("owner") + .validator(is_valid_signer) + .value_name("SIGNER") + .takes_value(true) + .help("Keypair from which encryption keys for token account were derived.") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::ConfigureRSA.into()) + .about("Configure RSA public key for confidential permanent delegate") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("permanent_delegate") + .long("permanent-delegate") + .alias("owner") + .validator(is_valid_signer) + .value_name("SIGNER") + .takes_value(true) + .help("Keypair from which encryption keys for token account were derived.") + ) + .arg( + Arg::with_name("rsa_key") + .long("rsa-key") + .value_name("RSA_KEY") + .takes_value(true) + .help( + "PEM file containing either a RSA public or private key" + ) + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::ApproveWithConfidentialPermanentDelegate.into()) + .about("Approve an account for which the encryption keys have been posted to chain with the confidential permanent delegate") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(2) + .help("The address of the token account to for which to fetch the confidential balance") + ) + .arg( + Arg::with_name("permanent_delegate") + .long("permanent-delegate") + .alias("owner") + .validator(is_valid_signer) + .value_name("SIGNER") + .takes_value(true) + .help("Keypair from which encryption keys for token account were derived.") + ) + .arg( + Arg::with_name("rsa_key") + .long("rsa-key") + .value_name("RSA_KEY") + .takes_value(true) + .help( + "PEM file containing a RSA private key" + ) + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::GenerateRSA.into()) + .about("generate rsa key and persist it to file") + .arg( + Arg::with_name("outfile") + .long("outfile") + .value_name("OUT") + .takes_value(true) + .required(true) + .help("location to which to write generated key") + ) + .arg( + Arg::with_name("bits") + .long("bits") + .takes_value(true) + .default_value("4096") + .help("location to which to write generated key") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + ) } diff --git a/token/cli/src/command.rs b/token/cli/src/command.rs index 980408d74c7..831e685bc1a 100644 --- a/token/cli/src/command.rs +++ b/token/cli/src/command.rs @@ -10,6 +10,13 @@ use { }, clap::{value_t, value_t_or_exit, ArgMatches}, futures::try_join, + pem::parse, + rand_core::OsRng, + rsa::{ + pkcs1::FromRsaPublicKey, + pkcs8::{FromPrivateKey, ToPrivateKey}, + RsaPrivateKey, RsaPublicKey, + }, serde::Serialize, solana_account_decoder::{ parse_token::{get_token_account_mint, parse_token, TokenAccountType, UiAccountState}, @@ -36,9 +43,13 @@ use { spl_associated_token_account::get_associated_token_address_with_program_id, spl_token_2022::{ extension::{ + confidential_permanent_delegate::{ + encrypted_keys_pda_address, instruction::PrivateKeyType, + }, confidential_transfer::{ account_info::{ - ApplyPendingBalanceAccountInfo, TransferAccountInfo, WithdrawAccountInfo, + ApplyPendingBalanceAccountInfo, TransferAccountInfo, + WithdrawAccountInfo, }, instruction::TransferSplitContextStateAccounts, ConfidentialTransferAccount, ConfidentialTransferMint, @@ -72,7 +83,15 @@ use { }, spl_token_group_interface::state::TokenGroup, spl_token_metadata_interface::state::{Field, TokenMetadata}, - std::{collections::HashMap, fmt::Display, process::exit, rc::Rc, str::FromStr, sync::Arc}, + std::{ + collections::HashMap, + fmt::Display, + fs, + process::exit, + rc::Rc, + str::{self, FromStr}, + sync::Arc, + }, }; fn print_error_and_exit(e: E) -> T { @@ -239,6 +258,7 @@ async fn command_create_token( enable_group: bool, enable_member: bool, bulk_signers: Vec>, + enable_confidential_permanent_delegate: bool, ) -> CommandResult { println_display( config, @@ -285,6 +305,14 @@ async fn command_create_token( extensions.push(ExtensionInitializationParams::DefaultAccountState { state }) } + if enable_confidential_permanent_delegate { + extensions.push( + ExtensionInitializationParams::ConfidentialPermanentDelegate { + permanent_delegate: authority, + }, + ); + } + if let Some((transfer_fee_basis_points, maximum_fee)) = transfer_fee { extensions.push(ExtensionInitializationParams::TransferFeeConfig { transfer_fee_config_authority: Some(authority), @@ -909,6 +937,118 @@ async fn command_create_multisig( }) } +async fn command_configure_rsa( + config: &Config<'_>, + token_pubkey: Pubkey, + permanent_delegate: Pubkey, + rsa_pubkey: RsaPublicKey, + bulk_signers: Vec>, +) -> CommandResult { + let token = token_client_from_config(config, &token_pubkey, None)?; + + let res = token + .configure_rsa(&permanent_delegate, rsa_pubkey, &bulk_signers) + .await?; + + let tx_return = finish_tx(config, &res, false).await?; + Ok(match tx_return { + TransactionReturnData::CliSignature(signature) => { + config.output_format.formatted_string(&signature) + } + TransactionReturnData::CliSignOnlyData(sign_only_data) => { + config.output_format.formatted_string(&sign_only_data) + } + }) +} + +async fn command_post_encrypted_keys( + config: &Config<'_>, + token_pubkey: Pubkey, + address: Pubkey, + authority_signer: Arc, + bulk_signers: Vec>, +) -> CommandResult { + let token = token_client_from_config(config, &token_pubkey, None)?; + let elgamal_keypair = ElGamalKeypair::new_from_signer(&*authority_signer, b"").unwrap(); + let aes_seed = AeKey::seed_from_signer(&*authority_signer, b"").unwrap(); + + let res_elg = token + .post_encrypted_keys( + &authority_signer.pubkey(), + &address, + elgamal_keypair.to_bytes().to_vec(), + PrivateKeyType::ElGamalKeypair, + &bulk_signers, + ) + .await?; + + let res_aes = token + .post_encrypted_keys( + &authority_signer.pubkey(), + &address, + aes_seed, + PrivateKeyType::AeKey, + &bulk_signers, + ) + .await?; + + let tx_return_elg = finish_tx(config, &res_elg, false).await?; + println!( + "{}", + match tx_return_elg { + TransactionReturnData::CliSignature(signature) => { + config.output_format.formatted_string(&signature) + } + TransactionReturnData::CliSignOnlyData(sign_only_data) => { + config.output_format.formatted_string(&sign_only_data) + } + } + ); + let tx_return_aes = finish_tx(config, &res_aes, false).await?; + Ok(match tx_return_aes { + TransactionReturnData::CliSignature(signature) => { + config.output_format.formatted_string(&signature) + } + TransactionReturnData::CliSignOnlyData(sign_only_data) => { + config.output_format.formatted_string(&sign_only_data) + } + }) +} + +async fn command_approve_with_conf_perm_delegate( + config: &Config<'_>, + token_pubkey: Pubkey, + address: Pubkey, + perm_delegate: Pubkey, + rsa_key: RsaPrivateKey, + bulk_signers: Vec>, +) -> CommandResult { + let token = token_client_from_config(config, &token_pubkey, None)?; + + let key_pda = encrypted_keys_pda_address(&token_pubkey, &address, &config.program_id); + let key_pda_bytes = config.rpc_client.get_account_data(&key_pda).await?; + + let res = token + .approve_confidential_account_with_permanent_delegate( + &address, + &perm_delegate, + key_pda_bytes, + &rsa_key, + &bulk_signers, + ) + .await?; + + let tx_return_aes = finish_tx(config, &res, false).await?; + Ok(match tx_return_aes { + TransactionReturnData::CliSignature(signature) => { + config.output_format.formatted_string(&signature) + } + TransactionReturnData::CliSignOnlyData(sign_only_data) => { + config.output_format.formatted_string(&sign_only_data) + } + }) +} + #[allow(clippy::too_many_arguments)] async fn command_authorize( config: &Config<'_>, @@ -980,18 +1120,6 @@ async fn command_authorize( )) } } - CliAuthorityType::ConfidentialTransferMint => { - if let Ok(confidential_transfer_mint) = - mint.get_extension::() - { - Ok(Option::::from(confidential_transfer_mint.authority)) - } else { - Err(format!( - "Mint `{}` does not support confidential transfers", - account - )) - } - } CliAuthorityType::TransferHookProgramId => { if let Ok(extension) = mint.get_extension::() { Ok(Option::::from(extension.authority)) @@ -1094,7 +1222,6 @@ async fn command_authorize( | CliAuthorityType::WithheldWithdraw | CliAuthorityType::InterestRate | CliAuthorityType::PermanentDelegate - | CliAuthorityType::ConfidentialTransferMint | CliAuthorityType::TransferHookProgramId | CliAuthorityType::ConfidentialTransferFee | CliAuthorityType::MetadataPointer @@ -3523,6 +3650,7 @@ pub async fn process_command<'a>( arg_matches.is_present("enable_group"), arg_matches.is_present("enable_member"), bulk_signers, + arg_matches.is_present("enable_confidential_permanent_delegate"), ) .await } @@ -4557,6 +4685,100 @@ pub async fn process_command<'a>( ) .await } + (CommandName::ConfigureRSA, arg_matches) => { + let rsa_file = value_t_or_exit!(arg_matches, "rsa_key", String); + let pem_contents = fs::read_to_string(rsa_file.as_str()).unwrap(); + let pem = parse(pem_contents.clone()).unwrap(); + + let rsa_pubkey = match pem.tag.as_str() { + "PRIVATE KEY" => { + let private_key = RsaPrivateKey::from_pkcs8_pem(&pem_contents).unwrap(); + private_key.to_public_key() + } + "RSA PUBLIC KEY" => RsaPublicKey::from_pkcs1_pem(&pem_contents).unwrap(), + _ => { + panic!("no valid rsa key provided for ConfigureRSA"); + } + }; + + let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let (perm_delegate_signer, permanent_delegate) = + config.signer_or_default(arg_matches, "permanent_delegate", &mut wallet_manager); + let bulk_signers = vec![perm_delegate_signer]; + + command_configure_rsa( + config, + token_pubkey, + permanent_delegate, + rsa_pubkey, + bulk_signers, + ) + .await + } + (CommandName::PostEncryptedKeys, arg_matches) => { + let (auth_signer, _auth) = + config.signer_or_default(arg_matches, "authority", &mut wallet_manager); + let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let address = config + .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) + .await?; + + let bulk_signers = vec![auth_signer.clone()]; + command_post_encrypted_keys(config, token_pubkey, address, auth_signer, bulk_signers) + .await + } + (CommandName::ApproveWithConfidentialPermanentDelegate, arg_matches) => { + let rsa_file = value_t_or_exit!(arg_matches, "rsa_key", String); + let pem_contents = fs::read_to_string(rsa_file.as_str()).unwrap(); + let pem = parse(pem_contents.clone()).unwrap(); + + let rsa_key = match pem.tag.as_str() { + "PRIVATE KEY" => RsaPrivateKey::from_pkcs8_pem(&pem_contents).unwrap(), + _ => { + panic!("no valid rsa key provided for ConfigureRSA"); + } + }; + let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let address = config + .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) + .await?; + let (perm_delegate_signer, permanent_delegate) = + config.signer_or_default(arg_matches, "permanent_delegate", &mut wallet_manager); + let bulk_signers = vec![perm_delegate_signer]; + + command_approve_with_conf_perm_delegate( + config, + token_pubkey, + address, + permanent_delegate, + rsa_key, + bulk_signers, + ) + .await + } + (CommandName::GenerateRSA, arg_matches) => { + let rsa_file = value_t_or_exit!(arg_matches, "outfile", String); + let bits = value_t_or_exit!(arg_matches, "bits", String); + let bits = bits.parse::().unwrap(); + + let mut rng = OsRng; + let rsa_key = RsaPrivateKey::new(&mut rng, bits).unwrap(); + fs::write( + format!("./{}", rsa_file), + rsa_key.to_pkcs8_pem().unwrap().as_str(), + ) + .expect("Unable to write rsa key to file"); + + Ok(format!( + "RSA private key successfully written to {rsa_file}" + )) + } } } diff --git a/token/client/Cargo.toml b/token/client/Cargo.toml index 2d58adb0013..4efd3a69ee8 100644 --- a/token/client/Cargo.toml +++ b/token/client/Cargo.toml @@ -34,6 +34,8 @@ spl-token-group-interface = { version = "0.2.3", path = "../../token-group/inter spl-token-metadata-interface = { version = "0.3.3", path = "../../token-metadata/interface" } spl-transfer-hook-interface = { version = "0.6.3", path = "../transfer-hook/interface" } thiserror = "1.0" +rsa = "0.5.0" +sha2 = "0.9.9" [features] default = ["display"] diff --git a/token/client/src/token.rs b/token/client/src/token.rs index a9fefc21f70..f38ceade91b 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -8,6 +8,8 @@ use { }, futures::{future::join_all, try_join}, futures_util::TryFutureExt, + rsa::{PaddingScheme, RsaPrivateKey, RsaPublicKey}, + sha2::Sha256, solana_program_test::tokio::time, solana_sdk::{ account::Account as BaseAccount, @@ -18,7 +20,7 @@ use { program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, - signer::{signers::Signers, Signer, SignerError}, + signer::{signers::Signers, SeedDerivable, Signer, SignerError}, system_instruction, transaction::Transaction, }, @@ -30,6 +32,11 @@ use { }, spl_token_2022::{ extension::{ + confidential_permanent_delegate::{ + self, + instruction::{EncryptedPrivateKeyData, PrivateKeyType}, + ConfidentialPermanentDelegate, + }, confidential_transfer::{ self, account_info::{ @@ -54,7 +61,7 @@ use { proof::ProofLocation, solana_zk_token_sdk::{ encryption::{ - auth_encryption::AeKey, + auth_encryption::{AeCiphertext, AeKey}, elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey}, }, instruction::*, @@ -184,6 +191,9 @@ pub enum ExtensionInitializationParams { authority: Option, member_address: Option, }, + ConfidentialPermanentDelegate { + permanent_delegate: Pubkey, + }, } impl ExtensionInitializationParams { /// Get the extension type associated with the init params @@ -203,6 +213,9 @@ impl ExtensionInitializationParams { } Self::GroupPointer { .. } => ExtensionType::GroupPointer, Self::GroupMemberPointer { .. } => ExtensionType::GroupMemberPointer, + Self::ConfidentialPermanentDelegate { .. } => { + ExtensionType::ConfidentialPermanentDelegate + } } } /// Generate an appropriate initialization instruction for the given mint @@ -312,6 +325,13 @@ impl ExtensionInitializationParams { authority, member_address, ), + Self::ConfidentialPermanentDelegate { permanent_delegate } => { + confidential_permanent_delegate::instruction::initialize_mint( + token_program_id, + mint, + permanent_delegate, + ) + } } } } @@ -3377,6 +3397,7 @@ where &[confidential_transfer::instruction::apply_pending_balance( &self.program_id, account, + self.get_address(), expected_pending_balance_credit_counter, new_decryptable_available_balance, authority, @@ -4079,4 +4100,166 @@ where )); self.process_ixs(&instructions, signing_keypairs).await } + + pub async fn encryption_keys_from_bytes( + &self, + rsa_key: &RsaPrivateKey, + key_pda_bytes: Vec, + ) -> TokenResult<(ElGamalKeypair, AeKey)> { + let mint_info = self.get_mint_info().await?; + let confidential_permanent_delegate = + mint_info.get_extension::()?; + if !Into::::into(confidential_permanent_delegate.delegate_initialized) { + return Err(ProgramError::UninitializedAccount.into()); + } + + let padding = PaddingScheme::new_oaep::(); + let key_data: EncryptedPrivateKeyData = key_pda_bytes.try_into()?; + let elgamal_bytes = rsa_key + .decrypt( + padding, + &key_data.elgamal_keypair[..(Into::::into( + confidential_permanent_delegate.encryption_pubkey.len_n, + ) as usize)], + ) + .unwrap(); + let padding = PaddingScheme::new_oaep::(); + let aes_bytes = rsa_key + .decrypt( + padding, + &key_data.ae_key[..(Into::::into( + confidential_permanent_delegate.encryption_pubkey.len_n, + ) as usize)], + ) + .unwrap(); + let elgamal_keypair = ElGamalKeypair::from_bytes(&elgamal_bytes).unwrap(); + let aes_key = AeKey::from_seed(&aes_bytes).unwrap(); + + Ok((elgamal_keypair, aes_key)) + } + + /// Approve token account for usage of confidential transfers on mint + /// with a confidential permanent delegate + #[allow(clippy::too_many_arguments)] + pub async fn approve_confidential_account_with_permanent_delegate( + &self, + account: &Pubkey, + permanent_delegate: &Pubkey, + key_pda_bytes: Vec, + rsa_key: &RsaPrivateKey, + signing_keypairs: &S, + ) -> TokenResult { + let signing_pubkeys = signing_keypairs.pubkeys(); + let multisig_signers = self.get_multisig_signers(permanent_delegate, &signing_pubkeys); + + let account_data = self.get_account_info(account).await?; + let confidential_transfer_account = + account_data.get_extension::()?; + + let (elgamal_keypair, aes_key) = self + .encryption_keys_from_bytes(rsa_key, key_pda_bytes) + .await?; + + // generate dummy proof for change of 0 simply to + // prove ownership of encryption secrets + let proof_data = + confidential_transfer::instruction::PubkeyValidityData::new(&elgamal_keypair) + .map_err(|_| TokenError::ProofGeneration)?; + + let proof_location = ProofLocation::InstructionOffset(1.try_into().unwrap(), &proof_data); + + let decrypted_balance = aes_key + .decrypt( + &TryInto::::try_into( + confidential_transfer_account.decryptable_available_balance, + ) + .map_err(|_| ProgramError::InvalidAccountData)?, + ) + .ok_or(ProgramError::InvalidAccountData)?; + if decrypted_balance != 0 { + return Err(ProgramError::AccountAlreadyInitialized.into()); + } + + self.process_ixs( + &confidential_permanent_delegate::instruction::approve_account( + &self.program_id, + &self.pubkey, + account, + permanent_delegate, + &multisig_signers, + proof_location, + )?, + signing_keypairs, + ) + .await + } + + /// Configure RSA public key for mint with a confidential permanent delegate + #[allow(clippy::too_many_arguments)] + pub async fn configure_rsa( + &self, + permanent_delegate: &Pubkey, + rsa_pubkey: RsaPublicKey, + signing_keypairs: &S, + ) -> TokenResult { + let signing_pubkeys = signing_keypairs.pubkeys(); + let multisig_signers = self.get_multisig_signers(permanent_delegate, &signing_pubkeys); + + self.process_ixs( + &[confidential_permanent_delegate::instruction::configure_rsa( + &self.program_id, + &self.pubkey, + rsa_pubkey, + permanent_delegate, + &multisig_signers, + )?], + signing_keypairs, + ) + .await + } + + /// + #[allow(clippy::too_many_arguments)] + pub async fn post_encrypted_keys( + &self, + authority: &Pubkey, + ata_pubkey: &Pubkey, + key_bytes: Vec, + key_type: PrivateKeyType, + signing_keypairs: &S, + ) -> TokenResult { + let signing_pubkeys = signing_keypairs.pubkeys(); + let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); + let mint_info = self.get_mint_info().await?; + let confidential_permanent_delegate = + mint_info.get_extension::()?; + if !Into::::into(confidential_permanent_delegate.delegate_initialized) { + return Err(Into::::into( + spl_token_2022::error::TokenError::InvalidState, + ) + .into()); + } + + let rsa_pubkey = confidential_permanent_delegate + .encryption_pubkey + .to_rsa_public_key(); + + self.process_ixs( + &[ + confidential_permanent_delegate::instruction::post_encrypted_private_keys( + &self.program_id, + self.get_address(), + ata_pubkey, + authority, + &self.payer.pubkey(), + &multisig_signers, + rsa_pubkey, + key_bytes, + key_type, + )?, + ], + signing_keypairs, + ) + .await + } } diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index c123231d531..072600d4bdc 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -41,6 +41,15 @@ serde = { version = "1.0.203", optional = true } serde_with = { version = "3.8.1", optional = true } base64 = { version = "0.22.1", optional = true } +[target.'cfg(not(target_os = "solana"))'.dependencies.rsa] +version = ">=0.5.0" + +[target.'cfg(not(target_os = "solana"))'.dependencies.rand_core] +version = "0.6.3" + +[target.'cfg(not(target_os = "solana"))'.dependencies.sha2] +version = "0.9.9" + [dev-dependencies] lazy_static = "1.4.0" proptest = "1.4" diff --git a/token/program-2022/src/extension/confidential_permanent_delegate/instruction.rs b/token/program-2022/src/extension/confidential_permanent_delegate/instruction.rs new file mode 100644 index 00000000000..cbafd082425 --- /dev/null +++ b/token/program-2022/src/extension/confidential_permanent_delegate/instruction.rs @@ -0,0 +1,350 @@ +#[cfg(not(target_os = "solana"))] +use crate::{error::TokenError, proof::ProofLocation}; +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; +#[cfg(not(target_os = "solana"))] +use solana_zk_token_sdk::instruction::PubkeyValidityData; +#[cfg(not(target_os = "solana"))] +use { + super::encrypted_keys_pda_address, + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + rand_core::OsRng, + rsa::{PaddingScheme, PublicKey, RsaPublicKey}, + sha2::Sha256, + solana_program::{ + instruction::{AccountMeta, Instruction}, + system_program, sysvar, + }, + solana_zk_token_sdk::zk_token_proof_instruction::verify_pubkey_validity, +}; +use { + super::{EncyptionPublicKey, MAX_MODULUS_LENGTH}, + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_program::{program_error::ProgramError, pubkey::Pubkey}, + spl_pod::bytemuck::pod_get_packed_len, +}; + +/// Confidential Transfer extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] +#[repr(u8)] +pub enum ConfidentialPermanentDelegateInstruction { + /// Initializes the permanent delegate for the confidential mint + /// + /// The `ConfidentialPermanentDelegateInstruction::InitializeMint` + /// instruction requires no signers and MUST be included within the same + /// Transaction as `TokenInstruction::InitializeMint`. Otherwise another + /// party can initialize the configuration. + /// + /// The instruction fails if the `TokenInstruction::InitializeMint` + /// instruction has already executed for the mint. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The SPL Token mint. + /// + /// Data expected by this instruction: + /// `InitializeMintData` + InitializeMint, + /// Updates the permanent delegate for the mint + UpdateMint, + /// Configures RSA pubkey to be used for private key encryption + ConfigureRSA, + /// Persists ElGamal keypair and AES key encrypted with RSA + /// public key of permanent delegate into PDA + PostEncryptedPrivateKey, + /// Approves confidential transfer account for usage. For the approval + /// to happen, the permanent delegate signs the transaction and provides + /// a valid proof generated with the keys encrypted and posted by the + /// account owner + ApproveAccount, +} + +/// The type of private key shared / persited on chain +/// via a PostEncryptedPrivateKey instruction +#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] +#[repr(u8)] +pub enum PrivateKeyType { + /// ElGamalKeypair + ElGamalKeypair, + /// AeKey + AeKey, +} + +/// Data expected by `ConfidentialPermanentDelegateInstruction::InitializeMint` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct InitializeMintData { + /// Pubkey of the permanent delegate for the mint + pub permanent_delegate: Pubkey, +} + +/// Data expected by `ConfidentialPermanentDelegateInstruction::UpdateMint` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct UpdateMintData { + /// Updates the permanent delegate for the mint + pub new_permanent_delegate: Pubkey, +} + +/// Data expected by `ConfidentialPermanentDelegateInstruction::ConfigureRSA` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct ConfigureRSAInstructionData { + /// the rsa public key to be used for the encryption of the + /// private keys to be shared with the permanent delegate + pub rsa_pubkey: EncyptionPublicKey, +} + +/// Data expected by +/// `ConfidentialPermanentDelegateInstruction::PostEncryptedPrivateKeys` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct PostEncryptedKeysInstructionData { + /// Encrypted private key + pub data: [u8; MAX_MODULUS_LENGTH], + /// Type of encrypted key posted + pub key_type: u8, +} + +/// Data expected by +/// `ConfidentialPermanentDelegateInstruction::PostEncryptedPrivateKeys` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct EncryptedPrivateKeyData { + /// Encrypted elgamal_keypair + pub elgamal_keypair: [u8; MAX_MODULUS_LENGTH], + /// Encrypted aekey + pub ae_key: [u8; MAX_MODULUS_LENGTH], +} + +impl TryFrom> for EncryptedPrivateKeyData { + type Error = ProgramError; + fn try_from(bytes: Vec) -> Result { + if bytes.len() != pod_get_packed_len::() { + return Err(ProgramError::InvalidAccountData); + } + + let mut s = Self::zeroed(); + s.elgamal_keypair + .copy_from_slice(&bytes[..MAX_MODULUS_LENGTH]); + s.ae_key.copy_from_slice(&bytes[MAX_MODULUS_LENGTH..]); + Ok(s) + } +} + +/// Data expected by `ConfidentialPermanentDelegateInstruction::ApproveAccount` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct ApproveAccountData { + /// Relative location of the `ProofInstruction::VerifyPubkeyValidity` instruction + /// to the `ApproveAccount` instruction in the transaction. If the offset + /// is `0`, then use a context state account for the proof. + pub proof_instruction_offset: i8, +} + +/// Create a `InitializeMint` instruction +#[cfg(not(target_os = "solana"))] +pub fn initialize_mint( + token_program_id: &Pubkey, + mint: &Pubkey, + permanent_delegate: Pubkey, +) -> Result { + check_program_account(token_program_id)?; + let accounts = vec![AccountMeta::new(*mint, false)]; + + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialPermanentDelegateExtension, + ConfidentialPermanentDelegateInstruction::InitializeMint, + &InitializeMintData { permanent_delegate }, + )) +} + +/// Create a `UpdateMint` instruction +#[cfg(not(target_os = "solana"))] +pub fn update_mint( + token_program_id: &Pubkey, + mint: &Pubkey, + permanent_delegate: &Pubkey, + multisig_signers: &[&Pubkey], + new_permanent_delegate: Pubkey, +) -> Result { + check_program_account(token_program_id)?; + let mut accounts = vec![ + AccountMeta::new(*mint, false), + AccountMeta::new_readonly(*permanent_delegate, multisig_signers.is_empty()), + ]; + for multisig_signer in multisig_signers.iter() { + accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); + } + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialPermanentDelegateExtension, + ConfidentialPermanentDelegateInstruction::UpdateMint, + &UpdateMintData { + new_permanent_delegate, + }, + )) +} + +/// Create a `ConfigureRSA` instruction +#[cfg(not(target_os = "solana"))] +pub fn configure_rsa( + token_program_id: &Pubkey, + mint: &Pubkey, + rsa_pubkey: RsaPublicKey, + permanent_delegate: &Pubkey, + multisig_signers: &[&Pubkey], +) -> Result { + check_program_account(token_program_id)?; + let mut accounts = vec![ + AccountMeta::new(*mint, false), + AccountMeta::new_readonly(*permanent_delegate, multisig_signers.is_empty()), + ]; + + for multisig_signer in multisig_signers.iter() { + accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); + } + + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialPermanentDelegateExtension, + ConfidentialPermanentDelegateInstruction::ConfigureRSA, + &ConfigureRSAInstructionData { + rsa_pubkey: EncyptionPublicKey::from(rsa_pubkey), + }, + )) +} + +/// Create a `PostEncryptedPrivateKeys` instruction +#[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] +pub fn post_encrypted_private_keys( + token_program_id: &Pubkey, + mint: &Pubkey, + token_account: &Pubkey, + ata_authority: &Pubkey, + rent_payer: &Pubkey, + multisig_signers: &[&Pubkey], + rsa_pubkey: RsaPublicKey, + key_bytes: Vec, + key_type: PrivateKeyType, +) -> Result { + check_program_account(token_program_id)?; + let mut accounts = vec![ + AccountMeta::new_readonly(*mint, false), + AccountMeta::new(*token_account, false), + AccountMeta::new( + encrypted_keys_pda_address(mint, token_account, token_program_id), + false, + ), + AccountMeta::new_readonly(*ata_authority, multisig_signers.is_empty()), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new(*rent_payer, true), + ]; + + for multisig_signer in multisig_signers.iter() { + accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); + } + + let mut rng = OsRng; + let padding = PaddingScheme::new_oaep::(); + let encrypted_key = rsa_pubkey + .encrypt(&mut rng, padding, &key_bytes) + .expect("failed to encrypt secret key"); + let mut data = PostEncryptedKeysInstructionData::zeroed(); + data.key_type = key_type.into(); + if encrypted_key.len() != MAX_MODULUS_LENGTH { + println!("provided RSA public key is too large, only modulus lengths of up to 4096 bits are supported"); + return Err(ProgramError::InvalidInstructionData); + } + data.data[..encrypted_key.len()].copy_from_slice(&encrypted_key); + + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialPermanentDelegateExtension, + ConfidentialPermanentDelegateInstruction::PostEncryptedPrivateKey, + &data, + )) +} + +/// Create a `ApproveAccount` instruction +#[cfg(not(target_os = "solana"))] +pub fn approve_account( + token_program_id: &Pubkey, + mint: &Pubkey, + token_account: &Pubkey, + permanent_delegate: &Pubkey, + multisig_signers: &[&Pubkey], + proof_data_location: ProofLocation, +) -> Result, ProgramError> { + check_program_account(token_program_id)?; + let mut accounts = vec![ + AccountMeta::new_readonly(*mint, false), + AccountMeta::new(*token_account, false), + AccountMeta::new( + encrypted_keys_pda_address(mint, token_account, token_program_id), + false, + ), + AccountMeta::new_readonly(*permanent_delegate, multisig_signers.is_empty()), + ]; + + let proof_instruction_offset = match proof_data_location { + ProofLocation::InstructionOffset(proof_instruction_offset, _) => { + accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); + proof_instruction_offset.into() + } + ProofLocation::ContextStateAccount(context_state_account) => { + accounts.push(AccountMeta::new_readonly(*context_state_account, false)); + 0 + } + }; + + for multisig_signer in multisig_signers.iter() { + accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); + } + + let mut instructions = vec![encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialPermanentDelegateExtension, + ConfidentialPermanentDelegateInstruction::ApproveAccount, + &ApproveAccountData { + proof_instruction_offset, + }, + )]; + + if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = + proof_data_location + { + let proof_instruction_offset: i8 = proof_instruction_offset.into(); + if proof_instruction_offset != 1 { + return Err(TokenError::InvalidProofInstructionOffset.into()); + } + instructions.push(verify_pubkey_validity(None, proof_data)); + }; + + Ok(instructions) +} diff --git a/token/program-2022/src/extension/confidential_permanent_delegate/mod.rs b/token/program-2022/src/extension/confidential_permanent_delegate/mod.rs new file mode 100644 index 00000000000..da17638948a --- /dev/null +++ b/token/program-2022/src/extension/confidential_permanent_delegate/mod.rs @@ -0,0 +1,156 @@ +use crate::pod::{PodAccount, PodMint}; +#[cfg(not(target_os = "solana"))] +use rsa::{BigUint, PublicKeyParts, RsaPublicKey}; + +use super::{PodStateWithExtensions, PodStateWithExtensionsMut}; + +use { + super::BaseStateWithExtensions, + crate::extension::{Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, + solana_program::{account_info::AccountInfo, hash::hash, pubkey::Pubkey}, + spl_pod::primitives::{PodBool, PodU16}, +}; + +/// Maximum bit length of any mint or burn amount +/// +/// Any mint or burn amount must be less than 2^48 +pub const MAXIMUM_DEPOSIT_TRANSFER_AMOUNT: u64 = (u16::MAX as u64) + (1 << 16) * (u32::MAX as u64); + +/// Bit length of the low bits of pending balance plaintext +pub const PENDING_BALANCE_LO_BIT_LENGTH: u32 = 16; + +/// Maximum length of 512 bytes allows RSA keys +/// with a modulus of up to 4096 bits +pub const MAX_MODULUS_LENGTH: usize = 512; + +/// Maximum length of 17 bytes allows for the usage +/// of 2^16 + 1 as the RSA public key exponent +pub const MAX_EXPONENT_LENGTH: usize = 17; + +/// Confidential Transfer Extension instructions +pub mod instruction; + +/// Confidential Transfer Extension processor +pub mod processor; + +/// Confidential permanent delegate mint +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Default)] +#[repr(C)] +pub struct ConfidentialPermanentDelegate { + /// Authority to modify the `ConfidentialTransferMint` configuration and to + /// approve new accounts (if `auto_approve_new_accounts` is true) + /// + /// The legacy Token Multisig account is not supported as the authority + pub permanent_delegate: Pubkey, + + /// Flag whether the encryption public key has been initialized after + /// the creation of a mint with a confidential permanent delegate + pub delegate_initialized: PodBool, + + /// RSA public key to encrypt AES-Key and ElGamal-Keypair for new + /// confidential balance accounts with + pub encryption_pubkey: EncyptionPublicKey, +} + +/// Representation of RsaPublicKey usable for extension state +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct EncyptionPublicKey { + /// RSA public key modulus + pub n: [u8; MAX_MODULUS_LENGTH], + /// RSA public key exponent + pub e: [u8; MAX_EXPONENT_LENGTH], + /// RSA public key modulus length + pub len_n: PodU16, + /// RSA public key exponent length + pub len_e: u8, +} + +impl Default for EncyptionPublicKey { + fn default() -> Self { + Self { + n: [0_u8; MAX_MODULUS_LENGTH], + e: [0_u8; MAX_EXPONENT_LENGTH], + len_n: PodU16::zeroed(), + len_e: 0_u8, + } + } +} + +#[cfg(not(target_os = "solana"))] +impl EncyptionPublicKey { + /// converts EncyptionPublicKey into rsa::RsaPublicKey + pub fn to_rsa_public_key(&self) -> RsaPublicKey { + let mut n = Vec::from(self.n); + n.truncate(Into::::into(self.len_n) as usize); + let mut e = Vec::from(self.e); + e.truncate(self.len_e as usize); + let n = BigUint::from_bytes_le(&n); + let e = BigUint::from_bytes_le(&e); + + RsaPublicKey::new(n, e).unwrap() + } +} + +#[cfg(not(target_os = "solana"))] +impl From for EncyptionPublicKey { + fn from(rsa_pubkey: RsaPublicKey) -> Self { + let n = rsa_pubkey.n().to_bytes_le(); + let e = rsa_pubkey.e().to_bytes_le(); + + let mut pk = EncyptionPublicKey::zeroed(); + pk.n[..n.len()].copy_from_slice(&n); + pk.e[..e.len()].copy_from_slice(&e); + pk.len_n = PodU16::from(n.len() as u16); + pk.len_e = e.len() as u8; + pk + } +} + +impl Extension for ConfidentialPermanentDelegate { + const TYPE: ExtensionType = ExtensionType::ConfidentialPermanentDelegate; +} + +/// generates seed for pda to store encrypted private keys in +pub fn encrypted_keys_pda_seed(mint: &Pubkey, ata: &Pubkey) -> [u8; 32] { + let mut enc_key_pda_seed = mint.to_bytes().to_vec(); + enc_key_pda_seed.extend(ata.to_bytes()); + enc_key_pda_seed.extend(b"encrypted_keys"); + hash(&enc_key_pda_seed).to_bytes() +} + +/// generates address and bump for pda to store whitelist info into +pub fn encrypted_keys_pda_address_bump(seed: [u8; 32], program_id: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[&seed], program_id) +} + +/// generates address for pda to store whitelist info into +pub fn encrypted_keys_pda_address(mint: &Pubkey, ata: &Pubkey, program_id: &Pubkey) -> Pubkey { + let seed = encrypted_keys_pda_seed(mint, ata); + let (pda, _) = encrypted_keys_pda_address_bump(seed, program_id); + pda +} + +/// Returns the expected authority for the execution of a given instruction. +/// In case of the confidential-permanent-delegate extension not being +/// enabled on a mint this always return the token account owner +pub fn expected_authority( + mint: &PodStateWithExtensions<'_, PodMint>, + authority_info: &AccountInfo, + token_account: &PodStateWithExtensionsMut<'_, PodAccount>, +) -> Pubkey { + if let Ok(perm_del_ext) = mint.get_extension::() { + if &perm_del_ext.permanent_delegate == authority_info.key { + perm_del_ext.permanent_delegate + } else { + token_account.base.owner + } + } else { + token_account.base.owner + } +} diff --git a/token/program-2022/src/extension/confidential_permanent_delegate/processor.rs b/token/program-2022/src/extension/confidential_permanent_delegate/processor.rs new file mode 100644 index 00000000000..155ec9b32a6 --- /dev/null +++ b/token/program-2022/src/extension/confidential_permanent_delegate/processor.rs @@ -0,0 +1,297 @@ +#[cfg(feature = "zk-ops")] +use { + super::instruction::ApproveAccountData, + crate::extension::confidential_transfer::verify_proof::verify_configure_account_proof, +}; +use { + super::{ + encrypted_keys_pda_address, encrypted_keys_pda_address_bump, encrypted_keys_pda_seed, + instruction::{ + ConfidentialPermanentDelegateInstruction, PostEncryptedKeysInstructionData, + PrivateKeyType, + }, + ConfidentialPermanentDelegate, EncyptionPublicKey, MAX_MODULUS_LENGTH, + }, + crate::{ + check_program_account, + error::TokenError, + extension::{ + confidential_permanent_delegate::instruction::{ + ConfigureRSAInstructionData, EncryptedPrivateKeyData, InitializeMintData, + UpdateMintData, + }, + confidential_transfer::ConfidentialTransferAccount, + BaseStateWithExtensions, BaseStateWithExtensionsMut, PodStateWithExtensions, + PodStateWithExtensionsMut, + }, + instruction::{decode_instruction_data, decode_instruction_type}, + pod::{PodAccount, PodMint}, + processor::Processor, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program::invoke_signed, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + system_instruction, + sysvar::Sysvar, + }, + spl_pod::bytemuck::pod_get_packed_len, +}; + +/// Processes an [InitializeMint] instruction. +fn process_initialize_mint(accounts: &[AccountInfo], permanent_delegate: Pubkey) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + + check_program_account(mint_info.owner)?; + + let mint_data = &mut mint_info.data.borrow_mut(); + let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(mint_data)?; + let whitelist_transfer_mint = mint.init_extension::(true)?; + + whitelist_transfer_mint.permanent_delegate = permanent_delegate; + + Ok(()) +} + +/// Processes an [UpdateMint] instruction. +fn process_update_mint( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_delegate: Pubkey, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + let permanent_delegate_info = next_account_info(account_info_iter)?; + let delegate_info_data_len = permanent_delegate_info.data_len(); + + check_program_account(mint_info.owner)?; + let mint_data = &mut mint_info.data.borrow_mut(); + let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(mint_data)?; + let confidential_pd_mint = mint.get_extension_mut::()?; + + Processor::validate_owner( + program_id, + &confidential_pd_mint.permanent_delegate, + permanent_delegate_info, + delegate_info_data_len, + account_info_iter.as_slice(), + )?; + + confidential_pd_mint.permanent_delegate = new_delegate; + + Ok(()) +} + +/// Processes a [ConfigureRSA] instruction. +#[cfg(feature = "zk-ops")] +fn process_configure_rsa( + program_id: &Pubkey, + accounts: &[AccountInfo], + rsa_pubkey: EncyptionPublicKey, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + let permanent_delegate_info = next_account_info(account_info_iter)?; + let delegate_info_data_len = permanent_delegate_info.data_len(); + + check_program_account(mint_info.owner)?; + let mint_data = &mut mint_info.data.borrow_mut(); + let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; + let confidential_pd_mint = mint.get_extension_mut::()?; + + Processor::validate_owner( + program_id, + &confidential_pd_mint.permanent_delegate, + permanent_delegate_info, + delegate_info_data_len, + account_info_iter.as_slice(), + )?; + + confidential_pd_mint.encryption_pubkey = rsa_pubkey; + confidential_pd_mint.delegate_initialized = true.into(); + + Ok(()) +} + +/// Processes a [PostEncryptedPrivateKeys] instruction. +#[cfg(feature = "zk-ops")] +fn process_post_encrypted_keys( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &PostEncryptedKeysInstructionData, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + let token_account_info = next_account_info(account_info_iter)?; + let enc_key_pda_info = next_account_info(account_info_iter)?; + let ata_authority_info = next_account_info(account_info_iter)?; + let system_program_info = next_account_info(account_info_iter)?; + let rent_payer_info = next_account_info(account_info_iter)?; + + if !ata_authority_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + let mint_data = &mint_info.data.borrow(); + let mint = PodStateWithExtensions::::unpack(mint_data)?; + + if mint + .get_extension::() + .is_err() + { + return Err(TokenError::ExtensionNotFound.into()); + } + + let pda_seed = encrypted_keys_pda_seed(mint_info.key, token_account_info.key); + let (pda_address, pda_bump) = encrypted_keys_pda_address_bump(pda_seed, program_id); + if &pda_address != enc_key_pda_info.key { + msg!("calculated encrypted key pda and supplied account info do not match"); + return Err(ProgramError::InvalidInstructionData); + } + + let token_account_data = &mut token_account_info.data.borrow_mut(); + let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; + + let confidential_transfer_state = + token_account.get_extension_mut::()?; + if confidential_transfer_state.approved.into() { + msg!("cannot alter posted keys on already approved token accounts"); + return Err(ProgramError::AccountAlreadyInitialized); + } + + let num_bytes = pod_get_packed_len::(); + let pda_rent = Rent::get()?.minimum_balance(num_bytes); + + let key_type: PrivateKeyType = data + .key_type + .try_into() + .or(Err(ProgramError::InvalidInstructionData))?; + + if enc_key_pda_info.lamports() != pda_rent { + invoke_signed( + &system_instruction::create_account( + rent_payer_info.key, + &pda_address, + pda_rent, + num_bytes as u64, + program_id, + ), + &[ + rent_payer_info.clone(), + enc_key_pda_info.clone(), + system_program_info.clone(), + ], + &[&[&pda_seed, &[pda_bump]]], // signature + )?; + } + + let mut pda_data = enc_key_pda_info.data.borrow_mut(); + match key_type { + PrivateKeyType::ElGamalKeypair => { + pda_data[..MAX_MODULUS_LENGTH].copy_from_slice(&data.data) + } + PrivateKeyType::AeKey => pda_data[MAX_MODULUS_LENGTH..].copy_from_slice(&data.data), + } + + Ok(()) +} + +/// Processes a [ApproveAccount] instruction. +#[cfg(feature = "zk-ops")] +fn process_approve_account( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &ApproveAccountData, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + let token_account_info = next_account_info(account_info_iter)?; + let enc_key_pda_info = next_account_info(account_info_iter)?; + let permanent_delegate_info = next_account_info(account_info_iter)?; + + // zero-knowledge proof certifies that the supplied ElGamal public key is valid + let proof_context = + verify_configure_account_proof(account_info_iter, data.proof_instruction_offset as i64)?; + + if !permanent_delegate_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + let mint_data = &mint_info.data.borrow(); + let mint = PodStateWithExtensions::::unpack(mint_data)?; + + if mint + .get_extension::() + .is_err() + { + return Err(TokenError::ExtensionNotFound.into()); + } + + let pda_address = encrypted_keys_pda_address(mint_info.key, token_account_info.key, program_id); + if &pda_address != enc_key_pda_info.key { + msg!("calculated encrypted key pda and supplied account info do not match"); + return Err(ProgramError::InvalidInstructionData); + } + + if enc_key_pda_info.lamports() + < Rent::get()?.minimum_balance(pod_get_packed_len::()) + { + return Err(ProgramError::UninitializedAccount); + } + + let token_account_data = &mut token_account_info.data.borrow_mut(); + let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; + + let confidential_transfer_state = + token_account.get_extension_mut::()?; + if confidential_transfer_state.elgamal_pubkey != proof_context.pubkey { + msg!("elgamal pubkeys from proof and token account don't match"); + return Err(ProgramError::InvalidInstructionData); + } + + confidential_transfer_state.approved = true.into(); + + Ok(()) +} + +#[allow(dead_code)] +pub(crate) fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + check_program_account(program_id)?; + + match decode_instruction_type(input)? { + ConfidentialPermanentDelegateInstruction::InitializeMint => { + msg!("ConfidentialPermanentDelegateInstruction::InitializeMint"); + let data = decode_instruction_data::(input)?; + process_initialize_mint(accounts, data.permanent_delegate) + } + ConfidentialPermanentDelegateInstruction::UpdateMint => { + msg!("ConfidentialPermanentDelegateInstruction::UpdateMint"); + let data = decode_instruction_data::(input)?; + process_update_mint(program_id, accounts, data.new_permanent_delegate) + } + ConfidentialPermanentDelegateInstruction::ConfigureRSA => { + msg!("ConfidentialPermanentDelegateInstruction::ConfigureRSA"); + let data = decode_instruction_data::(input)?; + process_configure_rsa(program_id, accounts, data.rsa_pubkey) + } + ConfidentialPermanentDelegateInstruction::PostEncryptedPrivateKey => { + msg!("ConfidentialPermanentDelegateInstruction::PostEncryptedPrivateKeys"); + let data = decode_instruction_data::(input)?; + process_post_encrypted_keys(program_id, accounts, data) + } + ConfidentialPermanentDelegateInstruction::ApproveAccount => { + msg!("ConfidentialPermanentDelegateInstruction::ApproveAccount"); + let data = decode_instruction_data::(input)?; + process_approve_account(program_id, accounts, data) + } + } +} 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 bfa88b73b3e..6f311de06ad 100644 --- a/token/program-2022/src/extension/confidential_transfer/account_info.rs +++ b/token/program-2022/src/extension/confidential_transfer/account_info.rs @@ -274,6 +274,7 @@ impl TransferAccountInfo { /// Create a transfer proof data that is split into equality, ciphertext /// validity, and range proofs. + #[allow(clippy::type_complexity)] pub fn generate_split_transfer_proof_data( &self, transfer_amount: u64, @@ -367,7 +368,8 @@ impl TransferAccountInfo { } } -fn combine_balances(balance_lo: u64, balance_hi: u64) -> Option { +/// Combines pending balances low and high bits into singular pending balance +pub fn combine_balances(balance_lo: u64, balance_hi: u64) -> Option { balance_hi .checked_shl(PENDING_BALANCE_LO_BIT_LENGTH)? .checked_add(balance_lo) diff --git a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs index 85f9fd0b031..28aad3ee9a3 100644 --- a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs +++ b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs @@ -106,6 +106,33 @@ pub(crate) fn transfer_amount_destination_ciphertext( ElGamalCiphertext(destination_ciphertext_bytes) } +/// Extract the transfer amount ciphertext encrypted under the auditor 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 auditor consists of the `commitment` component +/// and the `decryption handle` component with respect to the auditor. +pub fn transfer_amount_auditor_ciphertext( + transfer_amount_ciphertext: &TransferAmountCiphertext, +) -> ElGamalCiphertext { + let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); + + let mut auditor_ciphertext_bytes = [0u8; 64]; + auditor_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); + auditor_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[96..128]); + + ElGamalCiphertext(auditor_ciphertext_bytes) +} + /// Extract the fee amount ciphertext encrypted under the destination ElGamal /// public key. /// diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index ac73574f5a9..82fa702fe35 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -1,8 +1,11 @@ #[cfg(not(target_os = "solana"))] +use crate::{proof::ProofLocation, solana_program::sysvar}; +#[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, }; + #[cfg(feature = "serde-traits")] use { crate::serialization::aeciphertext_fromstr, @@ -13,7 +16,6 @@ use { check_program_account, extension::confidential_transfer::{ciphertext_extraction::SourceDecryptHandles, *}, instruction::{encode_instruction, TokenInstruction}, - proof::ProofLocation, }, bytemuck::Zeroable, // `Pod` comes from zk_token_proof_instruction num_enum::{IntoPrimitive, TryFromPrimitive}, @@ -21,7 +23,6 @@ use { instruction::{AccountMeta, Instruction}, program_error::ProgramError, pubkey::Pubkey, - sysvar, }, }; @@ -665,6 +666,7 @@ pub fn initialize_mint( } /// Create a `UpdateMint` instruction +#[cfg(not(target_os = "solana"))] pub fn update_mint( token_program_id: &Pubkey, mint: &Pubkey, @@ -697,6 +699,7 @@ pub fn update_mint( /// /// This instruction is suitable for use with a cross-program `invoke` #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn inner_configure_account( token_program_id: &Pubkey, token_account: &Pubkey, @@ -749,6 +752,7 @@ pub fn inner_configure_account( /// Create a `ConfigureAccount` instruction #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn configure_account( token_program_id: &Pubkey, token_account: &Pubkey, @@ -788,6 +792,7 @@ pub fn configure_account( } /// Create an `ApproveAccount` instruction +#[cfg(not(target_os = "solana"))] pub fn approve_account( token_program_id: &Pubkey, account_to_approve: &Pubkey, @@ -816,6 +821,7 @@ pub fn approve_account( /// Create an inner `EmptyAccount` instruction /// /// This instruction is suitable for use with a cross-program `invoke` +#[cfg(not(target_os = "solana"))] pub fn inner_empty_account( token_program_id: &Pubkey, token_account: &Pubkey, @@ -858,6 +864,7 @@ pub fn inner_empty_account( } /// Create a `EmptyAccount` instruction +#[cfg(not(target_os = "solana"))] pub fn empty_account( token_program_id: &Pubkey, token_account: &Pubkey, @@ -892,6 +899,7 @@ pub fn empty_account( /// Create a `Deposit` instruction #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn deposit( token_program_id: &Pubkey, token_account: &Pubkey, @@ -928,6 +936,7 @@ pub fn deposit( /// /// This instruction is suitable for use with a cross-program `invoke` #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn inner_withdraw( token_program_id: &Pubkey, token_account: &Pubkey, @@ -981,6 +990,7 @@ pub fn inner_withdraw( /// Create a `Withdraw` instruction #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn withdraw( token_program_id: &Pubkey, token_account: &Pubkey, @@ -1025,6 +1035,7 @@ pub fn withdraw( /// /// This instruction is suitable for use with a cross-program `invoke` #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn inner_transfer( token_program_id: &Pubkey, source_token_account: &Pubkey, @@ -1076,6 +1087,7 @@ pub fn inner_transfer( /// Create a `Transfer` instruction with regular (no-fee) proof #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn transfer( token_program_id: &Pubkey, source_token_account: &Pubkey, @@ -1118,6 +1130,7 @@ pub fn transfer( /// /// This instruction is suitable for use with a cross-program `invoke` #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn inner_transfer_with_fee( token_program_id: &Pubkey, source_token_account: &Pubkey, @@ -1169,6 +1182,7 @@ pub fn inner_transfer_with_fee( /// Create a `Transfer` instruction with fee proof #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn transfer_with_fee( token_program_id: &Pubkey, source_token_account: &Pubkey, @@ -1210,9 +1224,11 @@ pub fn transfer_with_fee( /// Create a inner `ApplyPendingBalance` instruction /// /// This instruction is suitable for use with a cross-program `invoke` +#[cfg(not(target_os = "solana"))] pub fn inner_apply_pending_balance( token_program_id: &Pubkey, token_account: &Pubkey, + mint_address: &Pubkey, expected_pending_balance_credit_counter: u64, new_decryptable_available_balance: DecryptableBalance, authority: &Pubkey, @@ -1221,6 +1237,7 @@ pub fn inner_apply_pending_balance( check_program_account(token_program_id)?; let mut accounts = vec![ AccountMeta::new(*token_account, false), + AccountMeta::new_readonly(*mint_address, false), AccountMeta::new_readonly(*authority, multisig_signers.is_empty()), ]; @@ -1241,9 +1258,11 @@ pub fn inner_apply_pending_balance( } /// Create a `ApplyPendingBalance` instruction +#[cfg(not(target_os = "solana"))] pub fn apply_pending_balance( token_program_id: &Pubkey, token_account: &Pubkey, + mint_address: &Pubkey, pending_balance_instructions: u64, new_decryptable_available_balance: AeCiphertext, authority: &Pubkey, @@ -1252,6 +1271,7 @@ pub fn apply_pending_balance( inner_apply_pending_balance( token_program_id, token_account, + mint_address, pending_balance_instructions, new_decryptable_available_balance.into(), authority, @@ -1259,6 +1279,7 @@ pub fn apply_pending_balance( ) // calls check_program_account } +#[cfg(not(target_os = "solana"))] fn enable_or_disable_balance_credits( instruction: ConfidentialTransferInstruction, token_program_id: &Pubkey, @@ -1286,6 +1307,7 @@ fn enable_or_disable_balance_credits( } /// Create a `EnableConfidentialCredits` instruction +#[cfg(not(target_os = "solana"))] pub fn enable_confidential_credits( token_program_id: &Pubkey, token_account: &Pubkey, @@ -1302,6 +1324,7 @@ pub fn enable_confidential_credits( } /// Create a `DisableConfidentialCredits` instruction +#[cfg(not(target_os = "solana"))] pub fn disable_confidential_credits( token_program_id: &Pubkey, token_account: &Pubkey, @@ -1318,6 +1341,7 @@ pub fn disable_confidential_credits( } /// Create a `EnableNonConfidentialCredits` instruction +#[cfg(not(target_os = "solana"))] pub fn enable_non_confidential_credits( token_program_id: &Pubkey, token_account: &Pubkey, @@ -1334,6 +1358,7 @@ pub fn enable_non_confidential_credits( } /// Create a `DisableNonConfidentialCredits` instruction +#[cfg(not(target_os = "solana"))] pub fn disable_non_confidential_credits( token_program_id: &Pubkey, token_account: &Pubkey, @@ -1351,6 +1376,7 @@ pub fn disable_non_confidential_credits( /// Create a `TransferWithSplitProof` instruction without fee #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn transfer_with_split_proofs( token_program_id: &Pubkey, source_token_account: &Pubkey, @@ -1429,6 +1455,7 @@ pub fn transfer_with_split_proofs( /// Create a `TransferWithSplitProof` instruction with fee #[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] pub fn transfer_with_fee_and_split_proofs( token_program_id: &Pubkey, source_token_account: &Pubkey, diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 21fed2b3682..1cdec443b71 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -3,6 +3,7 @@ use crate::extension::transfer_hook; #[cfg(feature = "zk-ops")] use { + crate::extension::confidential_permanent_delegate::expected_authority, crate::extension::non_transferable::NonTransferableAccount, solana_zk_token_sdk::zk_token_elgamal::ops as syscall, }; @@ -11,6 +12,7 @@ use { check_program_account, error::TokenError, extension::{ + confidential_permanent_delegate::ConfidentialPermanentDelegate, confidential_transfer::{ciphertext_extraction::*, instruction::*, verify_proof::*, *}, confidential_transfer_fee::{ ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, @@ -135,7 +137,15 @@ fn process_configure_account( // `ConfidentialTransferAccount` extension let confidential_transfer_account = token_account.init_extension::(false)?; - confidential_transfer_account.approved = confidential_transfer_mint.auto_approve_new_accounts; + let approved = if mint + .get_extension::() + .is_ok() + { + false.into() + } else { + confidential_transfer_mint.auto_approve_new_accounts + }; + confidential_transfer_account.approved = approved; confidential_transfer_account.elgamal_pubkey = proof_context.pubkey; confidential_transfer_account.maximum_pending_balance_credit_counter = *maximum_pending_balance_credit_counter; @@ -182,6 +192,10 @@ fn process_approve_account(accounts: &[AccountInfo]) -> ProgramResult { let mint_data = &mint_info.data.borrow_mut(); let mint = PodStateWithExtensions::::unpack(mint_data)?; let confidential_transfer_mint = mint.get_extension::()?; + if let Ok(_ext) = mint.get_extension::() { + msg!("account approval for mints with the confidential permanent delegate extension have to be done via that extension"); + return Err(TokenError::InvalidInstruction.into()); + } let maybe_confidential_transfer_mint_authority: Option = confidential_transfer_mint.authority.into(); let confidential_transfer_mint_authority = @@ -514,6 +528,7 @@ fn process_transfer( program_id, source_account_info, mint_info, + &mint, authority_info, account_info_iter.as_slice(), maybe_proof_context.as_ref(), @@ -618,9 +633,7 @@ fn process_transfer( )?; if maybe_proof_context.is_none() { - msg!( - "Context state not fully initialized: returning with no op; transfer is NOT yet executed" - ); + msg!("Context state not fully initialized: returning with no op; transfer is NOT yet executed"); } authority_info }; @@ -664,11 +677,14 @@ fn process_transfer( Ok(()) } +/// Processes the changes for the sending party of a confidential transfer +#[allow(clippy::too_many_arguments)] #[cfg(feature = "zk-ops")] fn process_source_for_transfer( program_id: &Pubkey, source_account_info: &AccountInfo, mint_info: &AccountInfo, + mint: &PodStateWithExtensions<'_, PodMint>, authority_info: &AccountInfo, signers: &[AccountInfo], maybe_proof_context: Option<&TransferProofContextInfo>, @@ -687,7 +703,7 @@ fn process_source_for_transfer( Processor::validate_owner( program_id, - &token_account.base.owner, + &expected_authority(mint, authority_info, &token_account), authority_info, authority_info_data_len, signers, @@ -982,16 +998,19 @@ fn process_apply_pending_balance( ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let token_account_info = next_account_info(account_info_iter)?; + let mint_info = next_account_info(account_info_iter)?; let authority_info = next_account_info(account_info_iter)?; let authority_info_data_len = authority_info.data_len(); check_program_account(token_account_info.owner)?; let token_account_data = &mut token_account_info.data.borrow_mut(); let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; + let mint_data = mint_info.data.borrow(); + let mint = PodStateWithExtensions::::unpack(&mint_data)?; Processor::validate_owner( program_id, - &token_account.base.owner, + &expected_authority(&mint, authority_info, &token_account), authority_info, authority_info_data_len, account_info_iter.as_slice(), 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 index 9d376bb0883..40593d48aa0 100644 --- a/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs +++ b/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs @@ -26,6 +26,7 @@ use crate::{ }; /// The main logic to create the three split proof data for a transfer. +#[allow(clippy::type_complexity)] pub fn transfer_split_proof_data( current_available_balance: &ElGamalCiphertext, current_decryptable_available_balance: &AeCiphertext, diff --git a/token/program-2022/src/extension/mod.rs b/token/program-2022/src/extension/mod.rs index 3fefd993ca0..2dc0268fed6 100644 --- a/token/program-2022/src/extension/mod.rs +++ b/token/program-2022/src/extension/mod.rs @@ -6,6 +6,7 @@ use { crate::{ error::TokenError, extension::{ + confidential_permanent_delegate::ConfidentialPermanentDelegate, confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, confidential_transfer_fee::{ ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, @@ -84,6 +85,9 @@ pub mod transfer_fee; /// Transfer Hook extension pub mod transfer_hook; +/// Confidential permanent delegate extension +pub mod confidential_permanent_delegate; + /// Length in TLV structure #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] #[repr(transparent)] @@ -1101,6 +1105,11 @@ pub enum ExtensionType { GroupMemberPointer, /// Mint contains token group member configurations TokenGroupMember, + + /// Mint with confidential transfers and a permanent delegate for the + /// confidential balances + ConfidentialPermanentDelegate = u16::MAX - 4268, + /// Test variable-length mint extension #[cfg(test)] VariableLenMintTest = u16::MAX - 2, @@ -1181,6 +1190,9 @@ impl ExtensionType { ExtensionType::TokenGroup => pod_get_packed_len::(), ExtensionType::GroupMemberPointer => pod_get_packed_len::(), ExtensionType::TokenGroupMember => pod_get_packed_len::(), + ExtensionType::ConfidentialPermanentDelegate => { + pod_get_packed_len::() + } #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -1244,6 +1256,7 @@ impl ExtensionType { | ExtensionType::GroupPointer | ExtensionType::TokenGroup | ExtensionType::GroupMemberPointer + | ExtensionType::ConfidentialPermanentDelegate | ExtensionType::TokenGroupMember => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount diff --git a/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs index 9b2cb55109b..34679f71586 100644 --- a/token/program-2022/src/instruction.rs +++ b/token/program-2022/src/instruction.rs @@ -708,6 +708,9 @@ pub enum TokenInstruction<'a> { /// for further details about the extended instructions that share this /// instruction prefix GroupMemberPointerExtension, + /// Instruction prefix for instructions to the + /// confidential-permanent-delegate extension + ConfidentialPermanentDelegateExtension, } impl<'a> TokenInstruction<'a> { /// Unpacks a byte buffer into a @@ -847,6 +850,7 @@ impl<'a> TokenInstruction<'a> { 39 => Self::MetadataPointerExtension, 40 => Self::GroupPointerExtension, 41 => Self::GroupMemberPointerExtension, + 42 => Self::ConfidentialPermanentDelegateExtension, _ => return Err(TokenError::InvalidInstruction.into()), }) } @@ -1018,6 +1022,9 @@ impl<'a> TokenInstruction<'a> { &Self::GroupMemberPointerExtension => { buf.push(41); } + &Self::ConfidentialPermanentDelegateExtension => { + buf.push(42); + } }; buf } diff --git a/token/program-2022/src/pod_instruction.rs b/token/program-2022/src/pod_instruction.rs index dcf487c966d..aba8a839288 100644 --- a/token/program-2022/src/pod_instruction.rs +++ b/token/program-2022/src/pod_instruction.rs @@ -114,6 +114,7 @@ pub(crate) enum PodTokenInstruction { // 40 GroupPointerExtension, GroupMemberPointerExtension, + ConfidentialPermanentDelegateExtension, } fn unpack_pubkey_option(input: &[u8]) -> Result, ProgramError> { diff --git a/token/program-2022/src/processor.rs b/token/program-2022/src/processor.rs index 469b29f7f6a..27bf016df92 100644 --- a/token/program-2022/src/processor.rs +++ b/token/program-2022/src/processor.rs @@ -5,6 +5,7 @@ use { check_program_account, cmp_pubkeys, error::TokenError, extension::{ + confidential_permanent_delegate, confidential_transfer::{self, ConfidentialTransferAccount, ConfidentialTransferMint}, confidential_transfer_fee::{ self, ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, @@ -319,6 +320,7 @@ impl Processor { { return Err(TokenError::NonTransferable.into()); } + let (fee, maybe_permanent_delegate, maybe_transfer_hook_program_id) = if let Some((mint_info, expected_decimals)) = expected_mint_info { if !cmp_pubkeys(&source_account.base.mint, mint_info.key) { @@ -366,9 +368,9 @@ impl Processor { .is_ok() { return Err(TokenError::MintRequiredForTransfer.into()); - } else { - (0, None, None) } + + (0, None, None) }; if let Some(expected_fee) = expected_fee { if expected_fee != fee { @@ -1787,6 +1789,14 @@ impl Processor { &input[1..], ) } + PodTokenInstruction::ConfidentialPermanentDelegateExtension => { + msg!("Instruction: ConfidentialPermanentDelegateExtension"); + confidential_permanent_delegate::processor::process_instruction( + program_id, + accounts, + &input[1..], + ) + } } } else if let Ok(instruction) = TokenMetadataInstruction::unpack(input) { token_metadata::processor::process_instruction(program_id, accounts, instruction)