light_event/
parse.rs

1use borsh::BorshDeserialize;
2use light_compressed_account::{
3    compressed_account::{
4        CompressedAccount, CompressedAccountData, PackedCompressedAccountWithMerkleContext,
5    },
6    constants::{
7        ACCOUNT_COMPRESSION_PROGRAM_ID, CREATE_CPI_CONTEXT_ACCOUNT, LIGHT_SYSTEM_PROGRAM_ID,
8        REGISTERED_PROGRAM_PDA,
9    },
10    discriminators::*,
11    instruction_data::{
12        data::{InstructionDataInvoke, OutputCompressedAccountWithPackedContext},
13        insert_into_queues::InsertIntoQueuesInstructionData,
14        with_account_info::InstructionDataInvokeCpiWithAccountInfo,
15        with_readonly::InstructionDataInvokeCpiWithReadOnly,
16    },
17    nullifier::create_nullifier,
18    Pubkey,
19};
20use light_zero_copy::traits::ZeroCopyAt;
21
22use super::{
23    error::ParseIndexerEventError,
24    event::{
25        BatchNullifyContext, BatchPublicTransactionEvent, MerkleTreeSequenceNumber,
26        MerkleTreeSequenceNumberV1, NewAddress, PublicTransactionEvent,
27    },
28};
29
30#[derive(Debug, Clone, PartialEq)]
31struct ExecutingSystemInstruction<'a> {
32    output_compressed_accounts: Vec<OutputCompressedAccountWithPackedContext>,
33    input_compressed_accounts: Vec<PackedCompressedAccountWithMerkleContext>,
34    is_compress: bool,
35    relay_fee: Option<u64>,
36    compress_or_decompress_lamports: Option<u64>,
37    execute_cpi_context: bool,
38    accounts: &'a [Pubkey],
39}
40
41#[derive(Debug, Clone, PartialEq, Default)]
42pub(crate) struct Indices {
43    pub system: usize,
44    pub cpi: Vec<usize>,
45    pub insert_into_queues: usize,
46    pub found_solana_system_program_instruction: bool,
47    pub found_system: bool,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq)]
51pub(crate) enum ProgramId {
52    LightSystem,
53    AccountCompression,
54    SolanaSystem,
55    Unknown,
56}
57
58#[derive(Debug, Clone, PartialEq)]
59struct AssociatedInstructions<'a> {
60    pub executing_system_instruction: ExecutingSystemInstruction<'a>,
61    pub cpi_context_outputs: Vec<OutputCompressedAccountWithPackedContext>,
62    pub insert_into_queues_instruction: InsertIntoQueuesInstructionData<'a>,
63    pub accounts: &'a [Pubkey],
64}
65
66/// We piece the event together from 2 instructions:
67/// 1. light_system_program::{Invoke, InvokeCpi, InvokeCpiReadOnly} (one of the 3)
68/// 2. account_compression::InsertIntoQueues
69/// - We return new addresses in batched trees separately
70///   because from the PublicTransactionEvent there
71///   is no way to know which addresses are new and
72///   for batched address trees we need to index the queue of new addresses
73///   the tree&queue account only contains bloomfilters, roots and metadata.
74///
75/// Steps:
76/// 0. Wrap program ids of instructions to filter but not change the pattern
77///         system program cpi context creation ixs
78///         insert into queue ixs not by the system program
79///         instructions with less than 12 bytes ix data
80/// 1. Find associated instructions by cpi pattern.
81/// 2. Deserialize associated instructions.
82/// 3. Create batched transaction events.
83pub fn event_from_light_transaction(
84    program_ids: &[Pubkey],
85    instructions: &[Vec<u8>],
86    accounts: Vec<Vec<Pubkey>>,
87) -> Result<Option<Vec<BatchPublicTransactionEvent>>, ParseIndexerEventError> {
88    // 0. Wrap program ids of instructions to filter but not change the pattern.
89    let program_ids = wrap_program_ids(program_ids, instructions, &accounts);
90    // 1. Find associated instructions by cpi pattern.
91    let mut patterns = find_cpi_patterns(&program_ids);
92    if patterns.is_empty() {
93        return Ok(None);
94    }
95    // We searched from the last pattern to the first.
96    //      -> reverse to be in order
97    patterns.reverse();
98    // 2. Deserialize associated instructions.
99    let associated_instructions = patterns
100        .iter()
101        .map(|pattern| deserialize_associated_instructions(pattern, instructions, &accounts))
102        .collect::<Result<Vec<_>, _>>()?;
103    // 3. Create batched transaction events.
104    let batched_transaction_events = associated_instructions
105        .iter()
106        .map(|associated_instruction| create_batched_transaction_event(associated_instruction))
107        .collect::<Result<Vec<_>, _>>()?;
108
109    // // Sanity checks:
110    // // - this must not throw in production because indexing just works if all instructions are in the same transaction.
111    // // - It's ok if someone misues the cpi context account but transaction data will not be available in photon.
112    // // - if we would throw an error it would brick photon because we would not be able to index a transaction that changed queue state.
113    // // - I could add extra data to the account compression cpi to make this impossible. -> this makes sense it is more robust.
114    // // TODO: make debug
115    // batched_transaction_events.iter().for_each(|event| {
116    //     println!("event: {:?}", event);
117    //     assert_eq!(
118    //         event.event.input_compressed_account_hashes.len(),
119    //         event.batch_input_accounts.len(),
120    //         "Input hashes and input accounts length mismatch "
121    //     );
122    //     assert_eq!(
123    //         event.event.output_compressed_account_hashes.len(),
124    //         event.event.output_leaf_indices.len(),
125    //         "Output hashes and output leaf indices length mismatch "
126    //     );
127    //     assert_eq!(
128    //         event.event.output_compressed_account_hashes.len(),
129    //         event.event.output_compressed_accounts.len(),
130    //         "Output hashes and output compressed accounts length mismatch "
131    //     );
132    // });
133    Ok(Some(batched_transaction_events))
134}
135
136fn deserialize_associated_instructions<'a>(
137    indices: &Indices,
138    instructions: &'a [Vec<u8>],
139    accounts: &'a [Vec<Pubkey>],
140) -> Result<AssociatedInstructions<'a>, ParseIndexerEventError> {
141    let (insert_queues_instruction, cpi_context_outputs) = {
142        let ix = &instructions[indices.insert_into_queues];
143        if ix.len() < 12 {
144            return Err(ParseIndexerEventError::InstructionDataTooSmall(
145                ix.len(),
146                12,
147            ));
148        }
149        let discriminator: [u8; 8] = ix[0..8].try_into().unwrap();
150        if discriminator == DISCRIMINATOR_INSERT_INTO_QUEUES {
151            let (data, bytes) = InsertIntoQueuesInstructionData::zero_copy_at(&ix[12..])?;
152            let cpi_context_outputs =
153                Vec::<OutputCompressedAccountWithPackedContext>::deserialize(&mut &bytes[..])?;
154            Ok((data, cpi_context_outputs))
155        } else {
156            Err(ParseIndexerEventError::DeserializeAccountLightSystemCpiInputsError)
157        }
158    }?;
159    let exec_instruction =
160        deserialize_instruction(&instructions[indices.system], &accounts[indices.system])?;
161    Ok(AssociatedInstructions {
162        executing_system_instruction: exec_instruction,
163        cpi_context_outputs,
164        insert_into_queues_instruction: insert_queues_instruction,
165        // Remove signer and register program accounts.
166        accounts: &accounts[indices.insert_into_queues][2..],
167    })
168}
169
170/// Filter all system instructions which create cpi context accounts,
171/// so that we can infer that a system program instruction is a light transaction.
172/// Create new AssociatedInstructions when we find a system instruction
173/// if next instruct is solana system program isntruction followed by insert into queues is executable instruction
174/// else is cpi instruction
175/// only push into vec if insert into queues instruction is found
176fn find_cpi_patterns(program_ids: &[ProgramId]) -> Vec<Indices> {
177    let mut vec = Vec::new();
178    let mut next_index = usize::MAX;
179    for (last_index, program_id) in (0..program_ids.len()).rev().zip(program_ids.iter().rev()) {
180        // skip last found pattern
181        if last_index > next_index {
182            continue;
183        }
184        // In case that we encounter more than one account compression program ix
185        // before finding one or more system program ix we just overwrite.
186        if let ProgramId::AccountCompression = program_id {
187            let (res, last_index) = find_cpi_pattern(last_index, program_ids);
188            next_index = last_index;
189            if let Some(res) = res {
190                vec.push(res);
191            };
192        }
193    }
194    vec
195}
196
197/// Pattern, SYSTEM_PROGRAM_ID.., default ids .., account compression program id
198/// We search for the pattern in reverse because there can be multiple system instructions
199/// but only one account compression instruction.
200/// Start index points to ACCOUNT_COMPRESSION_PROGRAM_ID
201fn find_cpi_pattern(start_index: usize, program_ids: &[ProgramId]) -> (Option<Indices>, usize) {
202    let mut index_account = Indices {
203        insert_into_queues: start_index,
204        ..Default::default()
205    };
206    for (index, program_id) in (0..start_index)
207        .rev()
208        .zip(program_ids[..start_index].iter().rev())
209    {
210        if let ProgramId::SolanaSystem = program_id {
211            index_account.found_solana_system_program_instruction = true;
212            continue;
213        } else if matches!(program_id, ProgramId::LightSystem)
214            && index_account.found_solana_system_program_instruction
215            && !index_account.found_system
216        {
217            index_account.system = index;
218            index_account.found_system = true;
219        } else if index_account.found_system && matches!(program_id, ProgramId::LightSystem) {
220            index_account.cpi.push(index);
221        } else if matches!(program_id, ProgramId::AccountCompression) && index_account.found_system
222        {
223            // Possibly found next light transaction.
224            return (Some(index_account), index);
225        } else if !index_account.found_system {
226            // If no system program found pattern incomplete.
227            // Else search for cpi instructions until we find account compression program id.
228            return (None, index);
229        }
230    }
231    if index_account.found_system {
232        (Some(index_account), 0)
233    } else {
234        (None, 0)
235    }
236}
237
238fn wrap_program_ids(
239    program_ids: &[Pubkey],
240    instructions: &[Vec<u8>],
241    accounts: &[Vec<Pubkey>],
242) -> Vec<ProgramId> {
243    let mut vec = Vec::new();
244    for ((instruction, program_id), accounts) in instructions
245        .iter()
246        .zip(program_ids.iter())
247        .zip(accounts.iter())
248    {
249        if instruction.len() < 12 {
250            vec.push(ProgramId::Unknown);
251            continue;
252        }
253        let discriminator: [u8; 8] = instruction[0..8].try_into().unwrap();
254        if program_id == &Pubkey::default() {
255            vec.push(ProgramId::SolanaSystem);
256        } else if program_id == &LIGHT_SYSTEM_PROGRAM_ID {
257            if discriminator == CREATE_CPI_CONTEXT_ACCOUNT {
258                vec.push(ProgramId::Unknown);
259            } else {
260                vec.push(ProgramId::LightSystem);
261            }
262        } else if program_id == &ACCOUNT_COMPRESSION_PROGRAM_ID {
263            if discriminator == DISCRIMINATOR_INSERT_INTO_QUEUES
264                && accounts.len() > 2
265                && accounts[1] == REGISTERED_PROGRAM_PDA
266            {
267                vec.push(ProgramId::AccountCompression);
268            } else {
269                vec.push(ProgramId::Unknown);
270            }
271        } else {
272            vec.push(ProgramId::Unknown);
273        }
274    }
275    vec
276}
277
278fn deserialize_instruction<'a>(
279    instruction: &'a [u8],
280    accounts: &'a [Pubkey],
281) -> Result<ExecutingSystemInstruction<'a>, ParseIndexerEventError> {
282    if instruction.len() < 12 {
283        return Err(ParseIndexerEventError::InstructionDataTooSmall(
284            instruction.len(),
285            12,
286        ));
287    }
288    let instruction_discriminator = instruction[0..8].try_into().unwrap();
289    let instruction = instruction.split_at(8).1;
290    match instruction_discriminator {
291        // Cannot be exucted with cpi context -> executing tx
292        DISCRIMINATOR_INVOKE => {
293            if accounts.len() < 9 {
294                return Err(ParseIndexerEventError::DeserializeSystemInstructionError);
295            }
296            let accounts = accounts.split_at(9).1;
297            // Skips vec size bytes
298            let data = InstructionDataInvoke::deserialize(&mut &instruction[4..])?;
299            Ok(ExecutingSystemInstruction {
300                output_compressed_accounts: data.output_compressed_accounts,
301                input_compressed_accounts: data.input_compressed_accounts_with_merkle_context,
302                is_compress: data.is_compress,
303                relay_fee: data.relay_fee,
304                compress_or_decompress_lamports: data.compress_or_decompress_lamports,
305                execute_cpi_context: false,
306                accounts,
307            })
308        }
309        DISCRIMINATOR_INVOKE_CPI => {
310            if accounts.len() < 11 {
311                return Err(ParseIndexerEventError::DeserializeSystemInstructionError);
312            }
313            let accounts = accounts.split_at(11).1;
314            let data = light_compressed_account::instruction_data::invoke_cpi::InstructionDataInvokeCpi::deserialize(
315                &mut &instruction[4..],
316            )?;
317            Ok(ExecutingSystemInstruction {
318                output_compressed_accounts: data.output_compressed_accounts,
319                input_compressed_accounts: data.input_compressed_accounts_with_merkle_context,
320                is_compress: data.is_compress,
321                relay_fee: data.relay_fee,
322                compress_or_decompress_lamports: data.compress_or_decompress_lamports,
323                execute_cpi_context: data.cpi_context.is_some(),
324                accounts,
325            })
326        }
327        DISCRIMINATOR_INVOKE_CPI_WITH_READ_ONLY => {
328            // Min len for a small instruction 3 accounts + 1 tree or queue
329            // Fee payer + authority + registered program + account compression program + account compression authority
330            if accounts.len() < 5 {
331                return Err(ParseIndexerEventError::DeserializeSystemInstructionError);
332            }
333            let data: InstructionDataInvokeCpiWithReadOnly =
334                InstructionDataInvokeCpiWithReadOnly::deserialize(&mut &instruction[..])?;
335            let system_accounts_len = if data.mode == 0 {
336                11
337            } else {
338                let mut len = 6; // fee_payer + authority + registered_program + account_compression_program + account_compression_authority + system_program
339                if data.compress_or_decompress_lamports > 0 {
340                    len += 1;
341                }
342                if !data.is_compress && data.compress_or_decompress_lamports > 0 {
343                    len += 1;
344                }
345                if data.with_cpi_context {
346                    len += 1;
347                }
348                len
349            };
350
351            let accounts = accounts.split_at(system_accounts_len).1;
352            Ok(ExecutingSystemInstruction {
353                output_compressed_accounts: data.output_compressed_accounts,
354                input_compressed_accounts: data
355                    .input_compressed_accounts
356                    .iter()
357                    .map(|x| {
358                        x.into_packed_compressed_account_with_merkle_context(
359                            data.invoking_program_id,
360                        )
361                    })
362                    .collect::<Vec<_>>(),
363                is_compress: data.is_compress && data.compress_or_decompress_lamports > 0,
364                relay_fee: None,
365                compress_or_decompress_lamports: if data.compress_or_decompress_lamports == 0 {
366                    None
367                } else {
368                    Some(data.compress_or_decompress_lamports)
369                },
370                execute_cpi_context: data.with_cpi_context,
371                accounts,
372            })
373        }
374        INVOKE_CPI_WITH_ACCOUNT_INFO_INSTRUCTION => {
375            // Min len for a small instruction 4 accounts + 1 tree or queue
376            // Fee payer + authority + registered program + account compression program + account compression authority
377            if accounts.len() < 5 {
378                return Err(ParseIndexerEventError::DeserializeSystemInstructionError);
379            }
380            let data: InstructionDataInvokeCpiWithAccountInfo =
381                InstructionDataInvokeCpiWithAccountInfo::deserialize(&mut &instruction[..])?;
382            let system_accounts_len = if data.mode == 0 {
383                11
384            } else {
385                let mut len = 6; // fee_payer + authority + registered_program + account_compression_program + account_compression_authority + system_program
386                if data.compress_or_decompress_lamports > 0 {
387                    len += 1;
388                }
389                if !data.is_compress && data.compress_or_decompress_lamports > 0 {
390                    len += 1;
391                }
392                if data.with_cpi_context {
393                    len += 1;
394                }
395                len
396            };
397            let accounts = accounts.split_at(system_accounts_len).1;
398
399            let instruction = ExecutingSystemInstruction {
400                output_compressed_accounts: data
401                    .account_infos
402                    .iter()
403                    .filter(|x| x.output.is_some())
404                    .map(|x| {
405                        let account = x.output.as_ref().unwrap();
406                        OutputCompressedAccountWithPackedContext {
407                            compressed_account: CompressedAccount {
408                                address: x.address,
409                                owner: data.invoking_program_id,
410                                lamports: account.lamports,
411                                data: Some(CompressedAccountData {
412                                    discriminator: account.discriminator,
413                                    data: account.data.clone(),
414                                    data_hash: account.data_hash,
415                                }),
416                            },
417                            merkle_tree_index: account.output_merkle_tree_index,
418                        }
419                    })
420                    .collect::<Vec<_>>(),
421                input_compressed_accounts: data
422                    .account_infos
423                    .iter()
424                    .filter(|x| x.input.is_some())
425                    .map(|x| {
426                        let account = x.input.as_ref().unwrap();
427                        PackedCompressedAccountWithMerkleContext {
428                            compressed_account: CompressedAccount {
429                                address: x.address,
430                                owner: data.invoking_program_id,
431                                lamports: account.lamports,
432                                data: Some(CompressedAccountData {
433                                    discriminator: account.discriminator,
434                                    data: vec![],
435                                    data_hash: account.data_hash,
436                                }),
437                            },
438                            read_only: false,
439                            root_index: account.root_index,
440                            merkle_context: account.merkle_context,
441                        }
442                    })
443                    .collect::<Vec<_>>(),
444                is_compress: data.is_compress && data.compress_or_decompress_lamports > 0,
445                relay_fee: None,
446                compress_or_decompress_lamports: if data.compress_or_decompress_lamports == 0 {
447                    None
448                } else {
449                    Some(data.compress_or_decompress_lamports)
450                },
451                execute_cpi_context: data.with_cpi_context,
452                accounts,
453            };
454
455            Ok(instruction)
456        }
457        _ => Err(ParseIndexerEventError::DeserializeSystemInstructionError),
458    }
459}
460
461fn create_batched_transaction_event(
462    associated_instructions: &AssociatedInstructions,
463) -> Result<BatchPublicTransactionEvent, ParseIndexerEventError> {
464    let input_sequence_numbers = associated_instructions
465        .insert_into_queues_instruction
466        .input_sequence_numbers
467        .iter()
468        .map(From::from)
469        .filter(|x: &MerkleTreeSequenceNumber| !(*x).is_empty())
470        .collect::<Vec<MerkleTreeSequenceNumber>>();
471    let mut batched_transaction_event = BatchPublicTransactionEvent {
472        event: PublicTransactionEvent {
473            input_compressed_account_hashes: associated_instructions
474                .insert_into_queues_instruction
475                .nullifiers
476                .iter()
477                .map(|x| x.account_hash)
478                .collect(),
479            output_compressed_account_hashes: associated_instructions
480                .insert_into_queues_instruction
481                .leaves
482                .iter()
483                .map(|x| x.leaf)
484                .collect(),
485            output_compressed_accounts: [
486                associated_instructions.cpi_context_outputs.clone(),
487                associated_instructions
488                    .executing_system_instruction
489                    .output_compressed_accounts
490                    .clone(),
491            ]
492            .concat(),
493            output_leaf_indices: associated_instructions
494                .insert_into_queues_instruction
495                .output_leaf_indices
496                .iter()
497                .map(|x| u32::from(*x))
498                .collect(),
499            sequence_numbers: associated_instructions
500                .insert_into_queues_instruction
501                .output_sequence_numbers
502                .iter()
503                .map(From::from)
504                .filter(|x: &MerkleTreeSequenceNumber| !(*x).is_empty())
505                .map(|x| MerkleTreeSequenceNumberV1 {
506                    seq: x.seq,
507                    tree_pubkey: x.tree_pubkey,
508                })
509                .collect(),
510            relay_fee: associated_instructions
511                .executing_system_instruction
512                .relay_fee,
513            is_compress: associated_instructions
514                .executing_system_instruction
515                .is_compress,
516            compress_or_decompress_lamports: associated_instructions
517                .executing_system_instruction
518                .compress_or_decompress_lamports,
519            pubkey_array: associated_instructions
520                .executing_system_instruction
521                .accounts
522                .to_vec(),
523            message: None,
524        },
525        tx_hash: associated_instructions
526            .insert_into_queues_instruction
527            .tx_hash,
528        new_addresses: associated_instructions
529            .insert_into_queues_instruction
530            .addresses
531            .iter()
532            .map(|x| NewAddress {
533                address: x.address,
534                mt_pubkey: associated_instructions.accounts[x.tree_index as usize],
535                queue_index: u64::MAX,
536            })
537            .collect::<Vec<_>>(),
538        address_sequence_numbers: associated_instructions
539            .insert_into_queues_instruction
540            .address_sequence_numbers
541            .iter()
542            .map(From::from)
543            .filter(|x: &MerkleTreeSequenceNumber| !(*x).is_empty())
544            .collect::<Vec<MerkleTreeSequenceNumber>>(),
545        batch_input_accounts: associated_instructions
546            .insert_into_queues_instruction
547            .nullifiers
548            .iter()
549            .filter(|x| {
550                input_sequence_numbers.iter().any(|y| {
551                    y.tree_pubkey == associated_instructions.accounts[x.tree_index as usize]
552                })
553            })
554            .map(|n| {
555                Ok(BatchNullifyContext {
556                    tx_hash: associated_instructions
557                        .insert_into_queues_instruction
558                        .tx_hash,
559                    account_hash: n.account_hash,
560                    nullifier: {
561                        // The nullifier is computed inside the account compression program.
562                        // -> it is not part of the cpi system to account compression program that we index.
563                        // -> we need to compute the nullifier here.
564                        create_nullifier(
565                            &n.account_hash,
566                            n.leaf_index.into(),
567                            &associated_instructions
568                                .insert_into_queues_instruction
569                                .tx_hash,
570                        )?
571                    },
572                    nullifier_queue_index: u64::MAX,
573                })
574            })
575            .collect::<Result<Vec<_>, ParseIndexerEventError>>()?,
576        input_sequence_numbers,
577    };
578
579    let nullifier_queue_indices = create_nullifier_queue_indices(
580        associated_instructions,
581        batched_transaction_event.batch_input_accounts.len(),
582    );
583
584    batched_transaction_event
585        .batch_input_accounts
586        .iter_mut()
587        .zip(nullifier_queue_indices.iter())
588        .for_each(|(context, index)| {
589            context.nullifier_queue_index = *index;
590        });
591
592    let address_queue_indices = create_address_queue_indices(
593        associated_instructions,
594        batched_transaction_event.new_addresses.len(),
595    );
596
597    batched_transaction_event
598        .new_addresses
599        .iter_mut()
600        .zip(address_queue_indices.iter())
601        .for_each(|(context, index)| {
602            context.queue_index = *index;
603        });
604
605    Ok(batched_transaction_event)
606}
607
608fn create_nullifier_queue_indices(
609    associated_instructions: &AssociatedInstructions,
610    len: usize,
611) -> Vec<u64> {
612    let input_merkle_tree_pubkeys = associated_instructions
613        .executing_system_instruction
614        .input_compressed_accounts
615        .iter()
616        .map(|x| {
617            associated_instructions
618                .executing_system_instruction
619                .accounts[x.merkle_context.merkle_tree_pubkey_index as usize]
620        })
621        .collect::<Vec<_>>();
622    let mut nullifier_queue_indices = vec![u64::MAX; len];
623    let mut internal_input_sequence_numbers = associated_instructions
624        .insert_into_queues_instruction
625        .input_sequence_numbers
626        .to_vec();
627    // For every sequence number:
628    // 1. Find every input compressed account
629    // 2. assign sequence number as nullifier queue index
630    // 3. increment the sequence number
631    internal_input_sequence_numbers.iter_mut().for_each(|seq| {
632        for (i, merkle_tree_pubkey) in input_merkle_tree_pubkeys.iter().enumerate() {
633            if *merkle_tree_pubkey == seq.tree_pubkey {
634                nullifier_queue_indices[i] = seq.seq.into();
635                seq.seq += 1;
636            }
637        }
638    });
639    nullifier_queue_indices
640}
641
642fn create_address_queue_indices(
643    associated_instructions: &AssociatedInstructions,
644    len: usize,
645) -> Vec<u64> {
646    let address_merkle_tree_pubkeys = associated_instructions
647        .insert_into_queues_instruction
648        .addresses
649        .iter()
650        .map(|x| associated_instructions.accounts[x.tree_index as usize])
651        .collect::<Vec<_>>();
652    let mut address_queue_indices = vec![u64::MAX; len];
653    let mut internal_address_sequence_numbers = associated_instructions
654        .insert_into_queues_instruction
655        .address_sequence_numbers
656        .to_vec();
657    internal_address_sequence_numbers
658        .iter_mut()
659        .for_each(|seq| {
660            for (i, merkle_tree_pubkey) in address_merkle_tree_pubkeys.iter().enumerate() {
661                if *merkle_tree_pubkey == seq.tree_pubkey {
662                    address_queue_indices[i] = seq.seq.into();
663                    seq.seq += 1;
664                }
665            }
666        });
667    address_queue_indices
668}
669
670#[cfg(test)]
671mod test {
672    use rand::{
673        rngs::{StdRng, ThreadRng},
674        Rng, RngCore, SeedableRng,
675    };
676
677    use super::*;
678    fn get_rnd_program_id<R: Rng>(rng: &mut R, with_system_program: bool) -> ProgramId {
679        let vec = [
680            ProgramId::Unknown,
681            ProgramId::AccountCompression,
682            ProgramId::LightSystem,
683        ];
684        let len = if with_system_program { 3 } else { 2 };
685        let index = rng.gen_range(0..len);
686        vec[index]
687    }
688    fn get_rnd_program_ids<R: Rng>(
689        rng: &mut R,
690        len: usize,
691        with_system_program: bool,
692    ) -> Vec<ProgramId> {
693        (0..len)
694            .map(|_| get_rnd_program_id(rng, with_system_program))
695            .collect()
696    }
697
698    #[test]
699    fn test_rnd_functional() {
700        let mut thread_rng = ThreadRng::default();
701        let seed = thread_rng.next_u64();
702        // Keep this print so that in case the test fails
703        // we can use the seed to reproduce the error.
704        println!("\n\ntest seed {}\n\n", seed);
705        let mut rng = StdRng::seed_from_u64(seed);
706        let num_iters = 100000;
707        for _ in 0..num_iters {
708            let len_pre = rng.gen_range(0..6);
709            let rnd_vec_pre = get_rnd_program_ids(&mut rng, len_pre, false);
710            let len_post = rng.gen_range(0..6);
711            let rnd_vec_post = get_rnd_program_ids(&mut rng, len_post, false);
712            let num_mid = rng.gen_range(1..6);
713
714            let program_ids = [
715                rnd_vec_pre.as_slice(),
716                [ProgramId::LightSystem].as_slice(),
717                vec![ProgramId::SolanaSystem; num_mid].as_slice(),
718                [ProgramId::AccountCompression].as_slice(),
719                rnd_vec_post.as_slice(),
720            ]
721            .concat();
722            let start_index = program_ids.len() - 1 - len_post;
723            let system_index = program_ids.len() - 1 - len_post - num_mid - 1;
724            let vec = find_cpi_patterns(&program_ids);
725            let expected = Indices {
726                system: system_index,
727                cpi: vec![],
728                insert_into_queues: start_index,
729                found_solana_system_program_instruction: true,
730                found_system: true,
731            };
732            assert!(
733                vec.contains(&expected),
734                "program ids {:?} parsed events  {:?} expected {:?} ",
735                program_ids,
736                vec,
737                expected,
738            );
739        }
740
741        for _ in 0..num_iters {
742            let len_pre = rng.gen_range(0..6);
743            let rnd_vec_pre = get_rnd_program_ids(&mut rng, len_pre, true);
744            let len_post = rng.gen_range(0..6);
745            let rnd_vec_post = get_rnd_program_ids(&mut rng, len_post, true);
746            let num_mid = rng.gen_range(1..6);
747
748            let program_ids = [
749                rnd_vec_pre.as_slice(),
750                [ProgramId::LightSystem].as_slice(),
751                vec![ProgramId::SolanaSystem; num_mid].as_slice(),
752                [ProgramId::AccountCompression].as_slice(),
753                rnd_vec_post.as_slice(),
754            ]
755            .concat();
756            let start_index = program_ids.len() - 1 - len_post;
757            let system_index = program_ids.len() - 1 - len_post - num_mid - 1;
758            let vec = find_cpi_patterns(&program_ids);
759            let expected = Indices {
760                system: system_index,
761                cpi: vec![],
762                insert_into_queues: start_index,
763                found_solana_system_program_instruction: true,
764                found_system: true,
765            };
766            assert!(
767                vec.iter().any(|x| x.system == expected.system
768                    && x.insert_into_queues == expected.insert_into_queues),
769                "program ids {:?} parsed events  {:?} expected {:?} ",
770                program_ids,
771                vec,
772                expected,
773            );
774        }
775    }
776
777    #[test]
778    fn test_rnd_failing() {
779        let mut thread_rng = ThreadRng::default();
780        let seed = thread_rng.next_u64();
781        // Keep this print so that in case the test fails
782        // we can use the seed to reproduce the error.
783        println!("\n\ntest seed {}\n\n", seed);
784        let mut rng = StdRng::seed_from_u64(seed);
785        let num_iters = 100000;
786        for _ in 0..num_iters {
787            let len = rng.gen_range(0..20);
788            let mut program_ids = get_rnd_program_ids(&mut rng, len, true);
789            // if any ProgramId::LightSystem is followed by ProgramId::SolanaSystem overwrite ProgramId::SolanaSystem with ProgramId::Unknown
790            for i in 0..program_ids.len().saturating_sub(1) {
791                if matches!(program_ids[i], ProgramId::LightSystem)
792                    && matches!(program_ids[i + 1], ProgramId::SolanaSystem)
793                {
794                    program_ids[i + 1] = ProgramId::Unknown;
795                }
796            }
797
798            let vec = find_cpi_patterns(&program_ids);
799
800            assert!(
801                vec.is_empty(),
802                "program_ids {:?} result {:?}",
803                program_ids,
804                vec
805            );
806        }
807    }
808
809    #[test]
810    fn test_find_two_patterns() {
811        // Std pattern
812        {
813            let program_ids = vec![
814                ProgramId::Unknown,
815                ProgramId::LightSystem,
816                ProgramId::SolanaSystem,
817                ProgramId::AccountCompression,
818                ProgramId::Unknown,
819                ProgramId::LightSystem,
820                ProgramId::SolanaSystem,
821                ProgramId::AccountCompression,
822            ];
823            let vec = find_cpi_patterns(&program_ids);
824            assert_eq!(vec.len(), 2);
825            assert_eq!(
826                vec[0],
827                Indices {
828                    system: 5,
829                    cpi: vec![],
830                    insert_into_queues: 7,
831                    found_solana_system_program_instruction: true,
832                    found_system: true,
833                }
834            );
835            assert_eq!(
836                vec[1],
837                Indices {
838                    system: 1,
839                    cpi: vec![],
840                    insert_into_queues: 3,
841                    found_solana_system_program_instruction: true,
842                    found_system: true,
843                }
844            );
845            // Modify only second event is valid
846            {
847                let mut program_ids = program_ids.clone();
848                program_ids[2] = ProgramId::Unknown;
849                let vec = find_cpi_patterns(&program_ids);
850                assert_eq!(vec.len(), 1);
851                assert_eq!(
852                    vec[0],
853                    Indices {
854                        system: 5,
855                        cpi: vec![],
856                        insert_into_queues: 7,
857                        found_solana_system_program_instruction: true,
858                        found_system: true,
859                    }
860                );
861            }
862            // Modify only first event is valid
863            {
864                let mut program_ids = program_ids;
865                program_ids[6] = ProgramId::Unknown;
866                let vec = find_cpi_patterns(&program_ids);
867                assert_eq!(vec.len(), 1);
868                assert_eq!(
869                    vec[0],
870                    Indices {
871                        system: 1,
872                        cpi: vec![],
873                        insert_into_queues: 3,
874                        found_solana_system_program_instruction: true,
875                        found_system: true,
876                    }
877                );
878            }
879        }
880    }
881
882    #[test]
883    fn test_find_pattern() {
884        // Std pattern
885        {
886            let program_ids = vec![
887                ProgramId::Unknown,
888                ProgramId::LightSystem,
889                ProgramId::SolanaSystem,
890                ProgramId::AccountCompression,
891            ];
892            let (res, last_index) = find_cpi_pattern(3, &program_ids);
893            assert_eq!(last_index, 0);
894            assert_eq!(
895                res,
896                Some(Indices {
897                    system: 1,
898                    cpi: vec![],
899                    insert_into_queues: 3,
900                    found_solana_system_program_instruction: true,
901                    found_system: true,
902                })
903            );
904        }
905        {
906            let program_ids = vec![
907                ProgramId::Unknown,
908                ProgramId::LightSystem,
909                ProgramId::SolanaSystem,
910                ProgramId::SolanaSystem,
911                ProgramId::SolanaSystem,
912                ProgramId::AccountCompression,
913            ];
914            let start_index = program_ids.len() - 1;
915            let (res, last_index) = find_cpi_pattern(start_index, &program_ids);
916            assert_eq!(last_index, 0);
917            assert_eq!(
918                res,
919                Some(Indices {
920                    system: 1,
921                    cpi: vec![],
922                    insert_into_queues: start_index,
923                    found_solana_system_program_instruction: true,
924                    found_system: true,
925                })
926            );
927        }
928        {
929            let program_ids = vec![
930                ProgramId::Unknown,
931                ProgramId::LightSystem,
932                ProgramId::SolanaSystem,
933                ProgramId::Unknown,
934                ProgramId::SolanaSystem,
935                ProgramId::AccountCompression,
936            ];
937            let start_index = program_ids.len() - 1;
938            let (res, last_index) = find_cpi_pattern(start_index, &program_ids);
939            assert_eq!(last_index, 3);
940            assert_eq!(res, None);
941        }
942        // With cpi context
943        {
944            let program_ids = vec![
945                ProgramId::Unknown,
946                ProgramId::LightSystem,
947                ProgramId::Unknown,
948                ProgramId::LightSystem,
949                ProgramId::SolanaSystem,
950                ProgramId::SolanaSystem,
951                ProgramId::SolanaSystem,
952                ProgramId::AccountCompression,
953            ];
954            let start_index = program_ids.len() - 1;
955            let (res, last_index) = find_cpi_pattern(start_index, &program_ids);
956            assert_eq!(last_index, 0);
957            assert_eq!(
958                res,
959                Some(Indices {
960                    system: 3,
961                    cpi: vec![1],
962                    insert_into_queues: start_index,
963                    found_solana_system_program_instruction: true,
964                    found_system: true,
965                })
966            );
967            // Failing
968            {
969                let mut program_ids = program_ids;
970                program_ids[5] = ProgramId::Unknown;
971                let (res, last_index) = find_cpi_pattern(start_index, &program_ids);
972                assert_eq!(last_index, 5);
973                assert_eq!(res, None);
974            }
975        }
976        // With cpi context
977        {
978            let program_ids = vec![
979                ProgramId::Unknown,
980                ProgramId::LightSystem,
981                ProgramId::LightSystem,
982                ProgramId::SolanaSystem,
983                ProgramId::SolanaSystem,
984                ProgramId::SolanaSystem,
985                ProgramId::AccountCompression,
986            ];
987            let start_index = program_ids.len() - 1;
988            let (res, last_index) = find_cpi_pattern(start_index, &program_ids);
989            assert_eq!(last_index, 0);
990            assert_eq!(
991                res,
992                Some(Indices {
993                    system: 2,
994                    cpi: vec![1],
995                    insert_into_queues: start_index,
996                    found_solana_system_program_instruction: true,
997                    found_system: true,
998                })
999            );
1000            // Failing
1001            {
1002                let mut program_ids = program_ids;
1003                program_ids[4] = ProgramId::Unknown;
1004                let (res, last_index) = find_cpi_pattern(start_index, &program_ids);
1005                assert_eq!(last_index, 4);
1006                assert_eq!(res, None);
1007            }
1008        }
1009    }
1010}