|
1 | 1 | use anchor_compressed_token::ErrorCode; |
2 | 2 | use anchor_lang::prelude::ProgramError; |
3 | 3 | use light_array_map::ArrayMap; |
4 | | -use light_compressed_account::instruction_data::with_readonly::InstructionDataInvokeCpiWithReadOnly; |
| 4 | +use light_compressed_account::instruction_data::with_readonly::{ |
| 5 | + InstructionDataInvokeCpiWithReadOnly, ZInstructionDataInvokeCpiWithReadOnlyMut, |
| 6 | +}; |
5 | 7 | use light_program_profiler::profile; |
6 | 8 | use light_token_interface::{ |
7 | 9 | hash_cache::HashCache, |
@@ -256,59 +258,10 @@ fn process_with_system_program_cpi<'a>( |
256 | 258 | accounts, |
257 | 259 | mint_cache, |
258 | 260 | )?; |
259 | | - |
260 | | - // ATA decompress is permissionless and idempotent. |
261 | | - // Detect from: exactly 1 input, 1 Decompress compression, CompressedOnly with is_ata=true. |
262 | | - // Multi-input batches (including mixed ATA + non-ATA) are not idempotent. |
263 | | - let is_ata_decompress = inputs.in_token_data.len() == 1 |
264 | | - && inputs |
265 | | - .compressions |
266 | | - .as_ref() |
267 | | - .is_some_and(|c| c.len() == 1 && c.iter().any(|c| c.mode.is_decompress())) |
268 | | - && inputs.in_tlv.as_ref().is_some_and(|tlvs| { |
269 | | - tlvs.iter().flatten().any(|ext| { |
270 | | - matches!(ext, ZExtensionInstructionData::CompressedOnly(data) if data.is_ata()) |
271 | | - }) |
272 | | - }); |
273 | | - |
274 | | - if is_ata_decompress { |
275 | | - let input_data = &inputs.in_token_data[0]; |
276 | | - let merkle_context = &input_data.merkle_context; |
277 | | - let input_account = cpi_instruction_struct |
278 | | - .input_compressed_accounts |
279 | | - .first() |
280 | | - .ok_or(ProgramError::InvalidAccountData)?; |
281 | | - |
282 | | - let owner_hashed = light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( |
283 | | - &crate::LIGHT_CPI_SIGNER.program_id, |
284 | | - ); |
285 | | - let tree_account = validated_accounts |
286 | | - .packed_accounts |
287 | | - .get_u8(merkle_context.merkle_tree_pubkey_index, "idempotent: tree")?; |
288 | | - let merkle_tree_hashed = |
289 | | - light_hasher::hash_to_field_size::hash_to_bn254_field_size_be(tree_account.key()); |
290 | | - |
291 | | - let lamports: u64 = (*input_account.lamports).into(); |
292 | | - let account_hash = light_compressed_account::compressed_account::hash_with_hashed_values( |
293 | | - &lamports, |
294 | | - input_account.address.as_ref().map(|x| x.as_slice()), |
295 | | - Some(( |
296 | | - input_account.discriminator.as_slice(), |
297 | | - input_account.data_hash.as_slice(), |
298 | | - )), |
299 | | - &owner_hashed, |
300 | | - &merkle_tree_hashed, |
301 | | - &merkle_context.leaf_index.get(), |
302 | | - true, |
303 | | - ) |
304 | | - .map_err(ProgramError::from)?; |
305 | | - |
306 | | - let mut tree = |
307 | | - light_batched_merkle_tree::merkle_tree::BatchedMerkleTreeAccount::state_from_account_info(tree_account) |
308 | | - .map_err(ProgramError::from)?; |
309 | | - |
310 | | - if tree.check_input_queue_non_inclusion(&account_hash).is_err() { |
311 | | - // Account is in bloom filter -- already spent. Idempotent no-op. |
| 261 | + let is_idempotent_ata_decompress = is_idempotent_ata_decompress(inputs); |
| 262 | + #[allow(clippy::collapsible_if)] |
| 263 | + if is_idempotent_ata_decompress { |
| 264 | + if check_ata_decompress_idempotent(inputs, &cpi_instruction_struct, validated_accounts)? { |
312 | 265 | return Ok(()); |
313 | 266 | } |
314 | 267 | } |
@@ -389,3 +342,76 @@ fn process_with_system_program_cpi<'a>( |
389 | 342 | } |
390 | 343 | Ok(()) |
391 | 344 | } |
| 345 | + |
| 346 | +/// Detect idempotent associated token account decompress: |
| 347 | +/// - exactly 1 input compressed token account with CompressedOnly extension is_ata=true |
| 348 | +/// - 1 Decompress compression. |
| 349 | +/// |
| 350 | +/// Multi-input batches (including mixed ATA + non-ATA) are not idempotent. |
| 351 | +#[inline(always)] |
| 352 | +pub fn is_idempotent_ata_decompress(inputs: &ZCompressedTokenInstructionDataTransfer2) -> bool { |
| 353 | + inputs.in_token_data.len() == 1 |
| 354 | + && inputs |
| 355 | + .compressions |
| 356 | + .as_ref() |
| 357 | + .is_some_and(|c| c.len() == 1 && c.iter().any(|c| c.mode.is_decompress())) |
| 358 | + && inputs.in_tlv.as_ref().is_some_and(|tlvs| { |
| 359 | + tlvs.iter().flatten().any(|ext| { |
| 360 | + matches!(ext, ZExtensionInstructionData::CompressedOnly(data) if data.is_ata()) |
| 361 | + }) |
| 362 | + }) |
| 363 | +} |
| 364 | + |
| 365 | +/// Computes the compressed account hash and checks whether the hash exists in the input queue bloom filters. |
| 366 | +/// The account compression program inserts spent compressed accounts into the respective input queue. |
| 367 | +/// |
| 368 | +/// if exists in bloom filter -> exit the ata was already decompressed |
| 369 | +/// else decompress |
| 370 | +#[cold] |
| 371 | +fn check_ata_decompress_idempotent( |
| 372 | + inputs: &ZCompressedTokenInstructionDataTransfer2, |
| 373 | + cpi_instruction_struct: &ZInstructionDataInvokeCpiWithReadOnlyMut<'_>, |
| 374 | + validated_accounts: &Transfer2Accounts, |
| 375 | +) -> Result<bool, ProgramError> { |
| 376 | + let input_data = inputs |
| 377 | + .in_token_data |
| 378 | + .first() |
| 379 | + .ok_or(ProgramError::InvalidAccountData)?; |
| 380 | + let merkle_context = &input_data.merkle_context; |
| 381 | + let input_account = cpi_instruction_struct |
| 382 | + .input_compressed_accounts |
| 383 | + .first() |
| 384 | + .ok_or(ProgramError::InvalidAccountData)?; |
| 385 | + |
| 386 | + let owner_hashed = light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( |
| 387 | + &crate::LIGHT_CPI_SIGNER.program_id, |
| 388 | + ); |
| 389 | + let tree_account = validated_accounts |
| 390 | + .packed_accounts |
| 391 | + .get_u8(merkle_context.merkle_tree_pubkey_index, "idempotent: tree")?; |
| 392 | + let merkle_tree_hashed = |
| 393 | + light_hasher::hash_to_field_size::hash_to_bn254_field_size_be(tree_account.key()); |
| 394 | + |
| 395 | + let lamports: u64 = input_account.lamports.get(); |
| 396 | + let account_hash = light_compressed_account::compressed_account::hash_with_hashed_values( |
| 397 | + &lamports, |
| 398 | + input_account.address.as_ref().map(|x| x.as_slice()), |
| 399 | + Some(( |
| 400 | + input_account.discriminator.as_slice(), |
| 401 | + input_account.data_hash.as_slice(), |
| 402 | + )), |
| 403 | + &owner_hashed, |
| 404 | + &merkle_tree_hashed, |
| 405 | + &merkle_context.leaf_index.get(), |
| 406 | + true, |
| 407 | + ) |
| 408 | + .map_err(ProgramError::from)?; |
| 409 | + |
| 410 | + let mut tree = |
| 411 | + light_batched_merkle_tree::merkle_tree::BatchedMerkleTreeAccount::state_from_account_info( |
| 412 | + tree_account, |
| 413 | + ) |
| 414 | + .map_err(ProgramError::from)?; |
| 415 | + |
| 416 | + Ok(tree.check_input_queue_non_inclusion(&account_hash).is_err()) |
| 417 | +} |
0 commit comments