Skip to main content

carbon_core/
instruction.rs

1//! Instruction decoding, CPI nesting, and instruction-shaped pipe wiring.
2//!
3//! # Components
4//!
5//! - [`InstructionMetadata`] — slot/tx-context surrounding a single instruction
6//!   (`stack_height`, `index`, `absolute_path` for CPI tree position).
7//! - [`InstructionDecoder`] — user trait mapping raw `Instruction` → typed
8//!   `Self::InstructionType`.
9//! - [`InstructionProcessorInputType<'a, T>`] — borrowed bundle delivered to
10//!   processors (metadata + decoded body + nested children + raw).
11//! - [`InstructionPipe`] / [`InstructionPipes`] — internal pipe wrapping
12//!   decoder + processor + filters; constructed by `PipelineBuilder`.
13//! - [`NestedInstruction`] / [`NestedInstructions`] — recursive CPI tree
14//!   rebuilt from the flat `(InstructionMetadata, Instruction)` list.
15//! - [`UnsafeNestedBuilder`] — internal builder that turns a flat list into a
16//!   nested tree without reallocating mid-build (uses raw pointers under the
17//!   hood; safety invariants documented inline).
18//! - [`MAX_INSTRUCTION_STACK_DEPTH`] — Solana's per-transaction CPI depth
19//!   ceiling (5).
20
21use {
22    crate::{
23        deserialize::CarbonDeserialize, error::CarbonResult, filter::Filter, processor::Processor,
24        transaction::TransactionMetadata,
25    },
26    async_trait::async_trait,
27    std::{
28        ops::{Deref, DerefMut},
29        sync::Arc,
30    },
31};
32
33/// Per-instruction context: which transaction it belongs to, where in
34/// the CPI tree it sits, and what its position is among siblings.
35#[derive(Debug, Clone)]
36pub struct InstructionMetadata {
37    pub transaction_metadata: Arc<TransactionMetadata>,
38    pub stack_height: u32,
39    pub index: u32,
40    pub absolute_path: Vec<u8>,
41}
42
43#[derive(Debug)]
44enum LogType {
45    Start(usize), // stack_height
46    Data,
47    CU,
48    Finish,
49}
50
51const PRECOMPILE_PROGRAMS: &[&str] = &[
52    "Ed25519SigVerify111111111111111111111111111",
53    "KeccakSecp256k11111111111111111111111111111",
54    "Secp256r1SigVerify1111111111111111111111111",
55];
56
57// https://github.com/anza-xyz/agave/blob/master/program-runtime/src/execution_budget.rs#L7
58pub const MAX_INSTRUCTION_STACK_DEPTH: usize = 5;
59
60impl InstructionMetadata {
61    pub fn decode_log_events<T: CarbonDeserialize>(&self) -> Vec<T> {
62        self.extract_event_log_data()
63            .into_iter()
64            .filter_map(|log| <T as CarbonDeserialize>::deserialize(&log))
65            .collect()
66    }
67
68    fn extract_event_log_data(&self) -> Vec<Vec<u8>> {
69        let logs = match &self.transaction_metadata.meta.log_messages {
70            Some(logs) => logs,
71            None => return Vec::new(),
72        };
73
74        let precompile_offset = self.count_precompiles_before_index();
75        let adjusted_absolute_path: Vec<u8> = if !self.absolute_path.is_empty() {
76            let mut adjusted = self.absolute_path.clone();
77            adjusted[0] = adjusted[0].saturating_sub(precompile_offset as u8);
78            adjusted
79        } else {
80            self.absolute_path.clone()
81        };
82
83        let mut extracted_logs = Vec::new();
84        let mut current_stack_height = 0usize;
85        let mut last_stack_height = 0usize;
86
87        let mut position_at_level: std::collections::HashMap<usize, u8> =
88            std::collections::HashMap::new();
89
90        for log in logs {
91            let parsed_log = self.parse_log(log);
92
93            match parsed_log {
94                LogType::Start(stack_height) => {
95                    current_stack_height = stack_height;
96
97                    let current_pos = if stack_height > last_stack_height {
98                        0
99                    } else {
100                        position_at_level
101                            .get(&stack_height)
102                            .map(|&pos| pos + 1)
103                            .unwrap_or(0)
104                    };
105
106                    position_at_level.insert(stack_height, current_pos);
107                    last_stack_height = stack_height;
108                }
109                LogType::Finish => {
110                    current_stack_height = current_stack_height.saturating_sub(1);
111                }
112                _ => {}
113            }
114
115            let current_path: Vec<u8> = (1..=current_stack_height)
116                .map(|level| position_at_level.get(&level).copied().unwrap_or(0))
117                .collect();
118
119            if current_path == adjusted_absolute_path && matches!(parsed_log, LogType::Data) {
120                if let Some(data) = log.split_whitespace().last() {
121                    if let Ok(buf) =
122                        base64::Engine::decode(&base64::engine::general_purpose::STANDARD, data)
123                    {
124                        extracted_logs.push(buf);
125                    }
126                }
127            }
128        }
129
130        extracted_logs
131    }
132
133    fn count_precompiles_before_index(&self) -> usize {
134        if self.absolute_path.is_empty() {
135            return 0;
136        }
137
138        let outer_index = self.absolute_path[0] as usize;
139        let account_keys = self.transaction_metadata.message.static_account_keys();
140        let instructions = self.transaction_metadata.message.instructions();
141
142        let mut precompile_count = 0;
143        for (idx, ix) in instructions.iter().enumerate() {
144            if idx >= outer_index {
145                break;
146            }
147            if let Some(program_id) = account_keys.get(ix.program_id_index as usize) {
148                let program_id_str = program_id.to_string();
149                if PRECOMPILE_PROGRAMS.contains(&program_id_str.as_str()) {
150                    precompile_count += 1;
151                }
152            }
153        }
154
155        precompile_count
156    }
157
158    fn parse_log(&self, log: &str) -> LogType {
159        if log.starts_with("Program ") && log.contains(" invoke [") {
160            let parts: Vec<&str> = log.split_whitespace().collect();
161            if parts.len() >= 4 && parts[0] == "Program" && parts[2] == "invoke" {
162                let level_str = parts[3].trim_start_matches('[').trim_end_matches(']');
163                if let Ok(level) = level_str.parse::<usize>() {
164                    return LogType::Start(level);
165                }
166            }
167        } else if log.starts_with("Program ")
168            && (log.ends_with(" success") || log.contains(" failed"))
169        {
170            let parts: Vec<&str> = log.split_whitespace().collect();
171            if parts.len() >= 3 && parts[0] == "Program" {
172                return LogType::Finish;
173            }
174        } else if log.contains("consumed") && log.contains("compute units") {
175            return LogType::CU;
176        }
177
178        LogType::Data
179    }
180}
181
182pub type InstructionsWithMetadata = Vec<(InstructionMetadata, solana_instruction::Instruction)>;
183
184/// User-implemented decoder mapping a raw `solana_instruction::Instruction`
185/// to a typed `Self::InstructionType`. Returning `None` skips the
186/// instruction for this pipe.
187pub trait InstructionDecoder<'a> {
188    type InstructionType;
189
190    fn decode_instruction(
191        &self,
192        instruction: &'a solana_instruction::Instruction,
193    ) -> Option<Self::InstructionType>;
194}
195
196/// Borrowed bundle delivered to a
197/// `Processor<InstructionProcessorInputType<T>>`: metadata, decoded body, child
198/// CPIs, and the raw instruction.
199#[derive(Debug)]
200pub struct InstructionProcessorInputType<'a, T> {
201    pub metadata: &'a InstructionMetadata,
202    pub decoded_instruction: &'a T,
203    pub nested_instructions: &'a NestedInstructions,
204    pub raw_instruction: &'a solana_instruction::Instruction,
205}
206
207pub struct InstructionPipe<T: Send, P> {
208    decoder: Box<dyn for<'a> InstructionDecoder<'a, InstructionType = T> + Send + Sync + 'static>,
209    processor: P,
210    filters: Vec<Box<dyn Filter + 'static>>,
211}
212
213impl<T: Send, P> InstructionPipe<T, P> {
214    pub fn new(
215        decoder: Box<
216            dyn for<'a> InstructionDecoder<'a, InstructionType = T> + Send + Sync + 'static,
217        >,
218        processor: P,
219        filters: Vec<Box<dyn Filter + 'static>>,
220    ) -> Self {
221        Self {
222            decoder,
223            processor,
224            filters,
225        }
226    }
227}
228
229#[async_trait]
230pub trait InstructionPipes<'a>: Send + Sync {
231    async fn run(&mut self, nested_instruction: &NestedInstruction) -> CarbonResult<()>;
232
233    fn filters(&self) -> &[Box<dyn Filter + 'static>];
234}
235
236#[async_trait]
237impl<T, P> InstructionPipes<'_> for InstructionPipe<T, P>
238where
239    T: Send + Sync + 'static,
240    P: for<'a> Processor<InstructionProcessorInputType<'a, T>> + Send + Sync + 'static,
241{
242    async fn run(&mut self, nested_instruction: &NestedInstruction) -> CarbonResult<()> {
243        if let Some(decoded_instruction) = self
244            .decoder
245            .decode_instruction(&nested_instruction.instruction)
246        {
247            let data = InstructionProcessorInputType {
248                metadata: &nested_instruction.metadata,
249                decoded_instruction: &decoded_instruction,
250                nested_instructions: &nested_instruction.inner_instructions,
251                raw_instruction: &nested_instruction.instruction,
252            };
253
254            self.processor.process(&data).await?;
255        }
256
257        Ok(())
258    }
259
260    fn filters(&self) -> &[Box<dyn Filter + 'static>] {
261        &self.filters
262    }
263}
264
265/// A node in the CPI tree: one instruction plus the inner instructions
266/// it invoked.
267#[derive(Debug, Clone)]
268pub struct NestedInstruction {
269    pub metadata: InstructionMetadata,
270    pub instruction: solana_instruction::Instruction,
271    pub inner_instructions: NestedInstructions,
272}
273
274/// Ordered collection of `NestedInstruction`s — typically the
275/// instructions of one transaction or one CPI subtree. Derefs to
276/// `&[NestedInstruction]`.
277#[derive(Debug, Default)]
278pub struct NestedInstructions(pub Vec<NestedInstruction>);
279
280impl NestedInstructions {
281    pub fn len(&self) -> usize {
282        self.0.len()
283    }
284
285    pub fn is_empty(&self) -> bool {
286        self.len() == 0
287    }
288
289    pub fn push(&mut self, nested_instruction: NestedInstruction) {
290        self.0.push(nested_instruction);
291    }
292}
293
294impl Deref for NestedInstructions {
295    type Target = [NestedInstruction];
296
297    fn deref(&self) -> &[NestedInstruction] {
298        &self.0[..]
299    }
300}
301
302impl DerefMut for NestedInstructions {
303    fn deref_mut(&mut self) -> &mut [NestedInstruction] {
304        &mut self.0[..]
305    }
306}
307
308impl Clone for NestedInstructions {
309    fn clone(&self) -> Self {
310        NestedInstructions(self.0.clone())
311    }
312}
313
314impl IntoIterator for NestedInstructions {
315    type Item = NestedInstruction;
316    type IntoIter = std::vec::IntoIter<NestedInstruction>;
317
318    fn into_iter(self) -> Self::IntoIter {
319        self.0.into_iter()
320    }
321}
322
323impl From<InstructionsWithMetadata> for NestedInstructions {
324    fn from(instructions: InstructionsWithMetadata) -> Self {
325        // To avoid reallocations that result in dangling pointers.
326        // Therefore the number of "push"s must be calculated to set the capacity
327        let estimated_capacity = instructions
328            .iter()
329            .filter(|(meta, _)| meta.stack_height == 1)
330            .count();
331
332        UnsafeNestedBuilder::new(estimated_capacity).build(instructions)
333    }
334}
335
336pub struct UnsafeNestedBuilder {
337    nested_ixs: Vec<NestedInstruction>,
338    level_ptrs: [Option<*mut NestedInstruction>; MAX_INSTRUCTION_STACK_DEPTH],
339}
340
341impl UnsafeNestedBuilder {
342    pub fn new(capacity: usize) -> Self {
343        Self {
344            nested_ixs: Vec::with_capacity(capacity),
345            level_ptrs: [None; MAX_INSTRUCTION_STACK_DEPTH],
346        }
347    }
348
349    pub fn build(mut self, instructions: InstructionsWithMetadata) -> NestedInstructions {
350        for (metadata, instruction) in instructions {
351            let stack_height = metadata.stack_height as usize;
352
353            assert!(stack_height > 0);
354            assert!(stack_height <= MAX_INSTRUCTION_STACK_DEPTH);
355
356            for ptr in &mut self.level_ptrs[stack_height..] {
357                *ptr = None;
358            }
359
360            let new_instruction = NestedInstruction {
361                metadata,
362                instruction,
363                inner_instructions: NestedInstructions::default(),
364            };
365
366            // SAFETY:The following operation is safe.
367            // because:
368            // 1. All pointers come from pre-allocated Vec (no extension)
369            // 2. level_ptr does not guarantee any aliasing
370            // 3. Lifecycle is limited to the build() method
371            unsafe {
372                if stack_height == 1 {
373                    self.nested_ixs.push(new_instruction);
374                    let ptr = self.nested_ixs.last_mut().unwrap_unchecked() as *mut _;
375                    self.level_ptrs[0] = Some(ptr);
376                } else if let Some(parent_ptr) = self.level_ptrs[stack_height - 2] {
377                    (*parent_ptr).inner_instructions.push(new_instruction);
378                    let ptr = (*parent_ptr)
379                        .inner_instructions
380                        .last_mut()
381                        .unwrap_unchecked() as *mut _;
382                    self.level_ptrs[stack_height - 1] = Some(ptr);
383                }
384            }
385        }
386
387        NestedInstructions(self.nested_ixs)
388    }
389}
390
391#[cfg(test)]
392mod tests {
393
394    use {
395        super::*,
396        solana_instruction::{AccountMeta, Instruction},
397        solana_pubkey::Pubkey,
398        solana_transaction_status::TransactionStatusMeta,
399        std::str::FromStr,
400    };
401
402    fn create_instruction_with_metadata(
403        index: u32,
404        stack_height: u32,
405        absolute_path: Vec<u8>,
406    ) -> (InstructionMetadata, Instruction) {
407        let metadata = InstructionMetadata {
408            transaction_metadata: Arc::new(TransactionMetadata {
409                meta: TransactionStatusMeta {
410                    log_messages: Some(vec!["Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK invoke [1]".to_string(), "Program data: QMbN6CYIceLh9Vdh3ndmrpChVVDCYAykCoHLEYdQWNcAxLJNu7nWNHiJzugda0JT2xgyBCWGtm7/oWjb/wT2kcbwA0JRUuwSV88ABSiDPpXudmLYK2jIBhqh3sTXxnR7WMgtjWsyqjga53NruXU9Dj/hyRRE/RQ9xCEh3052KbW6tbtNksNK4HIr+0wAAAAAAAAAAAAAAACz/t2FxQIAAAAAAAAAAAAAACdJpynsFrOoMAAAAAAAAAD4JhBoAxAAAAAAAAAAAAAAhC8BAA==".to_string(), "Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK consumed 91799 of 185765 compute units".to_string(), "Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK success".to_string()]),
411                    ..Default::default()
412                },
413                ..Default::default()
414            }),
415            stack_height,
416            index,
417            absolute_path,
418        };
419        let instruction = Instruction {
420            program_id: Pubkey::new_unique(),
421            accounts: vec![AccountMeta::new(Pubkey::new_unique(), false)],
422            data: vec![],
423        };
424        (metadata, instruction)
425    }
426
427    fn create_metadata_with_message(
428        absolute_path: Vec<u8>,
429        stack_height: u32,
430        logs: Vec<String>,
431        account_keys: Vec<Pubkey>,
432        instructions: Vec<solana_message::compiled_instruction::CompiledInstruction>,
433    ) -> InstructionMetadata {
434        use solana_message::{legacy::Message as LegacyMessage, VersionedMessage};
435
436        let message = VersionedMessage::Legacy(LegacyMessage {
437            header: solana_message::MessageHeader {
438                num_required_signatures: 1,
439                num_readonly_signed_accounts: 0,
440                num_readonly_unsigned_accounts: 0,
441            },
442            account_keys,
443            recent_blockhash: solana_hash::Hash::default(),
444            instructions,
445        });
446
447        InstructionMetadata {
448            transaction_metadata: Arc::new(TransactionMetadata {
449                meta: TransactionStatusMeta {
450                    log_messages: Some(logs),
451                    ..Default::default()
452                },
453                message,
454                ..Default::default()
455            }),
456            stack_height,
457            index: absolute_path.first().copied().unwrap_or(0) as u32,
458            absolute_path,
459        }
460    }
461
462    #[test]
463    fn test_nested_instructions_single_level() {
464        let instructions = vec![
465            create_instruction_with_metadata(1, 1, vec![1]),
466            create_instruction_with_metadata(2, 1, vec![2]),
467        ];
468        let nested_instructions: NestedInstructions = instructions.into();
469        assert_eq!(nested_instructions.len(), 2);
470        assert!(nested_instructions[0].inner_instructions.is_empty());
471        assert!(nested_instructions[1].inner_instructions.is_empty());
472    }
473
474    #[test]
475    fn test_nested_instructions_empty() {
476        let instructions: InstructionsWithMetadata = vec![];
477        let nested_instructions: NestedInstructions = instructions.into();
478        assert!(nested_instructions.is_empty());
479    }
480
481    #[test]
482    fn test_deep_nested_instructions() {
483        let instructions = vec![
484            create_instruction_with_metadata(0, 1, vec![0]),
485            create_instruction_with_metadata(0, 1, vec![0]),
486            create_instruction_with_metadata(1, 2, vec![0, 1]),
487            create_instruction_with_metadata(1, 3, vec![0, 1, 1]),
488            create_instruction_with_metadata(1, 3, vec![0, 1, 1]),
489            create_instruction_with_metadata(1, 3, vec![0, 1, 1]),
490            create_instruction_with_metadata(1, 3, vec![0, 1, 1]),
491        ];
492
493        let nested_instructions: NestedInstructions = instructions.into();
494        assert_eq!(nested_instructions.len(), 2);
495        assert_eq!(nested_instructions.0[1].inner_instructions.len(), 1);
496    }
497
498    #[test]
499    fn test_decode_log_events_accepts_payloads_under_eight_bytes() {
500        #[derive(crate::borsh::BorshDeserialize)]
501        struct TinyEvent {
502            value: u8,
503        }
504
505        impl CarbonDeserialize for TinyEvent {
506            const DISCRIMINATOR: &'static [u8] = &[9];
507
508            fn deserialize(data: &[u8]) -> Option<Self> {
509                let payload = data.strip_prefix(Self::DISCRIMINATOR)?;
510                crate::borsh::BorshDeserialize::try_from_slice(payload).ok()
511            }
512        }
513
514        let metadata = create_metadata_with_message(
515            vec![0],
516            1,
517            vec![
518                "Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK invoke [1]".to_string(),
519                "Program data: CSo=".to_string(),
520                "Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK success".to_string(),
521            ],
522            vec![],
523            vec![],
524        );
525
526        let events = metadata.decode_log_events::<TinyEvent>();
527        assert_eq!(events.len(), 1);
528        assert_eq!(events[0].value, 42);
529    }
530
531    #[test]
532    fn test_extract_event_log_data() {
533        let logs = create_instruction_with_metadata(0, 1, vec![0])
534            .0
535            .extract_event_log_data();
536        assert_eq!(logs.len(), 1);
537        assert_eq!(
538            logs[0],
539            base64::Engine::decode(
540                &base64::engine::general_purpose::STANDARD,
541                "QMbN6CYIceLh9Vdh3ndmrpChVVDCYAykCoHLEYdQWNcAxLJNu7nWNHiJzugda0JT2xgyBCWGtm7/oWjb/wT2kcbwA0JRUuwSV88ABSiDPpXudmLYK2jIBhqh3sTXxnR7WMgtjWsyqjga53NruXU9Dj/hyRRE/RQ9xCEh3052KbW6tbtNksNK4HIr+0wAAAAAAAAAAAAAAACz/t2FxQIAAAAAAAAAAAAAACdJpynsFrOoMAAAAAAAAAD4JhBoAxAAAAAAAAAAAAAAhC8BAA=="
542            )
543            .expect("decode base64")
544        );
545    }
546
547    #[test]
548    fn test_count_precompiles_before_index_no_precompiles() {
549        use solana_message::compiled_instruction::CompiledInstruction;
550
551        // Transaction structure:
552        // ix 0: program1
553        // ix 1: program2
554        //
555        // No precompiles present, so count before ix 1 should be 0.
556
557        let program1 = Pubkey::new_unique();
558        let program2 = Pubkey::new_unique();
559
560        let metadata = create_metadata_with_message(
561            vec![1],
562            1,
563            vec![],
564            vec![Pubkey::new_unique(), program1, program2],
565            vec![
566                CompiledInstruction {
567                    program_id_index: 1,
568                    accounts: vec![],
569                    data: vec![],
570                },
571                CompiledInstruction {
572                    program_id_index: 2,
573                    accounts: vec![],
574                    data: vec![],
575                },
576            ],
577        );
578
579        assert_eq!(metadata.count_precompiles_before_index(), 0);
580    }
581
582    #[test]
583    fn test_count_precompiles_before_index_with_ed25519() {
584        use solana_message::compiled_instruction::CompiledInstruction;
585
586        // Transaction structure:
587        // ix 0: Ed25519 precompile
588        // ix 1: program1
589        // ix 2: program2
590        //
591        // One precompile before ix 2, so count should be 1.
592
593        let ed25519 = Pubkey::from_str("Ed25519SigVerify111111111111111111111111111").unwrap();
594        let program1 = Pubkey::new_unique();
595        let program2 = Pubkey::new_unique();
596
597        let metadata = create_metadata_with_message(
598            vec![2],
599            1,
600            vec![],
601            vec![Pubkey::new_unique(), ed25519, program1, program2],
602            vec![
603                CompiledInstruction {
604                    program_id_index: 1,
605                    accounts: vec![],
606                    data: vec![],
607                },
608                CompiledInstruction {
609                    program_id_index: 2,
610                    accounts: vec![],
611                    data: vec![],
612                },
613                CompiledInstruction {
614                    program_id_index: 3,
615                    accounts: vec![],
616                    data: vec![],
617                },
618            ],
619        );
620
621        assert_eq!(metadata.count_precompiles_before_index(), 1);
622    }
623
624    #[test]
625    fn test_count_precompiles_before_index_with_multiple_precompiles() {
626        use solana_message::compiled_instruction::CompiledInstruction;
627
628        // Transaction structure:
629        // ix 0: Ed25519 precompile
630        // ix 1: Secp256k1 precompile
631        // ix 2: program1
632        // ix 3: program2
633        //
634        // Two precompiles before ix 3, so count should be 2.
635
636        let ed25519 = Pubkey::from_str("Ed25519SigVerify111111111111111111111111111").unwrap();
637        let secp256k1 = Pubkey::from_str("KeccakSecp256k11111111111111111111111111111").unwrap();
638        let program1 = Pubkey::new_unique();
639        let program2 = Pubkey::new_unique();
640
641        let metadata = create_metadata_with_message(
642            vec![3],
643            1,
644            vec![],
645            vec![Pubkey::new_unique(), ed25519, secp256k1, program1, program2],
646            vec![
647                CompiledInstruction {
648                    program_id_index: 1,
649                    accounts: vec![],
650                    data: vec![],
651                },
652                CompiledInstruction {
653                    program_id_index: 2,
654                    accounts: vec![],
655                    data: vec![],
656                },
657                CompiledInstruction {
658                    program_id_index: 3,
659                    accounts: vec![],
660                    data: vec![],
661                },
662                CompiledInstruction {
663                    program_id_index: 4,
664                    accounts: vec![],
665                    data: vec![],
666                },
667            ],
668        );
669
670        assert_eq!(metadata.count_precompiles_before_index(), 2);
671    }
672
673    #[test]
674    fn test_extract_event_log_data_with_precompile_offset() {
675        use solana_message::compiled_instruction::CompiledInstruction;
676
677        // Transaction structure:
678        // ix 0: ComputeBudget (logs invoke [1])
679        // ix 1: Ed25519 precompile (NO logs)
680        // ix 2: target_program (logs invoke [1])
681        //
682        // Log positions (ignoring precompiles):
683        // Position [0] = ComputeBudget
684        // Position [1] = target_program (message index 2, but log position 1 due to 1
685        // precompile)
686
687        let compute_budget = Pubkey::new_unique();
688        let ed25519 = Pubkey::from_str("Ed25519SigVerify111111111111111111111111111").unwrap();
689        let target_program = Pubkey::new_unique();
690
691        let logs = vec![
692            "Program ComputeBudget111111111111111111111111111 invoke [1]".to_string(),
693            "Program ComputeBudget111111111111111111111111111 success".to_string(),
694            format!("Program {} invoke [1]", target_program),
695            "Program data: dGVzdF9kYXRh".to_string(), // "test_data" in base64
696            format!("Program {} success", target_program),
697        ];
698
699        let metadata = create_metadata_with_message(
700            vec![2],
701            1,
702            logs,
703            vec![
704                Pubkey::new_unique(),
705                compute_budget,
706                ed25519,
707                target_program,
708            ],
709            vec![
710                CompiledInstruction {
711                    program_id_index: 1,
712                    accounts: vec![],
713                    data: vec![],
714                },
715                CompiledInstruction {
716                    program_id_index: 2,
717                    accounts: vec![],
718                    data: vec![],
719                },
720                CompiledInstruction {
721                    program_id_index: 3,
722                    accounts: vec![],
723                    data: vec![],
724                },
725            ],
726        );
727
728        let extracted = metadata.extract_event_log_data();
729        assert_eq!(extracted.len(), 1);
730        assert_eq!(extracted[0], b"test_data");
731    }
732
733    #[test]
734    fn test_extract_event_log_data_cpi_with_precompile_offset() {
735        use solana_message::compiled_instruction::CompiledInstruction;
736
737        // Transaction structure:
738        // ix 0: ComputeBudget (logs invoke [1])
739        // ix 1: Ed25519 precompile (NO logs)
740        // ix 2: router_program (logs invoke [1])
741        //   -> CPI to target_program (logs invoke [2])
742        //
743        // Log positions (ignoring precompiles):
744        // Position [0] = ComputeBudget
745        // Position [1] = router_program (message index 2, but log position 1 due to 1
746        // precompile) Position [1, 0] = target_program CPI
747
748        let compute_budget = Pubkey::new_unique();
749        let ed25519 = Pubkey::from_str("Ed25519SigVerify111111111111111111111111111").unwrap();
750        let router_program = Pubkey::new_unique();
751        let target_program = Pubkey::new_unique();
752
753        let logs = vec![
754            "Program ComputeBudget111111111111111111111111111 invoke [1]".to_string(),
755            "Program ComputeBudget111111111111111111111111111 success".to_string(),
756            format!("Program {} invoke [1]", router_program),
757            format!("Program {} invoke [2]", target_program),
758            "Program data: Y3BpX2RhdGE=".to_string(), // "cpi_data" in base64
759            format!("Program {} success", target_program),
760            format!("Program {} success", router_program),
761        ];
762
763        let metadata = create_metadata_with_message(
764            vec![2, 0],
765            2,
766            logs,
767            vec![
768                Pubkey::new_unique(),
769                compute_budget,
770                ed25519,
771                router_program,
772                target_program,
773            ],
774            vec![
775                CompiledInstruction {
776                    program_id_index: 1,
777                    accounts: vec![],
778                    data: vec![],
779                },
780                CompiledInstruction {
781                    program_id_index: 2,
782                    accounts: vec![],
783                    data: vec![],
784                },
785                CompiledInstruction {
786                    program_id_index: 3,
787                    accounts: vec![],
788                    data: vec![],
789                },
790            ],
791        );
792
793        let extracted = metadata.extract_event_log_data();
794        assert_eq!(extracted.len(), 1);
795        assert_eq!(extracted[0], b"cpi_data");
796    }
797
798    #[test]
799    fn test_extract_event_log_data_cpi_with_multiple_precompiles_and_instructions() {
800        use solana_message::compiled_instruction::CompiledInstruction;
801
802        let compute_budget = Pubkey::new_unique();
803        let ed25519 = Pubkey::from_str("Ed25519SigVerify111111111111111111111111111").unwrap();
804        let secp256k1 = Pubkey::from_str("KeccakSecp256k11111111111111111111111111111").unwrap();
805        let router_program = Pubkey::new_unique();
806        let swap_program = Pubkey::new_unique();
807        let token_program = Pubkey::new_unique();
808
809        // Transaction structure:
810        // ix 0: ComputeBudget (logs invoke [1])
811        // ix 1: Ed25519 precompile
812        // ix 2: Secp256k1 precompile
813        // ix 3: Router program (logs invoke [1])
814        //   -> CPI to swap_program (logs invoke [2])
815        //      -> CPI to token_program (logs invoke [3])
816        //   -> CPI to swap_program again (logs invoke [2])
817        //
818        // Log positions (ignoring precompiles):
819        // Position [0] = ComputeBudget
820        // Position [1] = Router (message index 3, but log position 1 due to 2
821        // precompiles) Position [1, 0] = first swap CPI
822        // Position [1, 0, 0] = token CPI inside first swap
823        // Position [1, 1] = second swap CPI
824
825        let logs = vec![
826            "Program ComputeBudget111111111111111111111111111 invoke [1]".to_string(),
827            "Program ComputeBudget111111111111111111111111111 success".to_string(),
828            format!("Program {} invoke [1]", router_program),
829            format!("Program {} invoke [2]", swap_program),
830            format!("Program {} invoke [3]", token_program),
831            "Program data: dG9rZW5fZGF0YQ==".to_string(), // "token_data" in base64
832            format!("Program {} success", token_program),
833            "Program data: c3dhcF9kYXRhXzE=".to_string(), // "swap_data_1" in base64
834            format!("Program {} success", swap_program),
835            format!("Program {} invoke [2]", swap_program),
836            "Program data: c3dhcF9kYXRhXzI=".to_string(), // "swap_data_2" in base64
837            format!("Program {} success", swap_program),
838            "Program data: cm91dGVyX2RhdGE=".to_string(), // "router_data" in base64
839            format!("Program {} success", router_program),
840        ];
841
842        let account_keys = vec![
843            Pubkey::new_unique(),
844            compute_budget,
845            ed25519,
846            secp256k1,
847            router_program,
848            swap_program,
849            token_program,
850        ];
851
852        let instructions = vec![
853            CompiledInstruction {
854                program_id_index: 1,
855                accounts: vec![],
856                data: vec![],
857            },
858            CompiledInstruction {
859                program_id_index: 2,
860                accounts: vec![],
861                data: vec![],
862            },
863            CompiledInstruction {
864                program_id_index: 3,
865                accounts: vec![],
866                data: vec![],
867            },
868            CompiledInstruction {
869                program_id_index: 4,
870                accounts: vec![],
871                data: vec![],
872            },
873        ];
874
875        let router_metadata = create_metadata_with_message(
876            vec![3],
877            1,
878            logs.clone(),
879            account_keys.clone(),
880            instructions.clone(),
881        );
882        let router_extracted = router_metadata.extract_event_log_data();
883        assert_eq!(router_extracted.len(), 1);
884        assert_eq!(router_extracted[0], b"router_data");
885
886        let first_swap_metadata = create_metadata_with_message(
887            vec![3, 0],
888            2,
889            logs.clone(),
890            account_keys.clone(),
891            instructions.clone(),
892        );
893        let first_swap_extracted = first_swap_metadata.extract_event_log_data();
894        assert_eq!(first_swap_extracted.len(), 1);
895        assert_eq!(first_swap_extracted[0], b"swap_data_1");
896
897        let token_metadata = create_metadata_with_message(
898            vec![3, 0, 0],
899            3,
900            logs.clone(),
901            account_keys.clone(),
902            instructions.clone(),
903        );
904        let token_extracted = token_metadata.extract_event_log_data();
905        assert_eq!(token_extracted.len(), 1);
906        assert_eq!(token_extracted[0], b"token_data");
907
908        let second_swap_metadata = create_metadata_with_message(
909            vec![3, 1],
910            2,
911            logs.clone(),
912            account_keys.clone(),
913            instructions.clone(),
914        );
915        let second_swap_extracted = second_swap_metadata.extract_event_log_data();
916        assert_eq!(second_swap_extracted.len(), 1);
917        assert_eq!(second_swap_extracted[0], b"swap_data_2");
918
919        assert_eq!(router_metadata.count_precompiles_before_index(), 2);
920        assert_eq!(first_swap_metadata.count_precompiles_before_index(), 2);
921    }
922}