diff --git a/token/cli/src/clap_app.rs b/token/cli/src/clap_app.rs index 814c1c18c67..48fc2d65de4 100644 --- a/token/cli/src/clap_app.rs +++ b/token/cli/src/clap_app.rs @@ -168,6 +168,11 @@ pub enum CommandName { ApplyPendingBalance, UpdateGroupAddress, UpdateMemberAddress, + MintConfidentialTokens, + BurnConfidentialTokens, + ConfidentialBalance, + ConfidentialSupply, + RotateSupplyElgamal, } impl fmt::Display for CommandName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -890,6 +895,42 @@ pub fn app<'a>( .takes_value(false) .help("Enables group member configurations in the mint. The mint authority must initialize the member."), ) + .arg( + Arg::with_name("enable_confidential_mint_burn") + .long("enable-confidential-mint-burn") + .takes_value(false) + .help( + "Enables minting of new tokens into confidential balance and burning of tokens directly from the confidential balance" + ), + ) + .arg( + Arg::with_name("auditor_pubkey") + .long("auditor-pubkey") + .value_name("AUDITOR_PUBKEY") + .takes_value(true) + .help( + "The auditor encryption public key for mints with the confidential \ + transfer extension enabled. The corresponding private key for \ + this auditor public key can be used to decrypt all confidential \ + transfers involving tokens from this mint. Currently, the auditor \ + public key can only be specified as a direct *base64* encoding of \ + an ElGamal public key. More methods of specifying the auditor public \ + key will be supported in a future version. To disable auditability \ + feature for the token, use \"none\"." + ) + ) + .arg( + Arg::with_name("confidential_supply_pubkey") + .long("confidential-supply-pubkey") + .value_name("CONFIDENTIAL_SUPPLY_PUBKEY") + .takes_value(true) + .help( + "The confidential supply encryption public key for mints with the \ + confidential transfer and confidential mint-burn extension enabled. \ + The corresponding private key for this supply public key can be \ + used to decrypt the confidential supply of the token." + ) + ) .arg(multisig_signer_arg()) .nonce_args(true) .arg(memo_arg()) @@ -2688,4 +2729,219 @@ pub fn app<'a>( .arg(multisig_signer_arg()) .nonce_args(true) ) + .subcommand( + SubCommand::with_name(CommandName::MintConfidentialTokens.into()) + .about("Mint tokens amounts for into confidential balance") + .arg( + Arg::with_name("token") + .long("token") + .validator(|s| is_valid_pubkey(s)) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("amount") + .value_parser(Amount::parse) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(2) + .required(true) + .help("Amount to deposit; accepts keyword ALL"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(|s| is_valid_pubkey(s)) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .help("The address of the token account to configure confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::BurnConfidentialTokens.into()) + .about("Burn tokens from available confidential balance") + .arg( + Arg::with_name("token") + .long("token") + .validator(|s| is_valid_pubkey(s)) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("amount") + .value_parser(Amount::parse) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(2) + .required(true) + .help("Amount to deposit; accepts keyword ALL"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(|s| is_valid_pubkey(s)) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .help("The address of the token account to configure confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::ConfidentialBalance.into()) + .about("Display confidential balance") + .arg( + Arg::with_name("token") + .long("token") + .validator(|s| is_valid_pubkey(s)) + .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(|s| is_valid_pubkey(s)) + .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(|s| is_valid_signer(s)) + .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::ConfidentialSupply.into()) + .about("Display supply of confidential token") + .arg( + Arg::with_name("token") + .long("token") + .validator(|s| is_valid_pubkey(s)) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("authority") + .long("authority") + .alias("owner") + .validator(|s| is_valid_signer(s)) + .value_name("SIGNER") + .takes_value(true) + .help("Keypair from which the supply elgamal keypair is derived. \ + Either the authority or the confidential-supply-keypair have \ + to be specified in order for the supply to be decrypted.") + ) + .arg( + Arg::with_name("confidential_supply_keypair") + .long("confidential-supply-keypair") + .value_name("CONFIDENTIAL_SUPPLY_KEYPAIR") + .takes_value(true) + .help( + "The confidential supply encryption keypair used to decrypt ElGamalCiphertext supply. \ + Either the authority or the confidential-supply-keypair have \ + to be specified in order for the supply to be decrypted." + ) + ) + .arg( + Arg::with_name("confidential_supply_aes_key") + .long("confidential-supply-aes-key") + .value_name("CONFIDENTIAL_SUPPLY_AES_KEY") + .takes_value(true) + .help( + "The aes key used to decrypt the decryptable portion of the confidential supply." + ) + ) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::RotateSupplyElgamal.into()) + .about("Display supply of confidential token") + .arg( + Arg::with_name("token") + .long("token") + .validator(|s| is_valid_pubkey(s)) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("authority") + .long("authority") + .alias("owner") + .validator(|s| is_valid_signer(s)) + .value_name("SIGNER") + .takes_value(true) + .required(true) + .help("Keypair holding the authority over the confidential-mint-burn extension.") + ) + .arg( + Arg::with_name("current_supply_keypair") + .long("current-supply-keypair") + .value_name("CURRENT_SUPPLY_KEYPAIR") + .takes_value(true) + .required(true) + .help( + "The current confidential supply encryption keypair." + ) + ) + .arg( + Arg::with_name("supply_aes_key") + .long("supply-aes-key") + .value_name("SUPPLY_AES_KEY") + .takes_value(true) + .required(true) + .help( + "The aes key to decrypt the decryptable confidential supply." + ) + ) + .arg( + Arg::with_name("new_supply_keypair") + .long("new-supply-keypair") + .value_name("NEW_SUPPLY_KEYPAIR") + .takes_value(true) + .required(true) + .help( + "The new confidential supply encryption keypair to rotate to." + ) + ) + .nonce_args(true) + ) } diff --git a/token/cli/src/command.rs b/token/cli/src/command.rs index 8a7cf30b5dd..16a024aebf7 100644 --- a/token/cli/src/command.rs +++ b/token/cli/src/command.rs @@ -36,7 +36,9 @@ use { }, spl_associated_token_account_client::address::get_associated_token_address_with_program_id, spl_token_2022::{ + error::TokenError, extension::{ + confidential_mint_burn::{account_info::SupplyAccountInfo, ConfidentialMintBurn}, confidential_transfer::{ account_info::{ ApplyPendingBalanceAccountInfo, TransferAccountInfo, WithdrawAccountInfo, @@ -72,7 +74,8 @@ use { }, }, spl_token_confidential_transfer_proof_generation::{ - transfer::TransferProofData, withdraw::WithdrawProofData, + burn::burn_split_proof_data, mint::mint_split_proof_data, transfer::TransferProofData, + withdraw::WithdrawProofData, }, spl_token_group_interface::state::TokenGroup, spl_token_metadata_interface::state::{Field, TokenMetadata}, @@ -259,6 +262,9 @@ async fn command_create_token( enable_group: bool, enable_member: bool, bulk_signers: Vec>, + enable_confidential_mint_burn: bool, + auditor_pubkey: ElGamalPubkeyOrNone, + confidential_supply_pubkey: ElGamalPubkeyOrNone, ) -> CommandResult { println_display( config, @@ -318,7 +324,7 @@ async fn command_create_token( extensions.push(ExtensionInitializationParams::ConfidentialTransferMint { authority: Some(authority), auto_approve_new_accounts: auto_approve, - auditor_elgamal_pubkey: None, + auditor_elgamal_pubkey: auditor_pubkey.into(), }); if transfer_fee.is_some() { // Deriving ElGamal key from default signer. Custom ElGamal keys @@ -337,6 +343,19 @@ async fn command_create_token( } } + if enable_confidential_mint_burn { + let confidential_supply_pubkey: Option = + confidential_supply_pubkey.into(); + let confidential_supply_pubkey = confidential_supply_pubkey.unwrap(); + let aes_key = AeKey::new_from_signer(config.default_signer()?.as_ref(), b"").unwrap(); + let decryptable_supply = aes_key.encrypt(0).into(); + + extensions.push(ExtensionInitializationParams::ConfidentialMintBurnMint { + confidential_supply_pubkey, + decryptable_supply, + }); + } + if let Some(program_id) = transfer_hook_program_id { extensions.push(ExtensionInitializationParams::TransferHook { authority: Some(authority), @@ -3276,10 +3295,11 @@ async fn command_enable_disable_confidential_transfers( enum ConfidentialInstructionType { Deposit, Withdraw, + Mint, } #[allow(clippy::too_many_arguments)] -async fn command_deposit_withdraw_confidential_tokens( +async fn command_deposit_withdraw_mint_confidential_tokens( config: &Config<'_>, token_pubkey: Pubkey, owner: Pubkey, @@ -3375,6 +3395,15 @@ async fn command_deposit_withdraw_confidential_tokens( ), ); } + ConfidentialInstructionType::Mint => { + println_display( + config, + format!( + "Minting {} confidential tokens", + spl_token::amount_to_ui_amount(amount, mint_info.decimals) + ), + ); + } } let res = match instruction_type { @@ -3469,6 +3498,142 @@ async fn command_deposit_withdraw_confidential_tokens( withdraw_result } + ConfidentialInstructionType::Mint => { + let payer = config.fee_payer()?; + + let equality_proof_context_state_account = Keypair::new(); + let equality_proof_context_pubkey = equality_proof_context_state_account.pubkey(); + let ciphertext_validity_proof_context_state_account = Keypair::new(); + let ciphertext_validity_proof_context_pubkey = + ciphertext_validity_proof_context_state_account.pubkey(); + let range_proof_context_state_account = Keypair::new(); + let range_proof_context_pubkey = range_proof_context_state_account.pubkey(); + + let mint_to_elgamal_pubkey = + token.account_elgamal_pubkey(&token_account_address).await?; + let auditor_elgamal_pubkey = token.auditor_elgamal_pubkey().await?; + let supply_elgamal_pubkey = token.supply_elgamal_pubkey().await?; + + let dummy_elgamal = ElGamalKeypair::new_rand(); + let dummy_aes = AeKey::new_rand(); + let supply_elgamal_keypair = match elgamal_keypair { + Some(e) => e, + None => { + // if no ElGamalKeypair supplied just use a dummy for + // proof creation + &dummy_elgamal + } + }; + let supply_aes_key = match aes_key { + Some(e) => e, + None => { + // see above + &dummy_aes + } + }; + + let mint = token.get_mint_info().await?; + let mint_burn_extension = mint.get_extension::()?; + let supply_account_info = SupplyAccountInfo::new(mint_burn_extension); + + let proof_data = mint_split_proof_data( + &mint_burn_extension + .confidential_supply + .try_into() + .map_err(|_| TokenError::MalformedCiphertext)?, + amount, + supply_account_info + .decrypt_current_supply(supply_aes_key, supply_elgamal_keypair)?, + supply_elgamal_keypair, + supply_aes_key, + &mint_to_elgamal_pubkey, + &auditor_elgamal_pubkey.unwrap_or_default(), + )?; + + let equality_proof_signer = &[&equality_proof_context_state_account]; + let ciphertext_validity_proof_signer = + &[&ciphertext_validity_proof_context_state_account]; + let range_proof_signer = &[&range_proof_context_state_account]; + let context_state_auth = payer.pubkey(); + let _ = try_join!( + token.confidential_transfer_create_context_state_account( + &equality_proof_context_pubkey, + &context_state_auth, + &proof_data.equality_proof_data, + true, + equality_proof_signer, + ), + token.confidential_transfer_create_context_state_account( + &ciphertext_validity_proof_context_pubkey, + &context_state_auth, + &proof_data + .ciphertext_validity_proof_data_with_ciphertext + .proof_data, + false, + ciphertext_validity_proof_signer, + ), + token.confidential_transfer_create_context_state_account( + &range_proof_context_pubkey, + &context_state_auth, + &proof_data.range_proof_data, + true, + range_proof_signer, + ), + )?; + + let equality_proof_location = + ProofAccount::ContextAccount(equality_proof_context_pubkey); + let ciphertext_validity_proof_location = + ProofAccount::ContextAccount(ciphertext_validity_proof_context_pubkey); + let ciphertext_validity_proof_location = ProofAccountWithCiphertext { + proof_account: ciphertext_validity_proof_location, + ciphertext_lo: proof_data + .ciphertext_validity_proof_data_with_ciphertext + .ciphertext_lo, + ciphertext_hi: proof_data + .ciphertext_validity_proof_data_with_ciphertext + .ciphertext_hi, + }; + let range_proof_location = ProofAccount::ContextAccount(range_proof_context_pubkey); + + let res = token + .confidential_mint( + &token_account_address, + &owner, + supply_elgamal_pubkey, + Some(&equality_proof_location), + Some(&ciphertext_validity_proof_location), + Some(&range_proof_location), + proof_data.new_decryptable_supply, + &bulk_signers, + ) + .await?; + + let close_context_auth = payer.pubkey(); + let close_context_state_signers = &[payer]; + let _ = try_join!( + token.confidential_transfer_close_context_state_account( + &equality_proof_context_pubkey, + &close_context_auth, + &close_context_auth, + close_context_state_signers, + ), + token.confidential_transfer_close_context_state_account( + &ciphertext_validity_proof_context_pubkey, + &close_context_auth, + &close_context_auth, + close_context_state_signers, + ), + token.confidential_transfer_close_context_state_account( + &range_proof_context_pubkey, + &close_context_auth, + &close_context_auth, + close_context_state_signers, + ), + )?; + + res + } }; let tx_return = finish_tx(config, &res, false).await?; @@ -3611,6 +3776,11 @@ pub async fn process_command<'a>( .value_of("enable_confidential_transfers") .map(|b| b == "auto"); + let auditor_elgamal_pubkey = + elgamal_pubkey_or_none(arg_matches, "auditor_pubkey").unwrap(); + let confidential_supply_pubkey = + elgamal_pubkey_or_none(arg_matches, "confidential_supply_pubkey").unwrap(); + command_create_token( config, decimals, @@ -3633,6 +3803,9 @@ 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_mint_burn"), + auditor_elgamal_pubkey, + confidential_supply_pubkey, ) .await } @@ -4591,7 +4764,43 @@ pub async fn process_command<'a>( ) .await } - (c @ CommandName::DepositConfidentialTokens, arg_matches) + (CommandName::BurnConfidentialTokens, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let ui_amount = match arg_matches.value_of("amount").unwrap() { + "ALL" => None, + amount => Some(amount.parse::().unwrap()), + }; + let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) + .unwrap() + .unwrap(); + + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + + let mint_decimals = ArgMatches::get_one(arg_matches, MINT_DECIMALS_ARG.name); + + let elgamal_keypair = ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); + let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); + + let bulk_signers = vec![owner_signer]; + + command_confidential_burn( + config, + token, + account, + ui_amount, + owner, + mint_decimals, + bulk_signers, + &elgamal_keypair, + &aes_key, + ) + .await + } + (c @ CommandName::MintConfidentialTokens, arg_matches) + | (c @ CommandName::DepositConfidentialTokens, arg_matches) | (c @ CommandName::WithdrawConfidentialTokens, arg_matches) => { let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) .unwrap() @@ -4607,6 +4816,9 @@ pub async fn process_command<'a>( CommandName::DepositConfidentialTokens => { (ConfidentialInstructionType::Deposit, None, None) } + CommandName::MintConfidentialTokens => { + (ConfidentialInstructionType::Mint, None, None) + } CommandName::WithdrawConfidentialTokens => { // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be // supported in the future once upgrading to clap-v3. @@ -4630,7 +4842,7 @@ pub async fn process_command<'a>( push_signer_with_dedup(owner_signer, &mut bulk_signers); } - command_deposit_withdraw_confidential_tokens( + command_deposit_withdraw_mint_confidential_tokens( config, token, owner, @@ -4675,7 +4887,279 @@ pub async fn process_command<'a>( ) .await } + (CommandName::ConfidentialBalance, arg_matches) => { + let token = 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 (auth_signer, _auth) = + config.signer_or_default(arg_matches, "authority", &mut wallet_manager); + command_confidential_balance(config, token, address, auth_signer).await + } + (CommandName::ConfidentialSupply, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let elgamal_keypair = if arg_matches.is_present("confidential_supply_keypair") { + elgamal_keypair_of(arg_matches, "confidential_supply_keypair").unwrap() + } else { + let (auth_signer, _auth) = + config.signer_or_default(arg_matches, "authority", &mut wallet_manager); + + ElGamalKeypair::new_from_signer(&*auth_signer, b"").unwrap() + }; + let aes_key = if arg_matches.is_present("confidential_supply_aes_key") { + aes_key_of(arg_matches, "confidential_supply_aes_key").unwrap() + } else { + let (auth_signer, _auth) = + config.signer_or_default(arg_matches, "authority", &mut wallet_manager); + + AeKey::new_from_signer(&*auth_signer, b"").unwrap() + }; + + let token_cl = token_client_from_config(config, &token, None)?; + let supply = token_cl + .confidential_supply(&elgamal_keypair, &aes_key) + .await + .map_err(|e| format!("Could not fetch confidential supply for {token}: {e}",))?; + + Ok(format!("Supply of {token} is {supply}")) + } + (CommandName::RotateSupplyElgamal, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let supply_elgamal_keypair = + elgamal_keypair_of(arg_matches, "current_supply_keypair").unwrap(); + let supply_aes_key = aes_key_of(arg_matches, "supply_aes_key").unwrap(); + let new_supply_elgamal_keypair = + elgamal_keypair_of(arg_matches, "new_supply_keypair").unwrap(); + let (auth_signer, auth) = + config.signer_or_default(arg_matches, "authority", &mut wallet_manager); + + let token = &token_client_from_config(config, &token, None)?; + let mint = token.get_mint_info().await?; + let mint_burn_extension = mint.get_extension::()?; + let supply_account_info = SupplyAccountInfo::new(mint_burn_extension); + + let proof_data = supply_account_info.generate_rotate_supply_elgamal_pubkey_proof( + &supply_aes_key, + &supply_elgamal_keypair, + &new_supply_elgamal_keypair, + )?; + + Ok( + match finish_tx( + config, + &token + .rotate_supply_elgamal( + &auth, + &new_supply_elgamal_keypair, + &[&auth_signer], + proof_data, + ) + .await?, + false, + ) + .await? + { + 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_confidential_balance( + config: &Config<'_>, + token: Pubkey, + address: Pubkey, + authority_signer: Arc, +) -> CommandResult { + let elgamal_keypair = ElGamalKeypair::new_from_signer(&*authority_signer, b"").unwrap(); + let aes_key = AeKey::new_from_signer(&*authority_signer, b"").unwrap(); + + let token = token_client_from_config(config, &token, None)?; + + let (available_balance, pending_balance) = token + .confidential_balance(&address, &elgamal_keypair, &aes_key) + .await + .unwrap(); + + Ok(format!("{address} has a pending balance of {pending_balance} and an available balance of {available_balance}") + ) +} + +#[allow(clippy::too_many_arguments)] +async fn command_confidential_burn( + config: &Config<'_>, + token_pubkey: Pubkey, + ata_pubkey: Pubkey, + ui_amount: Option, + authority: Pubkey, + mint_decimals: Option<&u8>, + bulk_signers: BulkSigners, + elgamal_keypair: &ElGamalKeypair, + aes_key: &AeKey, +) -> CommandResult { + let mint_info = config + .get_mint_info(&token_pubkey, mint_decimals.copied()) + .await?; + + // if the user got the decimals wrong, they may well have calculated the + // transfer amount wrong we only check in online mode, because in offline, + // mint_info.decimals is always 9 + if !config.sign_only && mint_decimals.is_some() && mint_decimals != Some(&mint_info.decimals) { + return Err(format!( + "Decimals {} was provided, but actual value is {}", + mint_decimals.unwrap(), + mint_info.decimals + ) + .into()); } + + let token = token_client_from_config(config, &token_pubkey, Some(mint_info.decimals))?; + + // the amount the user wants to tranfer, as a f64 + let burn_amount = ui_amount + .map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals)) + .unwrap(); + + let auditor_elgamal_pubkey = token.auditor_elgamal_pubkey().await?; + let supply_elgamal_pubkey = token.supply_elgamal_pubkey().await?; + + let context_state_authority = config.fee_payer()?; + let equality_proof_context_state_account = Keypair::new(); + let equality_proof_context_pubkey = equality_proof_context_state_account.pubkey(); + let ciphertext_validity_proof_context_state_account = Keypair::new(); + let ciphertext_validity_proof_context_pubkey = + ciphertext_validity_proof_context_state_account.pubkey(); + let range_proof_context_state_account = Keypair::new(); + let range_proof_context_pubkey = range_proof_context_state_account.pubkey(); + + let state = token.get_account_info(&ata_pubkey).await.unwrap(); + let extension = state + .get_extension::() + .unwrap(); + let transfer_account_info = TransferAccountInfo::new(extension); + + let proof_data = burn_split_proof_data( + &transfer_account_info + .available_balance + .try_into() + .map_err(|_| TokenError::MalformedCiphertext)?, + &transfer_account_info + .decryptable_available_balance + .try_into() + .map_err(|_| TokenError::MalformedCiphertext)?, + burn_amount, + elgamal_keypair, + aes_key, + &auditor_elgamal_pubkey.unwrap_or_default(), + &supply_elgamal_pubkey.unwrap_or_default(), + ) + .unwrap(); + + let range_proof_signer = &[&range_proof_context_state_account]; + let equality_proof_signer = &[&equality_proof_context_state_account]; + let ciphertext_validity_proof_signer = &[&ciphertext_validity_proof_context_state_account]; + let context_state_auth_pubkey = context_state_authority.pubkey(); + // setup proofs + let _ = try_join!( + token.confidential_transfer_create_context_state_account( + &equality_proof_context_pubkey, + &context_state_auth_pubkey, + &proof_data.equality_proof_data, + true, + equality_proof_signer, + ), + token.confidential_transfer_create_context_state_account( + &ciphertext_validity_proof_context_pubkey, + &context_state_auth_pubkey, + &proof_data + .ciphertext_validity_proof_data_with_ciphertext + .proof_data, + false, + ciphertext_validity_proof_signer, + ), + token.confidential_transfer_create_context_state_account( + &range_proof_context_pubkey, + &context_state_auth_pubkey, + &proof_data.range_proof_data, + true, + range_proof_signer, + ), + )?; + + let equality_proof_location = ProofAccount::ContextAccount(equality_proof_context_pubkey); + let ciphertext_validity_proof_location = + ProofAccount::ContextAccount(ciphertext_validity_proof_context_pubkey); + let ciphertext_validity_proof_location = ProofAccountWithCiphertext { + proof_account: ciphertext_validity_proof_location, + ciphertext_lo: proof_data + .ciphertext_validity_proof_data_with_ciphertext + .ciphertext_lo, + ciphertext_hi: proof_data + .ciphertext_validity_proof_data_with_ciphertext + .ciphertext_hi, + }; + let range_proof_location = ProofAccount::ContextAccount(range_proof_context_pubkey); + + // do the burn + let res = token + .confidential_burn( + &ata_pubkey, + &authority, + Some(&equality_proof_location), + Some(&ciphertext_validity_proof_location), + Some(&range_proof_location), + burn_amount, + supply_elgamal_pubkey, + aes_key, + &bulk_signers, + ) + .await?; + + // close context state accounts + let context_state_authority_pubkey = context_state_authority.pubkey(); + let close_context_state_signers = &[context_state_authority]; + let _ = try_join!( + token.confidential_transfer_close_context_state_account( + &equality_proof_context_pubkey, + &authority, + &context_state_authority_pubkey, + close_context_state_signers, + ), + token.confidential_transfer_close_context_state_account( + &ciphertext_validity_proof_context_pubkey, + &authority, + &context_state_authority_pubkey, + close_context_state_signers, + ), + token.confidential_transfer_close_context_state_account( + &range_proof_context_pubkey, + &authority, + &context_state_authority_pubkey, + close_context_state_signers, + ), + )?; + + 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) + } + }) } fn format_output(command_output: T, command_name: &CommandName, config: &Config) -> String diff --git a/token/cli/src/encryption_keypair.rs b/token/cli/src/encryption_keypair.rs index 4873d117512..b100ea7a303 100644 --- a/token/cli/src/encryption_keypair.rs +++ b/token/cli/src/encryption_keypair.rs @@ -5,7 +5,9 @@ use { base64::{prelude::BASE64_STANDARD, Engine}, clap::ArgMatches, + solana_sdk::signer::EncodableKey, spl_token_2022::solana_zk_sdk::encryption::{ + auth_encryption::AeKey, elgamal::{ElGamalKeypair, ElGamalPubkey}, pod::elgamal::PodElGamalPubkey, }, @@ -32,6 +34,9 @@ pub(crate) fn elgamal_pubkey_or_none( matches: &ArgMatches, name: &str, ) -> Result { + if !matches.is_present(name) { + return Ok(ElGamalPubkeyOrNone::None); + } let arg_str = matches.value_of(name).unwrap(); if arg_str == "none" { return Ok(ElGamalPubkeyOrNone::None); @@ -65,6 +70,11 @@ pub(crate) fn elgamal_keypair_of( ElGamalKeypair::read_json_file(path).map_err(|e| e.to_string()) } +pub(crate) fn aes_key_of(matches: &ArgMatches, name: &str) -> Result { + let path = matches.value_of(name).unwrap(); + AeKey::read_from_file(path).map_err(|e| e.to_string()) +} + fn elgamal_pubkey_from_str(s: &str) -> Option { if s.len() > ELGAMAL_PUBKEY_MAX_BASE64_LEN { return None; diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 30fa7d40318..27fe8a4b61b 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -29,14 +29,19 @@ use { }, spl_record::state::RecordData, spl_token_2022::{ + error::TokenError as Token2022Error, extension::{ + confidential_mint_burn::{self, account_info::SupplyAccountInfo, ConfidentialMintBurn}, confidential_transfer::{ self, account_info::{ - ApplyPendingBalanceAccountInfo, EmptyAccountAccountInfo, TransferAccountInfo, - WithdrawAccountInfo, + combine_balances, ApplyPendingBalanceAccountInfo, EmptyAccountAccountInfo, + TransferAccountInfo, WithdrawAccountInfo, }, - ConfidentialTransferAccount, DecryptableBalance, + instruction::{ + CiphertextCiphertextEqualityProofData, ProofContextState, ZkProofData, + }, + ConfidentialTransferAccount, ConfidentialTransferMint, DecryptableBalance, }, confidential_transfer_fee::{ self, account_info::WithheldTokensInfo, ConfidentialTransferFeeAmount, @@ -50,15 +55,16 @@ use { instruction, offchain, solana_zk_sdk::{ encryption::{ - auth_encryption::AeKey, + auth_encryption::{AeCiphertext, AeKey}, elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey}, - pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + pod::{ + auth_encryption::PodAeCiphertext, + elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + }, }, zk_elgamal_proof_program::{ self, instruction::{close_context_state, ContextStateInfo}, - proof_data::*, - state::ProofContextState, }, }, state::{Account, AccountState, Mint, Multisig}, @@ -113,6 +119,13 @@ pub enum TokenError { MissingDecimals, #[error("decimals specified, but incorrect")] InvalidDecimals, + #[error("TokenProgramError: {0}")] + TokenProgramError(String), +} +impl From for TokenError { + fn from(e: Token2022Error) -> Self { + Self::TokenProgramError(e.to_string()) + } } impl PartialEq for TokenError { fn eq(&self, other: &Self) -> bool { @@ -196,6 +209,10 @@ pub enum ExtensionInitializationParams { PausableConfig { authority: Pubkey, }, + ConfidentialMintBurnMint { + confidential_supply_pubkey: PodElGamalPubkey, + decryptable_supply: PodAeCiphertext, + }, } impl ExtensionInitializationParams { /// Get the extension type associated with the init params @@ -217,6 +234,7 @@ impl ExtensionInitializationParams { Self::GroupMemberPointer { .. } => ExtensionType::GroupMemberPointer, Self::ScaledUiAmountConfig { .. } => ExtensionType::ScaledUiAmount, Self::PausableConfig { .. } => ExtensionType::Pausable, + Self::ConfidentialMintBurnMint { .. } => ExtensionType::ConfidentialMintBurn, } } /// Generate an appropriate initialization instruction for the given mint @@ -338,6 +356,15 @@ impl ExtensionInitializationParams { Self::PausableConfig { authority } => { pausable::instruction::initialize(token_program_id, mint, &authority) } + Self::ConfidentialMintBurnMint { + confidential_supply_pubkey, + decryptable_supply, + } => confidential_mint_burn::instruction::initialize_mint( + token_program_id, + mint, + confidential_supply_pubkey, + decryptable_supply, + ), } } } @@ -3597,6 +3624,301 @@ where )); self.process_ixs(&instructions, signing_keypairs).await } + + pub async fn auditor_elgamal_pubkey(&self) -> TokenResult> { + Ok(Into::>::into( + self.get_mint_info() + .await? + .get_extension::()? + .auditor_elgamal_pubkey, + ) + .map(|pk| TryInto::::try_into(pk).unwrap())) + } + + pub async fn account_elgamal_pubkey(&self, account: &Pubkey) -> TokenResult { + TryInto::::try_into( + self.get_account_info(account) + .await? + .get_extension::()? + .elgamal_pubkey, + ) + .map_err(|_| TokenError::Program(ProgramError::InvalidAccountData)) + } + + /// Mint SPL Tokens into the pending balance of a confidential token account + #[allow(clippy::too_many_arguments)] + pub async fn confidential_mint( + &self, + account: &Pubkey, + authority: &Pubkey, + supply_elgamal_pubkey: Option, + equality_proof_account: Option<&ProofAccount>, + ciphertext_validity_proof_account_with_ciphertext: Option<&ProofAccountWithCiphertext>, + range_proof_account: Option<&ProofAccount>, + new_decryptable_supply: AeCiphertext, + signing_keypairs: &S, + ) -> TokenResult { + let signing_pubkeys = signing_keypairs.pubkeys(); + let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); + + let (transfer_amount_auditor_ciphertext_lo, transfer_amount_auditor_ciphertext_hi) = + if let Some(proof_data_with_ciphertext) = + ciphertext_validity_proof_account_with_ciphertext + { + ( + proof_data_with_ciphertext.ciphertext_lo, + proof_data_with_ciphertext.ciphertext_hi, + ) + } else { + // unwrap is safe as long as either `proof_data_with_ciphertext`, + // `proof_account_with_ciphertext` is `Some(..)`, which is guaranteed by the + // previous check + ( + ciphertext_validity_proof_account_with_ciphertext + .unwrap() + .ciphertext_lo, + ciphertext_validity_proof_account_with_ciphertext + .unwrap() + .ciphertext_hi, + ) + }; + + if !([equality_proof_account, range_proof_account] + .iter() + .all(|proof_account| proof_account.is_some())) + { + return Err(TokenError::ProofGeneration); + } + if ciphertext_validity_proof_account_with_ciphertext.is_none() { + return Err(TokenError::ProofGeneration); + } + + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, + // which is guaranteed by the previous check + let equality_proof_location = + Self::confidential_transfer_create_proof_location(None, equality_proof_account, 1) + .unwrap(); + let ciphertext_validity_proof_location = Self::confidential_transfer_create_proof_location( + None, + ciphertext_validity_proof_account_with_ciphertext.map(|account| &account.proof_account), + 2, + ) + .unwrap(); + let range_proof_location = + Self::confidential_transfer_create_proof_location(None, range_proof_account, 3) + .unwrap(); + + self.process_ixs( + &confidential_mint_burn::instruction::confidential_mint_with_split_proofs( + &self.program_id, + account, + &self.pubkey, + supply_elgamal_pubkey, + &transfer_amount_auditor_ciphertext_lo, + &transfer_amount_auditor_ciphertext_hi, + authority, + &multisig_signers, + equality_proof_location, + ciphertext_validity_proof_location, + range_proof_location, + new_decryptable_supply, + )?, + signing_keypairs, + ) + .await + } + + /// Burn SPL Tokens from the available balance of a confidential token + /// account + #[allow(clippy::too_many_arguments)] + pub async fn confidential_burn( + &self, + ata_pubkey: &Pubkey, + authority: &Pubkey, + equality_proof_account: Option<&ProofAccount>, + ciphertext_validity_proof_account_with_ciphertext: Option<&ProofAccountWithCiphertext>, + range_proof_account: Option<&ProofAccount>, + amount: u64, + supply_elgamal_pubkey: Option, + aes_key: &AeKey, + signing_keypairs: &S, + ) -> TokenResult { + let signing_pubkeys = signing_keypairs.pubkeys(); + let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); + + let account = self.get_account_info(ata_pubkey).await?; + let confidential_transfer_account = + account.get_extension::()?; + let account_info = TransferAccountInfo::new(confidential_transfer_account); + + let new_decryptable_available_balance: PodAeCiphertext = account_info + .new_decryptable_available_balance(amount, aes_key) + .map_err(|_| TokenError::AccountDecryption)? + .into(); + + let (transfer_amount_auditor_ciphertext_lo, transfer_amount_auditor_ciphertext_hi) = + if let Some(proof_data_with_ciphertext) = + ciphertext_validity_proof_account_with_ciphertext + { + ( + proof_data_with_ciphertext.ciphertext_lo, + proof_data_with_ciphertext.ciphertext_hi, + ) + } else { + // unwrap is safe as long as either `proof_data_with_ciphertext`, + // `proof_account_with_ciphertext` is `Some(..)`, which is guaranteed by the + // previous check + ( + ciphertext_validity_proof_account_with_ciphertext + .unwrap() + .ciphertext_lo, + ciphertext_validity_proof_account_with_ciphertext + .unwrap() + .ciphertext_hi, + ) + }; + + if !([equality_proof_account, range_proof_account] + .iter() + .all(|proof_account| proof_account.is_some())) + { + return Err(TokenError::ProofGeneration); + } + if ciphertext_validity_proof_account_with_ciphertext.is_none() { + return Err(TokenError::ProofGeneration); + } + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, + // which is guaranteed by the previous check + let equality_proof_location = + Self::confidential_transfer_create_proof_location(None, equality_proof_account, 1) + .unwrap(); + let ciphertext_validity_proof_location = Self::confidential_transfer_create_proof_location( + None, + ciphertext_validity_proof_account_with_ciphertext.map(|account| &account.proof_account), + 2, + ) + .unwrap(); + let range_proof_location = + Self::confidential_transfer_create_proof_location(None, range_proof_account, 3) + .unwrap(); + + self.process_ixs( + &confidential_mint_burn::instruction::confidential_burn_with_split_proofs( + &self.program_id, + ata_pubkey, + &self.pubkey, + supply_elgamal_pubkey, + new_decryptable_available_balance, + &transfer_amount_auditor_ciphertext_lo, + &transfer_amount_auditor_ciphertext_hi, + authority, + &multisig_signers, + equality_proof_location, + ciphertext_validity_proof_location, + range_proof_location, + )?, + signing_keypairs, + ) + .await + } + + /// Fetch confidential balance for token account + #[allow(clippy::too_many_arguments)] + pub async fn confidential_balance( + &self, + token_account: &Pubkey, + elgamal_keypair: &ElGamalKeypair, + aes_key: &AeKey, + ) -> TokenResult<(u64, u64)> { + let account = self.get_account_info(token_account).await?; + let confidential_transfer_account = + account.get_extension::()?; + + let decryptable_available_balance = confidential_transfer_account + .decryptable_available_balance + .try_into() + .map_err(|_| Token2022Error::MalformedCiphertext)?; + let available_balance = aes_key + .decrypt(&decryptable_available_balance) + .ok_or(Token2022Error::AccountDecryption)?; + + let pending_balance_lo = confidential_transfer_account + .pending_balance_lo + .try_into() + .map_err(|_| Token2022Error::MalformedCiphertext)?; + let decrypted_pending_balance_lo = elgamal_keypair + .secret() + .decrypt_u32(&pending_balance_lo) + .ok_or(Token2022Error::AccountDecryption)?; + + let pending_balance_hi = confidential_transfer_account + .pending_balance_hi + .try_into() + .map_err(|_| Token2022Error::MalformedCiphertext)?; + let decrypted_pending_balance_hi = elgamal_keypair + .secret() + .decrypt_u32(&pending_balance_hi) + .ok_or(Token2022Error::AccountDecryption)?; + let pending_balance = + combine_balances(decrypted_pending_balance_lo, decrypted_pending_balance_hi) + .ok_or(Token2022Error::AccountDecryption)?; + + Ok((available_balance, pending_balance)) + } + + /// Burn SPL Tokens from the available balance of a confidential token + /// account + #[allow(clippy::too_many_arguments)] + pub async fn confidential_supply( + &self, + supply_elgamal_keypair: &ElGamalKeypair, + supply_aes_key: &AeKey, + ) -> Result { + let mint = self.get_mint_info().await?; + let mint_burn_extension = mint.get_extension::()?; + let supply_account_info = SupplyAccountInfo::new(mint_burn_extension); + + Ok(supply_account_info.decrypt_current_supply(supply_aes_key, supply_elgamal_keypair)?) + } + + pub async fn supply_elgamal_pubkey(&self) -> TokenResult> { + Ok(Into::>::into( + self.get_mint_info() + .await? + .get_extension::()? + .supply_elgamal_pubkey, + ) + .map(|pk| TryInto::::try_into(pk).unwrap())) + } + + /// Rotate the elgamal pubkey encrypting the confidential supply + #[allow(clippy::too_many_arguments)] + pub async fn rotate_supply_elgamal( + &self, + authority: &Pubkey, + new_supply_elgamal_keypair: &ElGamalKeypair, + signing_keypairs: &S, + proof_data: CiphertextCiphertextEqualityProofData, + ) -> TokenResult { + let signing_pubkeys = signing_keypairs.pubkeys(); + let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); + + let equality_proof_location = + Self::confidential_transfer_create_proof_location(Some(&proof_data), None, 1).unwrap(); + + self.process_ixs( + &confidential_mint_burn::instruction::rotate_supply_elgamal_pubkey( + &self.program_id, + self.get_address(), + authority, + &multisig_signers, + *new_supply_elgamal_keypair.pubkey(), + equality_proof_location, + )?, + signing_keypairs, + ) + .await + } } /// Calculates the maximum chunk size for a zero-knowledge proof record diff --git a/token/program-2022-test/tests/confidential_mint_burn.rs b/token/program-2022-test/tests/confidential_mint_burn.rs new file mode 100644 index 00000000000..af2b05f42ec --- /dev/null +++ b/token/program-2022-test/tests/confidential_mint_burn.rs @@ -0,0 +1,562 @@ +//#![cfg(feature = "test-sbf")] + +mod program_test; +use { + program_test::{ConfidentialTokenAccountMeta, TestContext, TokenContext}, + solana_program_test::tokio, + solana_sdk::{pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, signers::Signers}, + spl_token_2022::{ + extension::{ + confidential_mint_burn::{account_info::SupplyAccountInfo, ConfidentialMintBurn}, + confidential_transfer::{ + account_info::TransferAccountInfo, ConfidentialTransferAccount, + }, + BaseStateWithExtensions, + }, + solana_zk_sdk::encryption::{ + auth_encryption::AeKey, elgamal::*, pod::elgamal::PodElGamalPubkey, + }, + }, + spl_token_client::{ + client::ProgramBanksClientProcessTransaction, + token::{ExtensionInitializationParams, ProofAccount, ProofAccountWithCiphertext, Token}, + }, + spl_token_confidential_transfer_proof_generation::{ + burn::burn_split_proof_data, mint::mint_split_proof_data, + }, + std::convert::TryInto, +}; + +const MINT_AMOUNT: u64 = 42; +const BURN_AMOUNT: u64 = 12; + +#[tokio::test] +async fn test_confidential_mint() { + let authority = Keypair::new(); + let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); + let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); + let supply_aes_key = AeKey::new_rand(); + let mint_account = Keypair::new(); + + let mut context = TestContext::new().await; + context + .init_token_with_mint_keypair_and_freeze_authority_and_mint_authority( + mint_account, + vec![ + ExtensionInitializationParams::ConfidentialTransferMint { + authority: Some(authority.pubkey()), + auto_approve_new_accounts: true, + auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), + }, + ExtensionInitializationParams::ConfidentialMintBurnMint { + confidential_supply_pubkey: auditor_elgamal_pubkey, + decryptable_supply: supply_aes_key.encrypt(0).into(), + }, + ], + None, + // hacky but we have to clone somehow + Keypair::from_bytes(&authority.to_bytes()).unwrap(), + ) + .await + .unwrap(); + + let TokenContext { token, alice, .. } = context.token_context.unwrap(); + let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, None, false, false).await; + + mint_tokens( + &token, + &alice_meta.token_account, + &authority.pubkey(), + MINT_AMOUNT, + &auditor_elgamal_keypair, + &supply_aes_key, + &[&authority], + ) + .await; + + assert_eq!( + token + .confidential_balance( + &alice_meta.token_account, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key + ) + .await + .unwrap() + .1, + MINT_AMOUNT + ); + + token + .confidential_transfer_apply_pending_balance( + &alice_meta.token_account, + &alice.pubkey(), + None, + alice_meta.elgamal_keypair.secret(), + &alice_meta.aes_key, + &[&alice], + ) + .await + .unwrap(); + + assert_eq!( + token + .confidential_balance( + &alice_meta.token_account, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key + ) + .await + .unwrap() + .0, + MINT_AMOUNT + ); + + assert_eq!( + token + .confidential_supply(&auditor_elgamal_keypair, &supply_aes_key) + .await + .unwrap(), + MINT_AMOUNT + ); +} + +#[tokio::test] +async fn test_confidential_burn() { + let authority = Keypair::new(); + let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); + let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); + let supply_aes_key = AeKey::new_rand(); + let mint_account = Keypair::new(); + + let mut context = TestContext::new().await; + context + .init_token_with_mint_keypair_and_freeze_authority_and_mint_authority( + mint_account, + vec![ + ExtensionInitializationParams::ConfidentialTransferMint { + authority: Some(authority.pubkey()), + auto_approve_new_accounts: true, + auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), + }, + ExtensionInitializationParams::ConfidentialMintBurnMint { + confidential_supply_pubkey: auditor_elgamal_pubkey, + decryptable_supply: supply_aes_key.encrypt(0).into(), + }, + ], + None, + Keypair::from_bytes(&authority.to_bytes()).unwrap(), + ) + .await + .unwrap(); + + let TokenContext { token, alice, .. } = context.token_context.unwrap(); + let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, None, false, false).await; + + mint_tokens( + &token, + &alice_meta.token_account, + &authority.pubkey(), + MINT_AMOUNT, + &auditor_elgamal_keypair, + &supply_aes_key, + &[&authority], + ) + .await; + + assert_eq!( + token + .confidential_supply(&auditor_elgamal_keypair, &supply_aes_key) + .await + .unwrap(), + MINT_AMOUNT + ); + + token + .confidential_transfer_apply_pending_balance( + &alice_meta.token_account, + &alice.pubkey(), + None, + alice_meta.elgamal_keypair.secret(), + &alice_meta.aes_key, + &[&alice], + ) + .await + .unwrap(); + + let context_state_authority = Keypair::new(); + let auditor_elgamal_pubkey = token.auditor_elgamal_pubkey().await.unwrap(); + let supply_elgamal_pubkey = token.supply_elgamal_pubkey().await.unwrap(); + + let equality_proof_context_state_account = Keypair::new(); + let equality_proof_context_pubkey = equality_proof_context_state_account.pubkey(); + let ciphertext_validity_proof_context_state_account = Keypair::new(); + let ciphertext_validity_proof_context_pubkey = + ciphertext_validity_proof_context_state_account.pubkey(); + let range_proof_context_state_account = Keypair::new(); + let range_proof_context_pubkey = range_proof_context_state_account.pubkey(); + + let state = token + .get_account_info(&alice_meta.token_account) + .await + .unwrap(); + let extension = state + .get_extension::() + .unwrap(); + let transfer_account_info = TransferAccountInfo::new(extension); + + let proof_data = burn_split_proof_data( + &transfer_account_info.available_balance.try_into().unwrap(), + &transfer_account_info + .decryptable_available_balance + .try_into() + .unwrap(), + BURN_AMOUNT, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key, + &auditor_elgamal_pubkey.unwrap_or_default(), + &supply_elgamal_pubkey.unwrap_or_default(), + ) + .unwrap(); + + let range_proof_signer = &[&range_proof_context_state_account]; + let equality_proof_signer = &[&equality_proof_context_state_account]; + let ciphertext_validity_proof_signer = &[&ciphertext_validity_proof_context_state_account]; + let context_state_auth_pubkey = context_state_authority.pubkey(); + // setup proofs + token + .confidential_transfer_create_context_state_account( + &equality_proof_context_pubkey, + &context_state_auth_pubkey, + &proof_data.equality_proof_data, + true, + equality_proof_signer, + ) + .await + .unwrap(); + token + .confidential_transfer_create_context_state_account( + &ciphertext_validity_proof_context_pubkey, + &context_state_auth_pubkey, + &proof_data + .ciphertext_validity_proof_data_with_ciphertext + .proof_data, + false, + ciphertext_validity_proof_signer, + ) + .await + .unwrap(); + token + .confidential_transfer_create_context_state_account( + &range_proof_context_pubkey, + &context_state_auth_pubkey, + &proof_data.range_proof_data, + true, + range_proof_signer, + ) + .await + .unwrap(); + + let equality_proof_location = ProofAccount::ContextAccount(equality_proof_context_pubkey); + let ciphertext_validity_proof_location = + ProofAccount::ContextAccount(ciphertext_validity_proof_context_pubkey); + let ciphertext_validity_proof_location = ProofAccountWithCiphertext { + proof_account: ciphertext_validity_proof_location, + ciphertext_lo: proof_data + .ciphertext_validity_proof_data_with_ciphertext + .ciphertext_lo, + ciphertext_hi: proof_data + .ciphertext_validity_proof_data_with_ciphertext + .ciphertext_hi, + }; + let range_proof_location = ProofAccount::ContextAccount(range_proof_context_pubkey); + + // do the burn + token + .confidential_burn( + &alice_meta.token_account, + &alice.pubkey(), + Some(&equality_proof_location), + Some(&ciphertext_validity_proof_location), + Some(&range_proof_location), + BURN_AMOUNT, + supply_elgamal_pubkey, + &alice_meta.aes_key, + &[&alice], + ) + .await + .unwrap(); + + // close context state accounts + let context_state_authority_pubkey = context_state_authority.pubkey(); + let close_context_state_signers = &[context_state_authority]; + token + .confidential_transfer_close_context_state_account( + &equality_proof_context_pubkey, + &context_state_authority_pubkey, + &context_state_authority_pubkey, + close_context_state_signers, + ) + .await + .unwrap(); + token + .confidential_transfer_close_context_state_account( + &ciphertext_validity_proof_context_pubkey, + &context_state_authority_pubkey, + &context_state_authority_pubkey, + close_context_state_signers, + ) + .await + .unwrap(); + token + .confidential_transfer_close_context_state_account( + &range_proof_context_pubkey, + &context_state_authority_pubkey, + &context_state_authority_pubkey, + close_context_state_signers, + ) + .await + .unwrap(); + + assert_eq!( + token + .confidential_balance( + &alice_meta.token_account, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key + ) + .await + .unwrap() + .0, + MINT_AMOUNT - BURN_AMOUNT, + ); + + assert_eq!( + token + .confidential_supply(&auditor_elgamal_keypair, &supply_aes_key) + .await + .unwrap(), + MINT_AMOUNT - BURN_AMOUNT, + ); +} + +#[tokio::test] +async fn test_rotate_supply_elgamal() { + let authority = Keypair::new(); + let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); + let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); + let supply_aes_key = AeKey::new_rand(); + let mint_account = Keypair::new(); + + let mut context = TestContext::new().await; + context + .init_token_with_mint_keypair_and_freeze_authority_and_mint_authority( + mint_account, + vec![ + ExtensionInitializationParams::ConfidentialTransferMint { + authority: Some(authority.pubkey()), + auto_approve_new_accounts: true, + auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), + }, + ExtensionInitializationParams::ConfidentialMintBurnMint { + confidential_supply_pubkey: auditor_elgamal_pubkey, + decryptable_supply: supply_aes_key.encrypt(0).into(), + }, + ], + None, + Keypair::from_bytes(&authority.to_bytes()).unwrap(), + ) + .await + .unwrap(); + + let TokenContext { token, alice, .. } = context.token_context.unwrap(); + let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, None, false, false).await; + + mint_tokens( + &token, + &alice_meta.token_account, + &authority.pubkey(), + MINT_AMOUNT, + &auditor_elgamal_keypair, + &supply_aes_key, + &[&authority], + ) + .await; + + assert_eq!( + token + .confidential_supply(&auditor_elgamal_keypair, &supply_aes_key) + .await + .unwrap(), + MINT_AMOUNT + ); + + let new_supply_elgamal_keypair = ElGamalKeypair::new_rand(); + + let mint = token.get_mint_info().await.unwrap(); + let mint_burn_extension = mint.get_extension::().unwrap(); + let supply_account_info = SupplyAccountInfo::new(mint_burn_extension); + let proof_data = supply_account_info + .generate_rotate_supply_elgamal_pubkey_proof( + &supply_aes_key, + &auditor_elgamal_keypair, + &new_supply_elgamal_keypair, + ) + .unwrap(); + + token + .rotate_supply_elgamal( + &authority.pubkey(), + &new_supply_elgamal_keypair, + &[authority], + proof_data, + ) + .await + .unwrap(); + + assert_eq!( + token + .confidential_supply(&new_supply_elgamal_keypair, &supply_aes_key) + .await + .unwrap(), + MINT_AMOUNT + ); + + let mint = token.get_mint_info().await.unwrap(); + let mint_burn_extension = mint.get_extension::().unwrap(); + + assert_eq!( + mint_burn_extension.supply_elgamal_pubkey, + Into::::into(*new_supply_elgamal_keypair.pubkey(),), + ); +} + +async fn mint_tokens( + token: &Token, + token_account: &Pubkey, + authority: &Pubkey, + mint_amount: u64, + supply_elgamal_keypair: &ElGamalKeypair, + supply_aes_key: &AeKey, + bulk_signers: &impl Signers, +) { + let context_state_auth = Keypair::new(); + let equality_proof_context_state_account = Keypair::new(); + let equality_proof_context_pubkey = equality_proof_context_state_account.pubkey(); + let ciphertext_validity_proof_context_state_account = Keypair::new(); + let ciphertext_validity_proof_context_pubkey = + ciphertext_validity_proof_context_state_account.pubkey(); + let range_proof_context_state_account = Keypair::new(); + let range_proof_context_pubkey = range_proof_context_state_account.pubkey(); + + let mint_to_elgamal_pubkey = token.account_elgamal_pubkey(token_account).await.unwrap(); + let auditor_elgamal_pubkey = token.auditor_elgamal_pubkey().await.unwrap(); + let supply_elgamal_pubkey = token.supply_elgamal_pubkey().await.unwrap(); + + let mint = token.get_mint_info().await.unwrap(); + let mint_burn_extension = mint.get_extension::().unwrap(); + let supply_account_info = SupplyAccountInfo::new(mint_burn_extension); + + let proof_data = mint_split_proof_data( + &mint_burn_extension.confidential_supply.try_into().unwrap(), + mint_amount, + supply_account_info + .decrypt_current_supply(supply_aes_key, supply_elgamal_keypair) + .unwrap(), + supply_elgamal_keypair, + supply_aes_key, + &mint_to_elgamal_pubkey, + &auditor_elgamal_pubkey.unwrap_or_default(), + ) + .unwrap(); + + let equality_proof_signer = &[&equality_proof_context_state_account]; + let ciphertext_validity_proof_signer = &[&ciphertext_validity_proof_context_state_account]; + let range_proof_signer = &[&range_proof_context_state_account]; + + token + .confidential_transfer_create_context_state_account( + &equality_proof_context_pubkey, + &context_state_auth.pubkey(), + &proof_data.equality_proof_data, + false, + equality_proof_signer, + ) + .await + .unwrap(); + token + .confidential_transfer_create_context_state_account( + &ciphertext_validity_proof_context_pubkey, + &context_state_auth.pubkey(), + &proof_data + .ciphertext_validity_proof_data_with_ciphertext + .proof_data, + false, + ciphertext_validity_proof_signer, + ) + .await + .unwrap(); + token + .confidential_transfer_create_context_state_account( + &range_proof_context_pubkey, + &context_state_auth.pubkey(), + &proof_data.range_proof_data, + false, + range_proof_signer, + ) + .await + .unwrap(); + + let equality_proof_location = ProofAccount::ContextAccount(equality_proof_context_pubkey); + let ciphertext_validity_proof_location = + ProofAccount::ContextAccount(ciphertext_validity_proof_context_pubkey); + let ciphertext_validity_proof_location = ProofAccountWithCiphertext { + proof_account: ciphertext_validity_proof_location, + ciphertext_lo: proof_data + .ciphertext_validity_proof_data_with_ciphertext + .ciphertext_lo, + ciphertext_hi: proof_data + .ciphertext_validity_proof_data_with_ciphertext + .ciphertext_hi, + }; + let range_proof_location = ProofAccount::ContextAccount(range_proof_context_pubkey); + + println!( + "TOKEN: {}, ata: {token_account}, auth: {authority}", + token.get_address() + ); + token + .confidential_mint( + token_account, + authority, + supply_elgamal_pubkey, + Some(&equality_proof_location), + Some(&ciphertext_validity_proof_location), + Some(&range_proof_location), + proof_data.new_decryptable_supply, + bulk_signers, + ) + .await + .unwrap(); + + let close_context_auth = context_state_auth.pubkey(); + let close_context_state_signers = &[context_state_auth]; + token + .confidential_transfer_close_context_state_account( + &range_proof_context_pubkey, + &close_context_auth, + &close_context_auth, + close_context_state_signers, + ) + .await + .unwrap(); + token + .confidential_transfer_close_context_state_account( + &ciphertext_validity_proof_context_pubkey, + &close_context_auth, + &close_context_auth, + close_context_state_signers, + ) + .await + .unwrap(); +} diff --git a/token/program-2022-test/tests/program_test.rs b/token/program-2022-test/tests/program_test.rs index 1fbd749dae6..2b99efb93b5 100644 --- a/token/program-2022-test/tests/program_test.rs +++ b/token/program-2022-test/tests/program_test.rs @@ -103,6 +103,23 @@ impl TestContext { mint_account: Keypair, extension_init_params: Vec, freeze_authority: Option, + ) -> TokenResult<()> { + let mint_authority = Keypair::new(); + self.init_token_with_mint_keypair_and_freeze_authority_and_mint_authority( + mint_account, + extension_init_params, + freeze_authority, + mint_authority, + ) + .await + } + + pub async fn init_token_with_mint_keypair_and_freeze_authority_and_mint_authority( + &mut self, + mint_account: Keypair, + extension_init_params: Vec, + freeze_authority: Option, + mint_authority: Keypair, ) -> TokenResult<()> { let payer = keypair_clone(&self.context.lock().await.payer); let client: Arc> = @@ -113,7 +130,6 @@ impl TestContext { let decimals: u8 = 9; - let mint_authority = Keypair::new(); let mint_authority_pubkey = mint_authority.pubkey(); let freeze_authority_pubkey = freeze_authority .as_ref()