diff --git a/Cargo.lock b/Cargo.lock index 44fa98541e1829..c86510256a4a26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8707,6 +8707,7 @@ dependencies = [ "bincode", "borsh 1.5.1", "bs58", + "bytemuck", "lazy_static", "log", "serde", @@ -8719,6 +8720,7 @@ dependencies = [ "spl-memo", "spl-token", "spl-token-2022", + "spl-token-confidential-transfer-proof-extraction", "spl-token-group-interface", "spl-token-metadata-interface", "thiserror 1.0.65", diff --git a/Cargo.toml b/Cargo.toml index cd056e7746cc5a..06a8e47c746df0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -530,6 +530,7 @@ solana-zk-sdk = { path = "zk-sdk", version = "=2.1.3" } solana-zk-token-proof-program = { path = "programs/zk-token-proof", version = "=2.1.3" } solana-zk-token-sdk = { path = "zk-token-sdk", version = "=2.1.3" } solana_rbpf = "=0.8.5" +<<<<<<< HEAD spl-associated-token-account = "=4.0.0" spl-instruction-padding = "0.2" spl-memo = "=5.0.0" @@ -538,6 +539,17 @@ spl-token = "=6.0.0" spl-token-2022 = "=4.0.0" spl-token-group-interface = "=0.3.0" spl-token-metadata-interface = "=0.4.0" +======= +spl-associated-token-account = "=6.0.0" +spl-instruction-padding = "0.3" +spl-memo = "=6.0.0" +spl-pod = "=0.5.0" +spl-token = "=7.0.0" +spl-token-2022 = "=6.0.0" +spl-token-confidential-transfer-proof-extraction = "0.2.0" +spl-token-group-interface = "=0.5.0" +spl-token-metadata-interface = "=0.6.0" +>>>>>>> 8730dbbb88 (transaction-status: Add confidential transfer tests (#3786)) static_assertions = "1.1.0" stream-cancel = "0.8.2" strum = "0.24" diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 3d913df0864d48..c4de162f9a8e7f 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -31,5 +31,9 @@ spl-token-group-interface = { workspace = true } spl-token-metadata-interface = { workspace = true } thiserror = { workspace = true } +[dev-dependencies] +bytemuck = { workspace = true } +spl-token-confidential-transfer-proof-extraction = { workspace = true } + [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/transaction-status/src/parse_token/extension/confidential_mint_burn.rs b/transaction-status/src/parse_token/extension/confidential_mint_burn.rs new file mode 100644 index 00000000000000..93e44a773700c9 --- /dev/null +++ b/transaction-status/src/parse_token/extension/confidential_mint_burn.rs @@ -0,0 +1,466 @@ +use { + super::*, + spl_token_2022::{ + extension::confidential_mint_burn::instruction::*, + instruction::{decode_instruction_data, decode_instruction_type}, + }, +}; + +pub(in crate::parse_token) fn parse_confidential_mint_burn_instruction( + instruction_data: &[u8], + account_indexes: &[u8], + account_keys: &AccountKeys, +) -> Result { + match decode_instruction_type(instruction_data) + .map_err(|_| ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken))? + { + ConfidentialMintBurnInstruction::InitializeMint => { + check_num_token_accounts(account_indexes, 1)?; + let initialize_mint_data: InitializeMintData = + *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let value = json!({ + "mint": account_keys[account_indexes[0] as usize].to_string(), + "supplyElGamalPubkey": initialize_mint_data.supply_elgamal_pubkey.to_string(), + "decryptableSupply": initialize_mint_data.decryptable_supply.to_string(), + }); + Ok(ParsedInstructionEnum { + instruction_type: "initializeConfidentialMintBurnMint".to_string(), + info: value, + }) + } + ConfidentialMintBurnInstruction::UpdateDecryptableSupply => { + check_num_token_accounts(account_indexes, 2)?; + let update_decryptable_supply: UpdateDecryptableSupplyData = + *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let mut value = json!({ + "mint": account_keys[account_indexes[0] as usize].to_string(), + "newDecryptableSupply": update_decryptable_supply.new_decryptable_supply.to_string(), + + }); + let map = value.as_object_mut().unwrap(); + parse_signers( + map, + 1, + account_keys, + account_indexes, + "owner", + "multisigOwner", + ); + Ok(ParsedInstructionEnum { + instruction_type: "updateConfidentialMintBurnDecryptableSupply".to_string(), + info: value, + }) + } + ConfidentialMintBurnInstruction::RotateSupplyElGamalPubkey => { + check_num_token_accounts(account_indexes, 3)?; + let rotate_supply_data: RotateSupplyElGamalPubkeyData = + *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let mut value = json!({ + "mint": account_keys[account_indexes[0] as usize].to_string(), + "newSupplyElGamalPubkey": rotate_supply_data.new_supply_elgamal_pubkey.to_string(), + "proofInstructionOffset": rotate_supply_data.proof_instruction_offset, + + }); + let map = value.as_object_mut().unwrap(); + if rotate_supply_data.proof_instruction_offset == 0 { + map.insert( + "proofAccount".to_string(), + json!(account_keys[account_indexes[1] as usize].to_string()), + ); + } else { + map.insert( + "instructionsSysvar".to_string(), + json!(account_keys[account_indexes[1] as usize].to_string()), + ); + } + parse_signers( + map, + 2, + account_keys, + account_indexes, + "owner", + "multisigOwner", + ); + Ok(ParsedInstructionEnum { + instruction_type: "rotateConfidentialMintBurnSupplyElGamalPubkey".to_string(), + info: value, + }) + } + ConfidentialMintBurnInstruction::Mint => { + check_num_token_accounts(account_indexes, 3)?; + let mint_data: MintInstructionData = *decode_instruction_data(instruction_data) + .map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let mut value = json!({ + "destination": account_keys[account_indexes[0] as usize].to_string(), + "mint": account_keys[account_indexes[1] as usize].to_string(), + "newDecryptableSupply": mint_data.new_decryptable_supply.to_string(), + "equalityProofInstructionOffset": mint_data.equality_proof_instruction_offset, + "ciphertextValidityProofInstructionOffset": mint_data.ciphertext_validity_proof_instruction_offset, + "rangeProofInstructionOffset": mint_data.range_proof_instruction_offset, + + }); + let mut offset = 2; + let map = value.as_object_mut().unwrap(); + if offset < account_indexes.len() - 1 + && (mint_data.equality_proof_instruction_offset != 0 + || mint_data.ciphertext_validity_proof_instruction_offset != 0 + || mint_data.range_proof_instruction_offset != 0) + { + map.insert( + "instructionsSysvar".to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + + // Assume that extra accounts are proof accounts and not multisig + // signers. This might be wrong, but it's the best possible option. + if offset < account_indexes.len() - 1 { + let label = if mint_data.equality_proof_instruction_offset == 0 { + "equalityProofContextStateAccount" + } else { + "equalityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + if offset < account_indexes.len() - 1 { + let label = if mint_data.ciphertext_validity_proof_instruction_offset == 0 { + "ciphertextValidityProofContextStateAccount" + } else { + "ciphertextValidityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + if offset < account_indexes.len() - 1 { + let label = if mint_data.range_proof_instruction_offset == 0 { + "rangeProofContextStateAccount" + } else { + "rangeProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + + parse_signers( + map, + offset, + account_keys, + account_indexes, + "owner", + "multisigOwner", + ); + Ok(ParsedInstructionEnum { + instruction_type: "confidentialMint".to_string(), + info: value, + }) + } + ConfidentialMintBurnInstruction::Burn => { + check_num_token_accounts(account_indexes, 3)?; + let burn_data: BurnInstructionData = *decode_instruction_data(instruction_data) + .map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let mut value = json!({ + "destination": account_keys[account_indexes[0] as usize].to_string(), + "mint": account_keys[account_indexes[1] as usize].to_string(), + "newDecryptableAvailableBalance": burn_data.new_decryptable_available_balance.to_string(), + "equalityProofInstructionOffset": burn_data.equality_proof_instruction_offset, + "ciphertextValidityProofInstructionOffset": burn_data.ciphertext_validity_proof_instruction_offset, + "rangeProofInstructionOffset": burn_data.range_proof_instruction_offset, + + }); + let mut offset = 2; + let map = value.as_object_mut().unwrap(); + if offset < account_indexes.len() - 1 + && (burn_data.equality_proof_instruction_offset != 0 + || burn_data.ciphertext_validity_proof_instruction_offset != 0 + || burn_data.range_proof_instruction_offset != 0) + { + map.insert( + "instructionsSysvar".to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + + // Assume that extra accounts are proof accounts and not multisig + // signers. This might be wrong, but it's the best possible option. + if offset < account_indexes.len() - 1 { + let label = if burn_data.equality_proof_instruction_offset == 0 { + "equalityProofContextStateAccount" + } else { + "equalityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + if offset < account_indexes.len() - 1 { + let label = if burn_data.ciphertext_validity_proof_instruction_offset == 0 { + "ciphertextValidityProofContextStateAccount" + } else { + "ciphertextValidityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + if offset < account_indexes.len() - 1 { + let label = if burn_data.range_proof_instruction_offset == 0 { + "rangeProofContextStateAccount" + } else { + "rangeProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + + parse_signers( + map, + offset, + account_keys, + account_indexes, + "owner", + "multisigOwner", + ); + Ok(ParsedInstructionEnum { + instruction_type: "confidentialBurn".to_string(), + info: value, + }) + } + } +} + +#[cfg(test)] +mod test { + use { + super::*, + bytemuck::Zeroable, + solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }, + spl_token_2022::{ + extension::confidential_mint_burn::instruction::{ + confidential_burn_with_split_proofs, confidential_mint_with_split_proofs, + initialize_mint, + }, + solana_program::message::Message, + solana_zk_sdk::{ + encryption::{ + auth_encryption::AeCiphertext, + elgamal::ElGamalPubkey, + pod::{auth_encryption::PodAeCiphertext, elgamal::PodElGamalPubkey}, + }, + zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCiphertextEqualityProofData, CiphertextCommitmentEqualityProofData, + }, + }, + }, + spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation}, + std::num::NonZero, + }; + + fn check_no_panic(mut instruction: Instruction) { + let account_meta = AccountMeta::new_readonly(Pubkey::new_unique(), false); + for i in 0..20 { + instruction.accounts = vec![account_meta.clone(); i]; + let message = Message::new(&[instruction.clone()], None); + let compiled_instruction = &message.instructions[0]; + let _ = parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None), + ); + } + } + + #[test] + fn test_initialize() { + let instruction = initialize_mint( + &spl_token_2022::id(), + &Pubkey::new_unique(), + PodElGamalPubkey::default(), + PodAeCiphertext::default(), + ) + .unwrap(); + check_no_panic(instruction); + } + + #[test] + fn test_update() { + let instruction = update_decryptable_supply( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &[], + AeCiphertext::default(), + ) + .unwrap(); + check_no_panic(instruction); + } + + #[test] + fn test_rotate() { + for location in [ + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::InstructionData(&CiphertextCiphertextEqualityProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ] { + let instructions = rotate_supply_elgamal_pubkey( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &[], + ElGamalPubkey::default(), + location, + ) + .unwrap(); + check_no_panic(instructions[0].clone()); + } + } + + #[test] + fn test_mint() { + for (equality_proof_location, ciphertext_validity_proof_location, range_proof_location) in [ + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::InstructionData(&CiphertextCommitmentEqualityProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(2).unwrap(), + ProofData::InstructionData( + &BatchedGroupedCiphertext3HandlesValidityProofData::zeroed(), + ), + ), + ProofLocation::InstructionOffset( + NonZero::new(3).unwrap(), + ProofData::InstructionData(&BatchedRangeProofU128Data::zeroed()), + ), + ), + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(2).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(3).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ), + ( + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ), + ] { + let instructions = confidential_mint_with_split_proofs( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + None, + &Pubkey::new_unique(), + &[], + equality_proof_location, + ciphertext_validity_proof_location, + range_proof_location, + AeCiphertext::default(), + ) + .unwrap(); + check_no_panic(instructions[0].clone()); + } + } + + #[test] + fn test_burn() { + for (equality_proof_location, ciphertext_validity_proof_location, range_proof_location) in [ + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::InstructionData(&CiphertextCommitmentEqualityProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(2).unwrap(), + ProofData::InstructionData( + &BatchedGroupedCiphertext3HandlesValidityProofData::zeroed(), + ), + ), + ProofLocation::InstructionOffset( + NonZero::new(3).unwrap(), + ProofData::InstructionData(&BatchedRangeProofU128Data::zeroed()), + ), + ), + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(2).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(3).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ), + ( + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ), + ] { + let instructions = confidential_burn_with_split_proofs( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + None, + PodAeCiphertext::default(), + &Pubkey::new_unique(), + &[], + equality_proof_location, + ciphertext_validity_proof_location, + range_proof_location, + ) + .unwrap(); + check_no_panic(instructions[0].clone()); + } + } +} diff --git a/transaction-status/src/parse_token/extension/confidential_transfer.rs b/transaction-status/src/parse_token/extension/confidential_transfer.rs index 78b1c229c34638..093d64239fbdbf 100644 --- a/transaction-status/src/parse_token/extension/confidential_transfer.rs +++ b/transaction-status/src/parse_token/extension/confidential_transfer.rs @@ -170,6 +170,47 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( }); let map = value.as_object_mut().unwrap(); +<<<<<<< HEAD +======= + if offset < account_indexes.len() - 1 + && (withdrawal_data.equality_proof_instruction_offset != 0 + || withdrawal_data.range_proof_instruction_offset != 0) + { + map.insert( + "instructionsSysvar".to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + + // Assume that extra accounts are proof accounts and not multisig + // signers. This might be wrong, but it's the best possible option. + if offset < account_indexes.len() - 1 { + let label = if withdrawal_data.equality_proof_instruction_offset == 0 { + "equalityProofContextStateAccount" + } else { + "equalityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + + if offset < account_indexes.len() - 1 { + let label = if withdrawal_data.range_proof_instruction_offset == 0 { + "rangeProofContextStateAccount" + } else { + "rangeProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } +>>>>>>> 8730dbbb88 (transaction-status: Add confidential transfer tests (#3786)) parse_signers( map, 4, @@ -200,6 +241,62 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( }); let map = value.as_object_mut().unwrap(); +<<<<<<< HEAD +======= + if offset < account_indexes.len() - 1 + && (transfer_data.equality_proof_instruction_offset != 0 + || transfer_data.ciphertext_validity_proof_instruction_offset != 0 + || transfer_data.range_proof_instruction_offset != 0) + { + map.insert( + "instructionsSysvar".to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + + // Assume that extra accounts are proof accounts and not multisig + // signers. This might be wrong, but it's the best possible option. + if offset < account_indexes.len() - 1 { + let label = if transfer_data.equality_proof_instruction_offset == 0 { + "equalityProofContextStateAccount" + } else { + "equalityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + + if offset < account_indexes.len() - 1 { + let label = if transfer_data.ciphertext_validity_proof_instruction_offset == 0 { + "ciphertextValidityProofContextStateAccount" + } else { + "ciphertextValidityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + + if offset < account_indexes.len() - 1 { + let label = if transfer_data.range_proof_instruction_offset == 0 { + "rangeProofContextStateAccount" + } else { + "rangeProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + +>>>>>>> 8730dbbb88 (transaction-status: Add confidential transfer tests (#3786)) parse_signers( map, 4, @@ -213,6 +310,127 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( info: value, }) } +<<<<<<< HEAD +======= + ConfidentialTransferInstruction::TransferWithFee => { + check_num_token_accounts(account_indexes, 4)?; + let transfer_data: TransferWithFeeInstructionData = + *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let equality_proof_instruction_offset: i8 = + transfer_data.equality_proof_instruction_offset; + let transfer_amount_ciphertext_validity_proof_instruction_offset: i8 = + transfer_data.transfer_amount_ciphertext_validity_proof_instruction_offset; + let fee_sigma_proof_instruction_offset: i8 = + transfer_data.fee_sigma_proof_instruction_offset; + let fee_ciphertext_validity_proof_instruction_offset: i8 = + transfer_data.fee_ciphertext_validity_proof_instruction_offset; + let range_proof_instruction_offset: i8 = transfer_data.range_proof_instruction_offset; + let mut value = json!({ + "source": account_keys[account_indexes[0] as usize].to_string(), + "mint": account_keys[account_indexes[1] as usize].to_string(), + "destination": account_keys[account_indexes[2] as usize].to_string(), + "newSourceDecryptableAvailableBalance": format!("{}", transfer_data.new_source_decryptable_available_balance), + "equalityProofInstructionOffset": equality_proof_instruction_offset, + "transferAmountCiphertextValidityProofInstructionOffset": transfer_amount_ciphertext_validity_proof_instruction_offset, + "feeCiphertextValidityProofInstructionOffset": fee_ciphertext_validity_proof_instruction_offset, + "feeSigmaProofInstructionOffset": fee_sigma_proof_instruction_offset, + "rangeProofInstructionOffset": range_proof_instruction_offset, + }); + + let mut offset = 3; + let map = value.as_object_mut().unwrap(); + if offset < account_indexes.len() - 1 + && (equality_proof_instruction_offset != 0 + || transfer_amount_ciphertext_validity_proof_instruction_offset != 0 + || fee_ciphertext_validity_proof_instruction_offset != 0 + || fee_sigma_proof_instruction_offset != 0 + || range_proof_instruction_offset != 0) + { + map.insert( + "instructionsSysvar".to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + + // Assume that extra accounts are proof accounts and not multisig + // signers. This might be wrong, but it's the best possible option. + if offset < account_indexes.len() - 1 { + let label = if equality_proof_instruction_offset == 0 { + "equalityProofContextStateAccount" + } else { + "equalityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + if offset < account_indexes.len() - 1 { + let label = if transfer_amount_ciphertext_validity_proof_instruction_offset == 0 { + "transferAmountCiphertextValidityProofContextStateAccount" + } else { + "transferAmountCiphertextValidityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + if offset < account_indexes.len() - 1 { + let label = if fee_ciphertext_validity_proof_instruction_offset == 0 { + "feeCiphertextValidityProofContextStateAccount" + } else { + "feeCiphertextValidityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + if offset < account_indexes.len() - 1 { + let label = if fee_sigma_proof_instruction_offset == 0 { + "feeSigmaProofContextStateAccount" + } else { + "feeSigmaProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + if offset < account_indexes.len() - 1 { + let label = if range_proof_instruction_offset == 0 { + "rangeProofContextStateAccount" + } else { + "rangeProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + parse_signers( + map, + offset, + account_keys, + account_indexes, + "owner", + "multisigOwner", + ); + Ok(ParsedInstructionEnum { + instruction_type: "confidentialTransferWithFee".to_string(), + info: value, + }) + } +>>>>>>> 8730dbbb88 (transaction-status: Add confidential transfer tests (#3786)) ConfidentialTransferInstruction::ApplyPendingBalance => { check_num_token_accounts(account_indexes, 2)?; let apply_pending_balance_data: ApplyPendingBalanceData = @@ -358,3 +576,321 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( } } } + +#[cfg(test)] +mod test { + use { + super::*, + bytemuck::Zeroable, + solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }, + spl_token_2022::{ + extension::confidential_transfer::instruction::{ + initialize_mint, inner_configure_account, inner_empty_account, update_mint, + }, + solana_program::message::Message, + solana_zk_sdk::{ + encryption::pod::auth_encryption::PodAeCiphertext, + zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCommitmentEqualityProofData, ZeroCiphertextProofData, + }, + }, + }, + spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation}, + std::num::NonZero, + }; + + fn check_no_panic(mut instruction: Instruction) { + let account_meta = AccountMeta::new_readonly(Pubkey::new_unique(), false); + for i in 0..20 { + instruction.accounts = vec![account_meta.clone(); i]; + let message = Message::new(&[instruction.clone()], None); + let compiled_instruction = &message.instructions[0]; + let _ = parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None), + ); + } + } + + #[test] + fn test_initialize() { + let instruction = initialize_mint( + &spl_token_2022::id(), + &Pubkey::new_unique(), + Some(Pubkey::new_unique()), + true, + None, + ) + .unwrap(); + check_no_panic(instruction); + } + + #[test] + fn test_approve() { + let instruction = approve_account( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &[], + ) + .unwrap(); + check_no_panic(instruction); + } + + #[test] + fn test_update() { + let instruction = update_mint( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &[], + true, + None, + ) + .unwrap(); + check_no_panic(instruction); + } + + #[test] + fn test_configure() { + for location in [ + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::InstructionData(&PubkeyValidityProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ] { + let instruction = inner_configure_account( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + PodAeCiphertext::default(), + 10_000, + &Pubkey::new_unique(), + &[], + location, + ) + .unwrap(); + check_no_panic(instruction); + } + } + + #[test] + fn test_empty_account() { + for location in [ + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::InstructionData(&ZeroCiphertextProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ] { + let instruction = inner_empty_account( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &[], + location, + ) + .unwrap(); + check_no_panic(instruction); + } + } + + #[test] + fn test_withdraw() { + for (equality_proof_location, range_proof_location) in [ + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::InstructionData(&CiphertextCommitmentEqualityProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(3).unwrap(), + ProofData::InstructionData(&BatchedRangeProofU64Data::zeroed()), + ), + ), + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(2).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ), + ( + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ), + ] { + let instruction = inner_withdraw( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 1, + 2, + PodAeCiphertext::default(), + &Pubkey::new_unique(), + &[], + equality_proof_location, + range_proof_location, + ) + .unwrap(); + check_no_panic(instruction); + } + } + + #[test] + fn test_transfer() { + for (equality_proof_location, ciphertext_validity_proof_location, range_proof_location) in [ + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::InstructionData(&CiphertextCommitmentEqualityProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(2).unwrap(), + ProofData::InstructionData( + &BatchedGroupedCiphertext3HandlesValidityProofData::zeroed(), + ), + ), + ProofLocation::InstructionOffset( + NonZero::new(3).unwrap(), + ProofData::InstructionData(&BatchedRangeProofU128Data::zeroed()), + ), + ), + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(2).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(3).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ), + ( + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ), + ] { + let instruction = inner_transfer( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + PodAeCiphertext::default(), + &Pubkey::new_unique(), + &[], + equality_proof_location, + ciphertext_validity_proof_location, + range_proof_location, + ) + .unwrap(); + check_no_panic(instruction); + } + } + + #[test] + fn test_transfer_with_fee() { + for ( + equality_proof_location, + transfer_amount_ciphertext_validity_proof_location, + fee_sigma_proof_location, + fee_ciphertext_validity_proof_location, + range_proof_location, + ) in [ + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::InstructionData(&CiphertextCommitmentEqualityProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(2).unwrap(), + ProofData::InstructionData( + &BatchedGroupedCiphertext3HandlesValidityProofData::zeroed(), + ), + ), + ProofLocation::InstructionOffset( + NonZero::new(3).unwrap(), + ProofData::InstructionData(&PercentageWithCapProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(4).unwrap(), + ProofData::InstructionData( + &BatchedGroupedCiphertext2HandlesValidityProofData::zeroed(), + ), + ), + ProofLocation::InstructionOffset( + NonZero::new(5).unwrap(), + ProofData::InstructionData(&BatchedRangeProofU256Data::zeroed()), + ), + ), + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(2).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(3).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(4).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::InstructionOffset( + NonZero::new(5).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ), + ( + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ), + ] { + let instruction = inner_transfer_with_fee( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + PodAeCiphertext::default(), + &Pubkey::new_unique(), + &[], + equality_proof_location, + transfer_amount_ciphertext_validity_proof_location, + fee_sigma_proof_location, + fee_ciphertext_validity_proof_location, + range_proof_location, + ) + .unwrap(); + check_no_panic(instruction); + } + } +} diff --git a/transaction-status/src/parse_token/extension/confidential_transfer_fee.rs b/transaction-status/src/parse_token/extension/confidential_transfer_fee.rs index 9a6856835c864b..105d435d635607 100644 --- a/transaction-status/src/parse_token/extension/confidential_transfer_fee.rs +++ b/transaction-status/src/parse_token/extension/confidential_transfer_fee.rs @@ -157,3 +157,96 @@ pub(in crate::parse_token) fn parse_confidential_transfer_fee_instruction( } } } + +#[cfg(test)] +mod test { + use { + super::*, + bytemuck::Zeroable, + solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }, + spl_token_2022::{ + extension::confidential_transfer_fee::instruction::{ + inner_withdraw_withheld_tokens_from_accounts, + inner_withdraw_withheld_tokens_from_mint, + }, + solana_program::message::Message, + solana_zk_sdk::{ + encryption::pod::auth_encryption::PodAeCiphertext, + zk_elgamal_proof_program::proof_data::CiphertextCiphertextEqualityProofData, + }, + }, + spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation}, + std::num::NonZero, + }; + + fn check_no_panic(mut instruction: Instruction) { + let account_meta = AccountMeta::new_readonly(Pubkey::new_unique(), false); + for i in 0..20 { + instruction.accounts = vec![account_meta.clone(); i]; + let message = Message::new(&[instruction.clone()], None); + let compiled_instruction = &message.instructions[0]; + let _ = parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None), + ); + } + } + + #[test] + fn test_withdraw_from_accounts() { + for location in [ + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::InstructionData(&CiphertextCiphertextEqualityProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ] { + let instruction = inner_withdraw_withheld_tokens_from_accounts( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &PodAeCiphertext::default(), + &Pubkey::new_unique(), + &[], + &[&Pubkey::new_unique(), &Pubkey::new_unique()], + location, + ) + .unwrap(); + check_no_panic(instruction); + } + } + + #[test] + fn test_withdraw_from_mint() { + for location in [ + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::InstructionData(&CiphertextCiphertextEqualityProofData::zeroed()), + ), + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + ProofData::RecordAccount(&Pubkey::new_unique(), 0), + ), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ] { + let instruction = inner_withdraw_withheld_tokens_from_mint( + &spl_token_2022::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &PodAeCiphertext::default(), + &Pubkey::new_unique(), + &[], + location, + ) + .unwrap(); + check_no_panic(instruction); + } + } +}